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 719f4347779..12e7bed3f46 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ conf/custom.ini fig.yml docker-compose.yml docker-compose.yaml +/conf/provisioning/**/custom.yaml profile.cov /grafana .notouch @@ -50,6 +51,9 @@ debug.test /packaging/**/*.rpm /packaging/**/*.deb +# Ignore OSX indexing +.DS_Store + /vendor/**/*.py /vendor/**/*.xml /vendor/**/*.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a75ad758c8..51bb6f0c199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,31 @@ -# 5.0.0 (unreleased) +# 5.0.0 (unreleased / master branch) -### WIP (in develop branch currently as its unstable or unfinished) -- Dashboard folders -- User groups -- Dashboard permissions (on folder & dashboard level), permissions can be assigned to groups or individual users -- UX changes to nav & side menu -- New dashboard grid layout system +Grafana v5.0 is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5. -# 4.7.0 (unreleased) +### New Features +- **Dashboards** Dashboard folders, [#1611](https://github.com/grafana/grafana/issues/1611) +- **Teams** User groups (teams) implemented. Can be used in folder & dashboard permission list. +- **Dashboard grid**: Panels are now layed out in a two dimensional grid (with x, y, w, h). [#9093](https://github.com/grafana/grafana/issues/9093). +- **Templating**: Vertical repeat direction for panel repeats. +- **UX**: Major update to page header and navigation +- **Dashboard settings**: Combine dashboard settings views into one with side menu, [#9750](https://github.com/grafana/grafana/issues/9750) + +## New Dashboard Grid + +The new grid engine is major upgrade for how you can position and move panels. It enables new layouts and a much easier dashboard building experience. The change is backwards compatible. Grafana will automatically upgrade your dashboards to the new schema and position panels to match your existing layout. There might be minor differences in panel height. + +Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w: 24, h: 5}`. Units are in grid dimensions (24 columns, 1 height unit 30px). Rows and Panels objects exist (together) in a flat array directly on the dashboard root object. Rows are not needed for layouts anymore and are mainly there for backward compatibility. Some panel plugins that do not respect their panel height might require an update. + +# 4.7.0 (unreleased / v4.7.x branch) + +## Breaking changes + +`[dashboard.json]` have been replaced with [dashboard provisioning](http://docs.grafana.org/administration/provisioning/). + +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) @@ -17,27 +35,38 @@ * **Datasources**: Its now possible to configure datasources with config files [#1789](https://github.com/grafana/grafana/issues/1789) * **Graphite**: Query editor updated to support new query by tag features [#9230](https://github.com/grafana/grafana/issues/9230) * **Dashboard history**: New config file option versions_to_keep sets how many versions per dashboard to store, [#9671](https://github.com/grafana/grafana/issues/9671) - +* **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) * **Cloudwatch**: Fixes broken query inspector for cloudwatch [#9661](https://github.com/grafana/grafana/issues/9661), thx [@mtanda](https://github.com/mtanda) * **Dashboard**: Make it possible to start dashboards from search and dashboard list panel [#1871](https://github.com/grafana/grafana/issues/1871) * **Annotations**: Posting annotations now return the id of the annotation [#9798](https://github.com/grafana/grafana/issues/9798) +* **Systemd**: Use systemd notification ready flag [#10024](https://github.com/grafana/grafana/issues/10024), thx [@jgrassler](https://github.com/jgrassler) +* **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) ## Tech * **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645) + ## Fixes * **Sensu**: Send alert message to sensu output [#9551](https://github.com/grafana/grafana/issues/9551), thx [@cjchand](https://github.com/cjchand) * **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu) * **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm) +* **Pagerduty**: Pagerduty dont auto resolve incidents by default anymore. [#10222](https://github.com/grafana/grafana/issues/10222) -# 4.6.3 (unreleased) +# 4.6.3 (2017-12-14) ## Fixes * **Gzip**: Fixes bug gravatar images when gzip was enabled [#5952](https://github.com/grafana/grafana/issues/5952) * **Alert list**: Now shows alert state changes even after adding manual annotations on dashboard [#9951](https://github.com/grafana/grafana/issues/9951) +* **Alerting**: Fixes bug where rules evaluated as firing when all conditions was false and using OR operator. [#9318](https://github.com/grafana/grafana/issues/9318) +* **Cloudwatch**: CloudWatch no longer display metrics' default alias [#10151](https://github.com/grafana/grafana/issues/10151), thx [@mtanda](https://github.com/mtanda) # 4.6.2 (2017-11-16) diff --git a/README.md b/README.md index aefc0c0802b..069958d9031 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB. ![](http://docs.grafana.org/assets/img/features/dashboard_ex1.png) +## Grafana v5 Alpha Preview +Grafana master is now v5.0 alpha. This is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5. + ## Installation Head to [docs.grafana.org](http://docs.grafana.org/installation/) and [download](https://grafana.com/get) the latest release. @@ -19,7 +22,7 @@ If you have any problems please read the [troubleshooting guide](http://docs.gra Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides. ## Run from master -If you want to build a package yourself, or contribute. Here is a guide for how to do that. You can always find +If you want to build a package yourself, or contribute - Here is a guide for how to do that. You can always find the latest master builds [here](https://grafana.com/grafana/download) ### Dependencies @@ -97,7 +100,7 @@ Writing & watching frontend tests (we have two test runners) ## Contribute -If you have any idea for an improvement or found a bug do not hesitate to open an issue. +If you have any idea for an improvement or found a bug, do not hesitate to open an issue. And if you have time clone this repo and submit a pull request and help me make Grafana the kickass metrics & devops dashboard we all dream about! diff --git a/ROADMAP.md b/ROADMAP.md index 4273d8df6a9..479c1933bc0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,7 +6,7 @@ But it will give you an idea of our current vision and plan. ### Short term (1-4 months) - Release Grafana v5 - - User groups + - Teams - Dashboard folders - Dashboard & folder permissions (assigned to users or groups) - New Dashboard layout engine diff --git a/build.go b/build.go index 6eb4ea63a91..3c63c34dd63 100644 --- a/build.go +++ b/build.go @@ -99,9 +99,9 @@ func main() { case "package": grunt(gruntBuildArg("release")...) - if runtime.GOOS != "windows" { - createLinuxPackages() - } + if runtime.GOOS != "windows" { + createLinuxPackages() + } case "pkg-rpm": grunt(gruntBuildArg("release")...) diff --git a/conf/defaults.ini b/conf/defaults.ini index a145d57482b..4e2929096a6 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -20,8 +20,8 @@ logs = data/log # Directory where grafana will automatically scan and look for plugins plugins = data/plugins -# Config files containing datasources that will be configured at startup -datasources = conf/datasources +# folder that contains provisioning config files that grafana will apply on startup and while running. +provisioning = conf/provisioning #################################### Server ############################## [server] @@ -221,6 +221,9 @@ external_manage_link_url = external_manage_link_name = external_manage_info = +# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard. +viewers_can_edit = false + [auth] # Set to true to disable (hide) the login form, useful if you use OAuth disable_login_form = false @@ -391,11 +394,6 @@ facility = # Syslog tag. By default, the process' argv[0] is used. tag = -#################################### Dashboard JSON files ################ -[dashboards.json] -enabled = false -path = /var/lib/grafana/dashboards - #################################### Usage Quotas ######################## [quota] enabled = false @@ -475,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] @@ -495,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/provisioning/dashboards/sample.yaml b/conf/provisioning/dashboards/sample.yaml new file mode 100644 index 00000000000..40992d1461e --- /dev/null +++ b/conf/provisioning/dashboards/sample.yaml @@ -0,0 +1,6 @@ +# - name: 'default' +# org_id: 1 +# folder: '' +# type: file +# options: +# folder: /var/lib/grafana/dashboards \ No newline at end of file diff --git a/conf/datasources/datasources.yaml b/conf/provisioning/datasources/sample.yaml similarity index 83% rename from conf/datasources/datasources.yaml rename to conf/provisioning/datasources/sample.yaml index d8ddc9c6bed..1bb9cb53b45 100644 --- a/conf/datasources/datasources.yaml +++ b/conf/provisioning/datasources/sample.yaml @@ -1,11 +1,11 @@ -# list of datasources that should be deleted from the database -delete_datasources: - # - name: Graphite - # org_id: 1 +# # list of datasources that should be deleted from the database +#delete_datasources: +# - name: Graphite +# org_id: 1 -# list of datasources to insert/update depending -# whats available in the datbase -datasources: +# # list of datasources to insert/update depending +# # whats available in the datbase +#datasources: # # name of the datasource. Required # - name: Graphite # # datasource type. Required @@ -33,7 +33,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 diff --git a/conf/sample.ini b/conf/sample.ini index 233a97deef8..d297d2db66a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -20,8 +20,8 @@ # Directory where grafana will automatically scan and look for plugins ;plugins = /var/lib/grafana/plugins -# Config files containing datasources that will be configured at startup -;datasources = conf/datasources +# folder that contains provisioning config files that grafana will apply on startup and while running. +; provisioning = conf/provisioning #################################### Server #################################### [server] @@ -205,6 +205,9 @@ log_queries = ;external_manage_link_name = ;external_manage_info = +# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard. +;viewers_can_edit = false + [auth] # Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false ;disable_login_form = false @@ -367,11 +370,6 @@ log_queries = ;tag = -;#################################### Dashboard JSON files ########################## -[dashboards.json] -;enabled = false -;path = /var/lib/grafana/dashboards - #################################### Alerting ############################ [alerting] # Disable alerting engine & UI features @@ -419,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] @@ -438,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/graphite/docker-compose.yaml b/docker/blocks/graphite/docker-compose.yaml index 2bd0dc322cc..606e28638f7 100644 --- a/docker/blocks/graphite/docker-compose.yaml +++ b/docker/blocks/graphite/docker-compose.yaml @@ -1,4 +1,4 @@ - graphite: + graphite09: build: blocks/graphite ports: - "8080:80" diff --git a/docker/blocks/mysql_tests/docker-compose.yaml b/docker/blocks/mysql_tests/docker-compose.yaml index 646cc7ee369..c6c3097d463 100644 --- a/docker/blocks/mysql_tests/docker-compose.yaml +++ b/docker/blocks/mysql_tests/docker-compose.yaml @@ -7,3 +7,4 @@ MYSQL_PASSWORD: password ports: - "3306:3306" + tmpfs: /var/lib/mysql:rw diff --git a/docker/blocks/postgres_tests/docker-compose.yaml b/docker/blocks/postgres_tests/docker-compose.yaml index 3d9a82c034c..44b66e8e558 100644 --- a/docker/blocks/postgres_tests/docker-compose.yaml +++ b/docker/blocks/postgres_tests/docker-compose.yaml @@ -5,3 +5,4 @@ POSTGRES_PASSWORD: grafanatest ports: - "5432:5432" + tmpfs: /var/lib/postgresql/data:rw \ No newline at end of file diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 70d9d7a81f3..3dd5c5fd1d3 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -65,15 +65,16 @@ Currently we do not provide any scripts/manifests for configuring Grafana. Rathe Tool | Project -----|------------ Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana) +Ansible | [https://github.com/cloudalchemy/ansible-grafana](https://github.com/cloudalchemy/ansible-grafana) Ansible | [https://github.com/picotrading/ansible-grafana](https://github.com/picotrading/ansible-grafana) Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana) Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana) ## Datasources -> This feature is available from v4.7 +> This feature is available from v5.0 -It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`conf/datasources`](/installation/configuration/#datasources) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list. +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. @@ -164,3 +165,20 @@ Secure json data is a map of settings that will be encrypted with [secret key](/ | tlsClientKey | string | *All* |TLS Client key for outgoing requests | | 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. + +The dashboard provider config file looks like this + +```yaml +- name: 'default' + org_id: 1 + folder: '' + type: file + options: + 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 diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 102134f34dd..a0673aaea98 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -126,30 +126,31 @@ There are couple of configurations options which need to be set in Grafana UI un Once these two properties are set, you can send the alerts to Kafka for further processing or throttling them. -### Other Supported Notification Channels +### All supported notifier -Grafana also supports the following Notification Channels: +Name | Type |Support images +-----|------------ | ------ +Slack | `slack` | yes +Pagerduty | `pagerduty` | yes +Email | `email` | yes +Webhook | `webhook` | link +Kafka | `kafka` | no +Hipchat | `hipchat` | yes +VictorOps | `victorops` | yes +Sensu | `sensu` | yes +OpsGenie | `opsgenie` | yes +Threema | `threema` | yes +Pushover | `pushover` | no +Telegram | `telegram` | no +Line | `line` | no +Prometheus Alertmanager | `prometheus-alertmanager` | no -- HipChat -- VictorOps - -- Sensu - -- OpsGenie - -- Threema - -- Pushover - -- Telegram - -- LINE # 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. diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index bdf661dc4fc..648957ed96e 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -78,11 +78,14 @@ CloudWatch Datasource Plugin provides the following queries you can specify in t edit view. They allow you to fill a variable's options list with things like `region`, `namespaces`, `metric names` and `dimension keys/values`. +In place of `region` you can specify `default` to use the default region configured in the datasource for the query, +e.g. `metrics(AWS/DynamoDB, default)` or `dimension_values(default, ..., ..., ...)`. + Name | Description ------- | -------- *regions()* | Returns a list of regions AWS provides their service. *namespaces()* | Returns a list of namespaces CloudWatch support. -*metrics(namespace, [region])* | Returns a list of metrics in the namespace. (specify region for custom metrics) +*metrics(namespace, [region])* | Returns a list of metrics in the namespace. (specify region or use "default" for custom metrics) *dimension_keys(namespace)* | Returns a list of dimension keys in the namespace. *dimension_values(region, namespace, metric, dimension_key)* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`. *ebs_volume_ids(region, instance_id)* | Returns a list of volume ids matching the specified `region`, `instance_id`. diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 69c6f667062..7fae7441b6d 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -45,7 +45,14 @@ To simplify syntax and to allow for dynamic parts, like date range filters, the Macro example | Description ------------ | ------------- +*$__time(dateColumn)* | Will be replaced by an expression to convert to a UNIX timestamp and rename the column to `time_sec`. For example, *UNIX_TIMESTAMP(dateColumn) as time_sec* *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > FROM_UNIXTIME(1494410783) AND dateColumn < FROM_UNIXTIME(1494497183)* +*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *FROM_UNIXTIME(1494410783)* +*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *FROM_UNIXTIME(1494497183)* +*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed) as time_sec,* +*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* +*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* +*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo. @@ -99,6 +106,19 @@ GROUP BY metric1, UNIX_TIMESTAMP(time_date_time) DIV 300 ORDER BY time_sec asc ``` +Example with $__timeGroup macro: + +```sql +SELECT + $__timeGroup(time_date_time,'5m') as time_sec, + min(value_double) as value, + metric_name as metric +FROM test_data +WHERE $__timeFilter(time_date_time) +GROUP BY 1, metric_name +ORDER BY 1 +``` + Currently, there is no support for a dynamic group by time based on time range & panel width. This is something we plan to add. @@ -127,6 +147,12 @@ A query can returns multiple columns and Grafana will automatically create a lis SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city ``` +To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*. + +```sql +SELECT event_name FROM event_log WHERE $__timeFilter(time_column) +``` + Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value: ```sql diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 2b111d51f0e..7d52df2fd3e 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -48,7 +48,7 @@ Macro example | Description *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *extract(epoch from dateColumn) BETWEEN 1494410783 AND 1494497183* *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *to_timestamp(1494410783)* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *to_timestamp(1494497183)* -*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from "dateColumn")/300)::bigint*300* +*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time* *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* @@ -94,7 +94,7 @@ Example with `metric` column ```sql SELECT - $__timeGroup(time_date_time,'5m') as time, + $__timeGroup(time_date_time,'5m'), min(value_double), 'min' as metric FROM test_data @@ -107,7 +107,7 @@ Example with multiple columns: ```sql SELECT - $__timeGroup(time_date_time,'5m') as time, + $__timeGroup(time_date_time,'5m'), min(value_double) as min_value, max(value_double) as max_value FROM test_data @@ -139,6 +139,12 @@ A query can return multiple columns and Grafana will automatically create a list SELECT host.hostname, other_host.hostname2 FROM host JOIN other_host ON host.city = other_host.city ``` +To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*. + +```sql +SELECT event_name FROM event_log WHERE $__timeFilter(time_column) +``` + Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value: ```sql diff --git a/docs/sources/features/panels/singlestat.md b/docs/sources/features/panels/singlestat.md index 5e2cb36600b..510642337ff 100644 --- a/docs/sources/features/panels/singlestat.md +++ b/docs/sources/features/panels/singlestat.md @@ -47,7 +47,7 @@ The coloring options of the Singlestat Panel config allow you to dynamically cha 2. **Thresholds**: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90. 3. **Colors**: Select a color and opacity 4. **Value**: This checkbox applies the configured thresholds and colors to the summary stat. -5. **Invert order**: This link toggles the threshold color order.
For example: Green, Orange, Red () will become Red, Orange, Green (). +5. **Invert order**: This link toggles the threshold color order.
For example: Green, Orange, Red () will become Red, Orange, Green (). ### Spark Lines 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/http_api/alerting.md b/docs/sources/http_api/alerting.md index e66218bb066..221552414e9 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -196,6 +196,8 @@ Content-Type: application/json ## Create alert notification +You can find the full list of [supported notifers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page. + `POST /api/alert-notifications` **Example Request**: diff --git a/docs/sources/http_api/auth.md b/docs/sources/http_api/auth.md index b526031fdeb..166a5a4fdb9 100644 --- a/docs/sources/http_api/auth.md +++ b/docs/sources/http_api/auth.md @@ -100,7 +100,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk JSON Body schema: - **name** – The key name -- **role** – Sets the access level/Grafana Role for the key. Can be one of the following values: `Viewer`, `Editor`, `Read Only Editor` or `Admin`. +- **role** – Sets the access level/Grafana Role for the key. Can be one of the following values: `Viewer`, `Editor` or `Admin`. **Example Response**: diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md index ba8afd4db22..134c1842851 100644 --- a/docs/sources/http_api/user.md +++ b/docs/sources/http_api/user.md @@ -156,7 +156,7 @@ HTTP/1.1 200 Content-Type: application/json { - "email": "user@mygraf.com" + "email": "user@mygraf.com", "name": "admin", "login": "admin", "theme": "light", @@ -409,4 +409,4 @@ HTTP/1.1 200 Content-Type: application/json {"message":"Dashboard unstarred"} -``` \ No newline at end of file +``` diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 483774f94f5..1e99d487c2b 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -91,9 +91,11 @@ file. Directory where grafana will automatically scan and look for plugins -### datasources +### provisioning -Config files containing datasources that will be configured at startup +> This feature is available in 5.0+ + +Folder that contains [provisioning](/administration/provisioning) config files that grafana will apply on startup. Dashboards will be reloaded when the json files changes ## [server] @@ -203,7 +205,7 @@ The database user (not applicable for `sqlite3`). ### password -The database user's password (not applicable for `sqlite3`). If the password contains `#` or `;` you have to wrap it with trippel quotes. Ex `"""#password;"""` +The database user's password (not applicable for `sqlite3`). If the password contains `#` or `;` you have to wrap it with triple quotes. Ex `"""#password;"""` ### ssl_mode @@ -212,19 +214,19 @@ For MySQL, use either `true`, `false`, or `skip-verify`. ### ca_cert_path -(MySQL only) The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`. +The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`. ### client_key_path -(MySQL only) The path to the client key. Only if server requires client authentication. +The path to the client key. Only if server requires client authentication. ### client_cert_path -(MySQL only) The path to the client cert. Only if server requires client authentication. +The path to the client cert. Only if server requires client authentication. ### server_cert_name -(MySQL only) The common name field of the certificate used by the `mysql` server. Not necessary if `ssl_mode` is set to `skip-verify`. +The common name field of the certificate used by the `mysql` or `postgres` server. Not necessary if `ssl_mode` is set to `skip-verify`. ### max_idle_conn The maximum number of connections in the idle connection pool. @@ -290,10 +292,14 @@ organization to be created for that new user. The role new users will be assigned for the main organization (if the above setting is set to true). Defaults to `Viewer`, other valid -options are `Admin` and `Editor` and `Read Only Editor`. e.g. : +options are `Admin` and `Editor`. e.g. : -`auto_assign_org_role = Read Only Editor` +`auto_assign_org_role = Viewer` +### viewers can edit + +Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard. +Defaults to `false`.
@@ -632,8 +638,7 @@ Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1. ## [dashboards.json] -If you have a system that automatically builds dashboards as json files you can enable this feature to have the -Grafana backend index those json dashboards which will make them appear in regular dashboard search. +> This have been replaced with dashboards [provisioning](/administration/provisioning) in 5.0+ ### enabled `true` or `false`. Is disabled by default. @@ -726,7 +731,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] @@ -781,6 +786,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/installation/debian.md b/docs/sources/installation/debian.md index d832ea7a8ed..b742e96c869 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -15,9 +15,7 @@ weight = 1 Description | Download ------------ | ------------- -Stable for Debian-based Linux | [grafana_4.6.2_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.2_amd64.deb) - - +Stable for Debian-based Linux | [grafana_4.6.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -26,21 +24,10 @@ installation. ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.2_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_4.6.2_amd64.deb +sudo dpkg -i grafana_4.6.3_amd64.deb ``` - - - ## APT Repository Add the following line to your `/etc/apt/sources.list` file. diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index fa2bd71ed07..d3e796a78c8 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -15,9 +15,7 @@ weight = 2 Description | Download ------------ | ------------- -Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.2 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2-1.x86_64.rpm) - - +Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -27,7 +25,7 @@ installation. You can install Grafana using Yum directly. ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2-1.x86_64.rpm +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm ``` Or install manually using `rpm`. @@ -35,15 +33,15 @@ Or install manually using `rpm`. #### On CentOS / Fedora / Redhat: ```bash -$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2-1.x86_64.rpm +$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm $ sudo yum install initscripts fontconfig -$ sudo rpm -Uvh grafana-4.6.2-1.x86_64.rpm +$ sudo rpm -Uvh grafana-4.6.3-1.x86_64.rpm ``` #### On OpenSuse: ```bash -$ sudo rpm -i --nodeps grafana-4.6.2-1.x86_64.rpm +$ sudo rpm -i --nodeps grafana-4.6.3-1.x86_64.rpm ``` ## Install via YUM Repository diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 9cfd689fb43..7c6a97085df 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -13,7 +13,7 @@ weight = 3 Description | Download ------------ | ------------- -Latest stable package for Windows | [grafana.4.6.2.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2.windows-x64.zip) +Latest stable package for Windows | [grafana.4.6.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3.windows-x64.zip) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. 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/jest.config.js b/jest.config.js index cbe77c4e30f..ead97e39dad 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,6 @@ module.exports = { - verbose: true, + verbose: false, "globals": { "ts-jest": { "tsConfigFile": "tsconfig.json" diff --git a/package.json b/package.json index 9a6edfb8be9..509b9bb106d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "4.7.0-pre1", + "version": "5.0.0-pre1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" @@ -14,8 +14,8 @@ "@types/enzyme": "^2.8.9", "@types/jest": "^21.1.4", "@types/node": "^8.0.31", - "@types/react": "^16.0.5", - "@types/react-dom": "^15.5.4", + "@types/react": "^16.0.25", + "@types/react-dom": "^16.0.3", "angular-mocks": "^1.6.6", "autoprefixer": "^6.4.0", "awesome-typescript-loader": "^3.2.3", @@ -65,7 +65,7 @@ "karma-sinon": "^1.0.5", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^2.0.4", - "lint-staged": "^4.2.3", + "lint-staged": "^6.0.0", "load-grunt-tasks": "3.5.2", "mocha": "^4.0.1", "ng-annotate-loader": "^0.6.1", @@ -76,7 +76,7 @@ "postcss-browser-reporter": "^0.5.0", "postcss-loader": "^2.0.6", "postcss-reporter": "^5.0.0", - "prettier": "1.7.3", + "prettier": "1.9.2", "react-test-renderer": "^16.0.0", "sass-lint": "^1.10.2", "sass-loader": "^6.0.6", @@ -103,7 +103,22 @@ "lint": "tslint -c tslint.json --project tsconfig.json --type-check", "karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev", "jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch", - "precommit": "node ./node_modules/grunt-cli/bin/grunt precommit" + "precommit": "lint-staged && node ./node_modules/grunt-cli/bin/grunt precommit" + }, + "lint-staged": { + "*.{ts,tsx}": [ + "prettier --write", + "git add" + ], + "*.scss": [ + "prettier --write", + "git add" + ] + }, + "prettier": { + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 120 }, "license": "Apache-2.0", "dependencies": { @@ -115,22 +130,26 @@ "angular-sanitize": "^1.6.6", "babel-polyfill": "^6.26.0", "brace": "^0.10.0", + "classnames": "^2.2.5", "clipboard": "^1.7.1", - "eventemitter3": "^2.0.3", + "d3": "^4.11.0", + "d3-scale-chromatic": "^1.1.1", + "eventemitter3": "^2.0.2", "file-saver": "^1.3.3", "jquery": "^3.2.1", "lodash": "^4.17.4", "moment": "^2.18.1", "mousetrap": "^1.6.0", - "ngreact": "^0.4.1", - "react": "^16.0.0", - "react-dom": "^16.0.0", + "perfect-scrollbar": "^1.2.0", + "prop-types": "^15.6.0", + "react": "^16.1.1", + "react-dom": "^16.1.1", + "react-grid-layout": "^0.16.1", + "react-sizeme": "^2.3.6", "remarkable": "^1.7.1", "rxjs": "^5.4.3", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop", - "tinycolor2": "^1.4.1", - "d3": "^4.11.0", - "d3-scale-chromatic": "^1.1.1" + "tinycolor2": "^1.4.1" } } diff --git a/packaging/deb/control/postinst b/packaging/deb/control/postinst index 8e25a0e4124..351c966a8e6 100755 --- a/packaging/deb/control/postinst +++ b/packaging/deb/control/postinst @@ -31,6 +31,12 @@ case "$1" in cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml fi + if [ ! -f $PROVISIONING_CFG_DIR ]; then + mkdir -p $PROVISIONING_CFG_DIR/dashboards $PROVISIONING_CFG_DIR/datasources + cp /usr/share/grafana/conf/provisioning/dashboards/sample.yaml $PROVISIONING_CFG_DIR/dashboards/sample.yaml + cp /usr/share/grafana/conf/provisioning/datasources/sample.yaml $PROVISIONING_CFG_DIR/datasources/sample.yaml + fi + # configuration files should not be modifiable by grafana user, as this can be a security issue chown -Rh root:$GRAFANA_GROUP /etc/grafana/* chmod 755 /etc/grafana diff --git a/packaging/deb/default/grafana-server b/packaging/deb/default/grafana-server index eaa75830d44..eb77e62d774 100644 --- a/packaging/deb/default/grafana-server +++ b/packaging/deb/default/grafana-server @@ -18,5 +18,7 @@ RESTART_ON_UPGRADE=true PLUGINS_DIR=/var/lib/grafana/plugins +PROVISIONING_CFG_DIR=/etc/grafana/provisioning + # Only used on systemd systems PID_FILE_DIR=/var/run/grafana diff --git a/packaging/deb/init.d/grafana-server b/packaging/deb/init.d/grafana-server index 85b0e412d35..567da94f881 100755 --- a/packaging/deb/init.d/grafana-server +++ b/packaging/deb/init.d/grafana-server @@ -33,6 +33,7 @@ DATA_DIR=/var/lib/grafana PLUGINS_DIR=/var/lib/grafana/plugins LOG_DIR=/var/log/grafana CONF_FILE=$CONF_DIR/grafana.ini +PROVISIONING_CFG_DIR=$CONF_DIR/provisioning MAX_OPEN_FILES=10000 PID_FILE=/var/run/$NAME.pid DAEMON=/usr/sbin/$NAME @@ -55,7 +56,7 @@ if [ -f "$DEFAULT" ]; then . "$DEFAULT" fi -DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" +DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" function checkUser() { if [ `id -u` -ne 0 ]; then diff --git a/packaging/deb/systemd/grafana-server.service b/packaging/deb/systemd/grafana-server.service index cb7b87932d1..acd2a360a93 100644 --- a/packaging/deb/systemd/grafana-server.service +++ b/packaging/deb/systemd/grafana-server.service @@ -14,12 +14,15 @@ Restart=on-failure WorkingDirectory=/usr/share/grafana RuntimeDirectory=grafana RuntimeDirectoryMode=0750 -ExecStart=/usr/sbin/grafana-server \ - --config=${CONF_FILE} \ - --pidfile=${PID_FILE_DIR}/grafana-server.pid \ - cfg:default.paths.logs=${LOG_DIR} \ - cfg:default.paths.data=${DATA_DIR} \ - cfg:default.paths.plugins=${PLUGINS_DIR} +ExecStart=/usr/sbin/grafana-server \ + --config=${CONF_FILE} \ + --pidfile=${PID_FILE_DIR}/grafana-server.pid \ + cfg:default.paths.logs=${LOG_DIR} \ + cfg:default.paths.data=${DATA_DIR} \ + cfg:default.paths.plugins=${PLUGINS_DIR} \ + cfg:default.paths.provisioning=${PROVISIONING_CFG_DIR} + + LimitNOFILE=10000 TimeoutStopSec=20 UMask=0027 diff --git a/packaging/mac/bin/grafana b/packaging/mac/bin/grafana index fb33079079e..74f4b00662b 100755 --- a/packaging/mac/bin/grafana +++ b/packaging/mac/bin/grafana @@ -6,10 +6,12 @@ HOMEPATH=/usr/local/share/grafana LOGPATH=/usr/local/var/log/grafana DATAPATH=/usr/local/var/lib/grafana PLUGINPATH=/usr/local/var/lib/grafana/plugins +DATASOURCECFGPATH=/usr/local/etc/grafana/datasources +DASHBOARDSCFGPATH=/usr/local/etc/grafana/dashboards case "$1" in start) - $EXECUTABLE --config=$CONFIG --homepath=$HOMEPATH cfg:default.paths.logs=$LOGPATH cfg:default.paths.data=$DATAPATH cfg:default.paths.plugins=$PLUGINPATH 2> /dev/null & + $EXECUTABLE --config=$CONFIG --homepath=$HOMEPATH cfg:default.paths.datasources=$DATASOURCECFGPATH cfg:default.paths.dashboards=$DASHBOARDSCFGPATH cfg:default.paths.logs=$LOGPATH cfg:default.paths.data=$DATAPATH cfg:default.paths.plugins=$PLUGINPATH 2> /dev/null & [ $? -eq 0 ] && echo "$DAEMON started" ;; stop) diff --git a/packaging/publish/publish_both.sh b/packaging/publish/publish_both.sh index 6c4f5a5c29a..9736cbddd6c 100755 --- a/packaging/publish/publish_both.sh +++ b/packaging/publish/publish_both.sh @@ -1,5 +1,5 @@ #! /usr/bin/env bash -version=4.6.2 +version=4.6.3 wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb diff --git a/packaging/rpm/control/postinst b/packaging/rpm/control/postinst index 0bfca949e7f..e75850f258e 100755 --- a/packaging/rpm/control/postinst +++ b/packaging/rpm/control/postinst @@ -45,6 +45,12 @@ if [ $1 -eq 1 ] ; then cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml fi + if [ ! -f $PROVISIONING_CFG_DIR ]; then + mkdir -p $PROVISIONING_CFG_DIR/dashboards $PROVISIONING_CFG_DIR/datasources + cp /usr/share/grafana/conf/provisioning/dashboards/sample.yaml $PROVISIONING_CFG_DIR/dashboards/sample.yaml + cp /usr/share/grafana/conf/provisioning/datasources/sample.yaml $PROVISIONING_CFG_DIR/datasources/sample.yaml + fi + # Set user permissions on /var/log/grafana, /var/lib/grafana mkdir -p /var/log/grafana /var/lib/grafana chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana diff --git a/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server index dc63e1ef4c6..cefe212116c 100755 --- a/packaging/rpm/init.d/grafana-server +++ b/packaging/rpm/init.d/grafana-server @@ -32,6 +32,7 @@ DATA_DIR=/var/lib/grafana PLUGINS_DIR=/var/lib/grafana/plugins LOG_DIR=/var/log/grafana CONF_FILE=$CONF_DIR/grafana.ini +PROVISIONING_CFG_DIR=$CONF_DIR/provisioning MAX_OPEN_FILES=10000 PID_FILE=/var/run/$NAME.pid DAEMON=/usr/sbin/$NAME @@ -59,7 +60,7 @@ fi # overwrite settings from default file [ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME -DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" +DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" function isRunning() { status -p $PID_FILE $NAME > /dev/null 2>&1 diff --git a/packaging/rpm/sysconfig/grafana-server b/packaging/rpm/sysconfig/grafana-server index eaa75830d44..eb77e62d774 100644 --- a/packaging/rpm/sysconfig/grafana-server +++ b/packaging/rpm/sysconfig/grafana-server @@ -18,5 +18,7 @@ RESTART_ON_UPGRADE=true PLUGINS_DIR=/var/lib/grafana/plugins +PROVISIONING_CFG_DIR=/etc/grafana/provisioning + # Only used on systemd systems PID_FILE_DIR=/var/run/grafana diff --git a/packaging/rpm/systemd/grafana-server.service b/packaging/rpm/systemd/grafana-server.service index 3e018e8b176..f228c8d8b14 100644 --- a/packaging/rpm/systemd/grafana-server.service +++ b/packaging/rpm/systemd/grafana-server.service @@ -9,17 +9,19 @@ After=postgresql.service mariadb.service mysql.service EnvironmentFile=/etc/sysconfig/grafana-server User=grafana Group=grafana -Type=simple +Type=notify Restart=on-failure WorkingDirectory=/usr/share/grafana RuntimeDirectory=grafana RuntimeDirectoryMode=0750 -ExecStart=/usr/sbin/grafana-server \ - --config=${CONF_FILE} \ - --pidfile=${PID_FILE_DIR}/grafana-server.pid \ - cfg:default.paths.logs=${LOG_DIR} \ - cfg:default.paths.data=${DATA_DIR} \ - cfg:default.paths.plugins=${PLUGINS_DIR} +ExecStart=/usr/sbin/grafana-server \ + --config=${CONF_FILE} \ + --pidfile=${PID_FILE_DIR}/grafana-server.pid \ + cfg:default.paths.logs=${LOG_DIR} \ + cfg:default.paths.data=${DATA_DIR} \ + cfg:default.paths.plugins=${PLUGINS_DIR} \ + cfg:default.paths.provisioning=${PROVISIONING_CFG_DIR} + LimitNOFILE=10000 TimeoutStopSec=20 diff --git a/pkg/api/api.go b/pkg/api/api.go index 1c57a3e3dba..ea082ff4741 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -40,9 +40,14 @@ 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", 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) r.Get("/admin", reqGrafanaAdmin, Index) r.Get("/admin/settings", reqGrafanaAdmin, Index) r.Get("/admin/users", reqGrafanaAdmin, Index) @@ -62,6 +67,7 @@ func (hs *HttpServer) registerRoutes() { r.Get("/dashboard-solo/snapshot/*", Index) r.Get("/dashboard-solo/*", reqSignedIn, Index) r.Get("/import/dashboard", reqSignedIn, Index) + r.Get("/dashboards/", reqSignedIn, Index) r.Get("/dashboards/*", reqSignedIn, Index) r.Get("/playlists/", reqSignedIn, Index) @@ -134,6 +140,18 @@ func (hs *HttpServer) registerRoutes() { usersRoute.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg)) }, reqGrafanaAdmin) + // team (admin permission required) + apiRoute.Group("/teams", func(teamsRoute RouteRegister) { + teamsRoute.Get("/:teamId", wrap(GetTeamById)) + teamsRoute.Get("/search", wrap(SearchTeams)) + teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam)) + teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam)) + teamsRoute.Delete("/:teamId", wrap(DeleteTeamById)) + teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers)) + teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) + teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember)) + }, reqOrgAdmin) + // org information available to all users. apiRoute.Group("/org", func(orgRoute RouteRegister) { orgRoute.Get("/", wrap(GetOrgCurrent)) @@ -224,20 +242,27 @@ func (hs *HttpServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) { - dashboardRoute.Get("/db/:slug", GetDashboard) - dashboardRoute.Delete("/db/:slug", reqEditorRole, DeleteDashboard) - - dashboardRoute.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions)) - dashboardRoute.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion)) - dashboardRoute.Post("/id/:dashboardId/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion)) + dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) + dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard)) dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff)) dashboardRoute.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard)) - dashboardRoute.Get("/file/:file", GetDashboardFromJsonFile) dashboardRoute.Get("/home", wrap(GetHomeDashboard)) dashboardRoute.Get("/tags", GetDashboardTags) dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) + + dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) { + dashIdRoute.Get("/versions", wrap(GetDashboardVersions)) + dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion)) + dashIdRoute.Post("/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion)) + + dashIdRoute.Group("/acl", func(aclRoute RouteRegister) { + aclRoute.Get("/", wrap(GetDashboardAclList)) + aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl)) + aclRoute.Delete("/:aclId", wrap(DeleteDashboardAcl)) + }) + }) }) // Dashboard snapshots diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index fdf93d06b5d..6824e330f00 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -25,6 +25,8 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" "gopkg.in/macaron.v1" + + gocache "github.com/patrickmn/go-cache" ) var gravatarSource string @@ -92,7 +94,7 @@ func (this *Avatar) Update() (err error) { type CacheServer struct { notFound *Avatar - cache map[string]*Avatar + cache *gocache.Cache } func (this *CacheServer) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) { @@ -110,7 +112,9 @@ func (this *CacheServer) Handler(ctx *macaron.Context) { var avatar *Avatar - if avatar, _ = this.cache[hash]; avatar == nil { + if obj, exist := this.cache.Get(hash); exist { + avatar = obj.(*Avatar) + } else { avatar = New(hash) } @@ -124,7 +128,7 @@ func (this *CacheServer) Handler(ctx *macaron.Context) { if avatar.notFound { avatar = this.notFound } else { - this.cache[hash] = avatar + this.cache.Add(hash, avatar, gocache.DefaultExpiration) } ctx.Resp.Header().Add("Content-Type", "image/jpeg") @@ -146,7 +150,7 @@ func NewCacheServer() *CacheServer { return &CacheServer{ notFound: newNotFound(), - cache: make(map[string]*Avatar), + cache: gocache.New(time.Hour, time.Hour*2), } } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index df0cbbd745c..87c42884e31 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -5,7 +5,8 @@ import ( "fmt" "os" "path" - "strings" + + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" @@ -16,8 +17,7 @@ import ( "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/alerting" - "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -35,23 +35,34 @@ func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error) return query.Result, nil } -func GetDashboard(c *middleware.Context) { - slug := strings.ToLower(c.Params(":slug")) - - query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId} - err := bus.Dispatch(&query) +func dashboardGuardianResponse(err error) Response { if err != nil { - c.JsonApiErr(404, "Dashboard not found", nil) - return + return ApiError(500, "Error while checking dashboard permissions", err) + } else { + return ApiError(403, "Access denied to this dashboard", nil) + } +} + +func GetDashboard(c *middleware.Context) Response { + dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0) + if rsp != nil { + return rsp } - isStarred, err := isDashboardStarredByUser(c, query.Result.Id) - if err != nil { - c.JsonApiErr(500, "Error while checking if dashboard was starred by user", err) - return + guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser) + if canView, err := guardian.CanView(); err != nil || !canView { + fmt.Printf("%v", err) + return dashboardGuardianResponse(err) } - dash := query.Result + canEdit, _ := guardian.CanEdit() + canSave, _ := guardian.CanSave() + canAdmin, _ := guardian.CanAdmin() + + isStarred, err := isDashboardStarredByUser(c, dash.Id) + if err != nil { + return ApiError(500, "Error while checking if dashboard was starred by user", err) + } // Finding creator and last updater of the dashboard updater, creator := "Anonymous", "Anonymous" @@ -62,29 +73,44 @@ func GetDashboard(c *middleware.Context) { creator = getUserLogin(dash.CreatedBy) } + meta := dtos.DashboardMeta{ + IsStarred: isStarred, + Slug: dash.Slug, + Type: m.DashTypeDB, + CanStar: c.IsSignedIn, + CanSave: canSave, + CanEdit: canEdit, + CanAdmin: canAdmin, + Created: dash.Created, + Updated: dash.Updated, + UpdatedBy: updater, + CreatedBy: creator, + Version: dash.Version, + HasAcl: dash.HasAcl, + IsFolder: dash.IsFolder, + FolderId: dash.FolderId, + FolderTitle: "Root", + } + + // lookup folder title + if dash.FolderId > 0 { + query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId} + if err := bus.Dispatch(&query); err != nil { + return ApiError(500, "Dashboard folder could not be read", err) + } + meta.FolderTitle = query.Result.Title + } + // make sure db version is in sync with json model version dash.Data.Set("version", dash.Version) dto := dtos.DashboardFullWithMeta{ Dashboard: dash.Data, - Meta: dtos.DashboardMeta{ - IsStarred: isStarred, - Slug: slug, - Type: m.DashTypeDB, - CanStar: c.IsSignedIn, - CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR, - CanEdit: canEditDashboard(c.OrgRole), - Created: dash.Created, - Updated: dash.Updated, - UpdatedBy: updater, - CreatedBy: creator, - Version: dash.Version, - }, + Meta: meta, } - // TODO(ben): copy this performance metrics logic for the new API endpoints added c.TimeRequest(metrics.M_Api_Dashboard_Get) - c.JSON(200, dto) + return Json(200, dto) } func getUserLogin(userId int64) string { @@ -98,24 +124,32 @@ func getUserLogin(userId int64) string { } } -func DeleteDashboard(c *middleware.Context) { - slug := c.Params(":slug") - - query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId} +func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Response) { + query := m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(404, "Dashboard not found", nil) - return + return nil, ApiError(404, "Dashboard not found", err) + } + return query.Result, nil +} + +func DeleteDashboard(c *middleware.Context) Response { + dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0) + if rsp != nil { + return rsp } - cmd := m.DeleteDashboardCommand{Slug: slug, OrgId: c.OrgId} + guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser) + if canSave, err := guardian.CanSave(); err != nil || !canSave { + return dashboardGuardianResponse(err) + } + + cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id} if err := bus.Dispatch(&cmd); err != nil { - c.JsonApiErr(500, "Failed to delete dashboard", err) - return + return ApiError(500, "Failed to delete dashboard", err) } - var resp = map[string]interface{}{"title": query.Result.Title} - - c.JSON(200, resp) + var resp = map[string]interface{}{"title": dash.Title} + return Json(200, resp) } func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { @@ -124,6 +158,15 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { dash := cmd.GetDashboardModel() + guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser) + if canSave, err := guardian.CanSave(); err != nil || !canSave { + return dashboardGuardianResponse(err) + } + + if dash.IsFolder && dash.FolderId > 0 { + return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil) + } + // Check if Title is empty if dash.Title == "" { return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil) @@ -139,17 +182,24 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { } } - validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{ + dashItem := &dashboards.SaveDashboardItem{ + Dashboard: dash, + Message: cmd.Message, OrgId: c.OrgId, UserId: c.UserId, - Dashboard: dash, + Overwrite: cmd.Overwrite, } - if err := bus.Dispatch(&validateAlertsCmd); err != nil { + dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem) + + if err == m.ErrDashboardTitleEmpty { + return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil) + } + + if err == m.ErrDashboardContainsInvalidAlertData { return ApiError(500, "Invalid alert data. Cannot save dashboard", err) } - err := bus.Dispatch(&cmd) if err != nil { if err == m.ErrDashboardWithSameNameExists { return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()}) @@ -171,22 +221,12 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { return ApiError(500, "Failed to save dashboard", err) } - alertCmd := alerting.UpdateDashboardAlertsCommand{ - OrgId: c.OrgId, - UserId: c.UserId, - Dashboard: cmd.Result, - } - - if err := bus.Dispatch(&alertCmd); err != nil { - return ApiError(500, "Failed to save alerts", err) + if err == m.ErrDashboardFailedToUpdateAlertData { + return ApiError(500, "Invalid alert data. Cannot save dashboard", err) } c.TimeRequest(metrics.M_Api_Dashboard_Save) - return Json(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version}) -} - -func canEditDashboard(role m.RoleType) bool { - return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR + return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id}) } func GetHomeDashboard(c *middleware.Context) Response { @@ -214,7 +254,9 @@ func GetHomeDashboard(c *middleware.Context) Response { dash := dtos.DashboardFullWithMeta{} dash.Meta.IsHome = true - dash.Meta.CanEdit = canEditDashboard(c.OrgRole) + dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_EDITOR) + dash.Meta.FolderTitle = "Root" + jsonParser := json.NewDecoder(file) if err := jsonParser.Decode(&dash.Dashboard); err != nil { return ApiError(500, "Failed to load home dashboard", err) @@ -228,55 +270,41 @@ func GetHomeDashboard(c *middleware.Context) Response { } func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) { - rows := dash.Get("rows").MustArray() - row := simplejson.NewFromAny(rows[0]) + panels := dash.Get("panels").MustArray() newpanel := simplejson.NewFromAny(map[string]interface{}{ "type": "gettingstarted", "id": 123123, - "span": 12, + "gridPos": map[string]interface{}{ + "x": 0, + "y": 3, + "w": 24, + "h": 4, + }, }) - panels := row.Get("panels").MustArray() panels = append(panels, newpanel) - row.Set("panels", panels) -} - -func GetDashboardFromJsonFile(c *middleware.Context) { - file := c.Params(":file") - - dashboard := search.GetDashboardFromJsonIndex(file) - if dashboard == nil { - c.JsonApiErr(404, "Dashboard not found", nil) - return - } - - dash := dtos.DashboardFullWithMeta{Dashboard: dashboard.Data} - dash.Meta.Type = m.DashTypeJson - dash.Meta.CanEdit = canEditDashboard(c.OrgRole) - - c.JSON(200, &dash) + dash.Set("panels", panels) } // GetDashboardVersions returns all dashboard versions as JSON func GetDashboardVersions(c *middleware.Context) Response { - dashboardId := c.ParamsInt64(":dashboardId") - limit := c.QueryInt("limit") - start := c.QueryInt("start") + dashId := c.ParamsInt64(":dashboardId") - if limit == 0 { - limit = 1000 + guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) + if canSave, err := guardian.CanSave(); err != nil || !canSave { + return dashboardGuardianResponse(err) } query := m.GetDashboardVersionsQuery{ OrgId: c.OrgId, - DashboardId: dashboardId, - Limit: limit, - Start: start, + DashboardId: dashId, + Limit: c.QueryInt("limit"), + Start: c.QueryInt("start"), } if err := bus.Dispatch(&query); err != nil { - return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashboardId), err) + return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err) } for _, version := range query.Result { @@ -300,17 +328,21 @@ func GetDashboardVersions(c *middleware.Context) Response { // GetDashboardVersion returns the dashboard version with the given ID. func GetDashboardVersion(c *middleware.Context) Response { - dashboardId := c.ParamsInt64(":dashboardId") - version := c.ParamsInt(":id") + dashId := c.ParamsInt64(":dashboardId") + + guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) + if canSave, err := guardian.CanSave(); err != nil || !canSave { + return dashboardGuardianResponse(err) + } query := m.GetDashboardVersionQuery{ OrgId: c.OrgId, - DashboardId: dashboardId, - Version: version, + DashboardId: dashId, + Version: c.ParamsInt(":id"), } if err := bus.Dispatch(&query); err != nil { - return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", version, dashboardId), err) + return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err) } creator := "Anonymous" @@ -361,19 +393,21 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff // RestoreDashboardVersion restores a dashboard to the given version. func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response { - dashboardId := c.ParamsInt64(":dashboardId") - - dashQuery := m.GetDashboardQuery{Id: dashboardId, OrgId: c.OrgId} - if err := bus.Dispatch(&dashQuery); err != nil { - return ApiError(404, "Dashboard not found", nil) + dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId")) + if rsp != nil { + return rsp } - versionQuery := m.GetDashboardVersionQuery{DashboardId: dashboardId, Version: apiCmd.Version, OrgId: c.OrgId} + guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser) + if canSave, err := guardian.CanSave(); err != nil || !canSave { + return dashboardGuardianResponse(err) + } + + versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId} if err := bus.Dispatch(&versionQuery); err != nil { return ApiError(404, "Dashboard version not found", nil) } - dashboard := dashQuery.Result version := versionQuery.Result saveCmd := m.SaveDashboardCommand{} @@ -381,7 +415,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard saveCmd.OrgId = c.OrgId saveCmd.UserId = c.UserId saveCmd.Dashboard = version.Data - saveCmd.Dashboard.Set("version", dashboard.Version) + saveCmd.Dashboard.Set("version", dash.Version) saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) return PostDashboard(c, saveCmd) diff --git a/pkg/api/dashboard_acl.go b/pkg/api/dashboard_acl.go new file mode 100644 index 00000000000..88cc74b9d1c --- /dev/null +++ b/pkg/api/dashboard_acl.go @@ -0,0 +1,79 @@ +package api + +import ( + "time" + + "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" + "github.com/grafana/grafana/pkg/services/guardian" +) + +func GetDashboardAclList(c *middleware.Context) Response { + dashId := c.ParamsInt64(":dashboardId") + + guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) + + if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { + return dashboardGuardianResponse(err) + } + + acl, err := guardian.GetAcl() + if err != nil { + return ApiError(500, "Failed to get dashboard acl", err) + } + + return Json(200, acl) +} + +func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response { + dashId := c.ParamsInt64(":dashboardId") + + guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) + if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { + return dashboardGuardianResponse(err) + } + + cmd := m.UpdateDashboardAclCommand{} + cmd.DashboardId = dashId + + for _, item := range apiCmd.Items { + cmd.Items = append(cmd.Items, &m.DashboardAcl{ + OrgId: c.OrgId, + DashboardId: dashId, + UserId: item.UserId, + TeamId: item.TeamId, + Role: item.Role, + Permission: item.Permission, + Created: time.Now(), + Updated: time.Now(), + }) + } + + if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty { + return ApiError(409, err.Error(), err) + } + return ApiError(500, "Failed to create permission", err) + } + + return ApiSuccess("Dashboard acl updated") +} + +func DeleteDashboardAcl(c *middleware.Context) Response { + dashId := c.ParamsInt64(":dashboardId") + aclId := c.ParamsInt64(":aclId") + + guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) + if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { + return dashboardGuardianResponse(err) + } + + cmd := m.RemoveDashboardAclCommand{OrgId: c.OrgId, AclId: aclId} + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to delete permission for user", err) + } + + return Json(200, "") +} diff --git a/pkg/api/dashboard_acl_test.go b/pkg/api/dashboard_acl_test.go new file mode 100644 index 00000000000..e22e625dcf9 --- /dev/null +++ b/pkg/api/dashboard_acl_test.go @@ -0,0 +1,174 @@ +package api + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestDashboardAclApiEndpoint(t *testing.T) { + Convey("Given a dashboard acl", t, func() { + mockResult := []*m.DashboardAclInfoDTO{ + {Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW}, + {Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT}, + {Id: 3, OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN}, + {Id: 4, OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, + {Id: 5, OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN}, + } + dtoRes := transformDashboardAclsToDTOs(mockResult) + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = dtoRes + return nil + }) + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) + + teamResp := []*m.Team{} + bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { + query.Result = teamResp + return nil + }) + + Convey("When user is org admin", func() { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) { + Convey("Should be able to access ACL", func() { + sc.handlerFunc = GetDashboardAclList + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + So(len(respJSON.MustArray()), ShouldEqual, 5) + So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2) + So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW) + }) + }) + }) + + Convey("When user is editor and has admin permission in the ACL", func() { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) { + mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) + + Convey("Should be able to access ACL", func() { + sc.handlerFunc = GetDashboardAclList + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) { + mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) + + bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { + return nil + }) + + Convey("Should be able to delete permission", func() { + sc.handlerFunc = DeleteDashboardAcl + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + + Convey("When user is a member of a team in the ACL with admin permission", func() { + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardsId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) { + teamResp = append(teamResp, &m.Team{Id: 2, OrgId: 1, Name: "UG2"}) + + bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { + return nil + }) + + Convey("Should be able to delete permission", func() { + sc.handlerFunc = DeleteDashboardAcl + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + }) + }) + + Convey("When user is editor and has edit permission in the ACL", func() { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) { + mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT}) + + Convey("Should not be able to access ACL", func() { + sc.handlerFunc = GetDashboardAclList + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) { + mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT}) + + bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { + return nil + }) + + Convey("Should be not be able to delete permission", func() { + sc.handlerFunc = DeleteDashboardAcl + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + }) + + Convey("When user is editor and not in the ACL", func() { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) { + + Convey("Should not be able to access ACL", func() { + sc.handlerFunc = GetDashboardAclList + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", m.ROLE_EDITOR, func(sc *scenarioContext) { + mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW}) + bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { + return nil + }) + + Convey("Should be not be able to delete permission", func() { + sc.handlerFunc = DeleteDashboardAcl + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + }) + }) +} + +func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardAclInfoDTO { + dtos := make([]*m.DashboardAclInfoDTO, 0) + + for _, acl := range acls { + dto := &m.DashboardAclInfoDTO{ + Id: acl.Id, + OrgId: acl.OrgId, + DashboardId: acl.DashboardId, + Permission: acl.Permission, + UserId: acl.UserId, + TeamId: acl.TeamId, + } + dtos = append(dtos, dto) + } + + return dtos +} diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go new file mode 100644 index 00000000000..e6228878625 --- /dev/null +++ b/pkg/api/dashboard_test.go @@ -0,0 +1,521 @@ +package api + +import ( + "encoding/json" + "path/filepath" + "testing" + + macaron "gopkg.in/macaron.v1" + + "github.com/go-macaron/session" + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/setting" + + . "github.com/smartystreets/goconvey/convey" +) + +type fakeDashboardRepo struct { + inserted []*dashboards.SaveDashboardItem + getDashboard []*m.Dashboard +} + +func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*m.Dashboard, error) { + repo.inserted = append(repo.inserted, json) + return json.Dashboard, nil +} + +var fakeRepo *fakeDashboardRepo + +func TestDashboardApiEndpoint(t *testing.T) { + Convey("Given a dashboard with a parent folder which does not have an acl", t, func() { + fakeDash := m.NewDashboard("Child dash") + fakeDash.Id = 1 + fakeDash.FolderId = 1 + fakeDash.HasAcl = false + + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = fakeDash + return nil + }) + + viewerRole := m.ROLE_VIEWER + editorRole := m.ROLE_EDITOR + + aclMockResp := []*m.DashboardAclInfoDTO{ + {Role: &viewerRole, Permission: m.PERMISSION_VIEW}, + {Role: &editorRole, Permission: m.PERMISSION_EDIT}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = aclMockResp + return nil + }) + + bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { + query.Result = []*m.Team{} + return nil + }) + + cmd := m.SaveDashboardCommand{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "folderId": fakeDash.FolderId, + "title": fakeDash.Title, + "id": fakeDash.Id, + }), + } + + Convey("When user is an Org Viewer", func() { + role := m.ROLE_VIEWER + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should not be able to edit or save dashboard", func() { + So(dash.Meta.CanEdit, ShouldBeFalse) + So(dash.Meta.CanSave, ShouldBeFalse) + So(dash.Meta.CanAdmin, ShouldBeFalse) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + CallDeleteDashboard(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + CallGetDashboardVersion(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + CallGetDashboardVersions(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { + CallPostDashboard(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + Convey("When user is an Org Editor", func() { + role := m.ROLE_EDITOR + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should be able to edit or save dashboard", func() { + So(dash.Meta.CanEdit, ShouldBeTrue) + So(dash.Meta.CanSave, ShouldBeTrue) + So(dash.Meta.CanAdmin, ShouldBeFalse) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + CallDeleteDashboard(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + CallGetDashboardVersion(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + CallGetDashboardVersions(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { + CallPostDashboard(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + Convey("When saving a dashboard folder in another folder", func() { + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = fakeDash + query.Result.IsFolder = true + return nil + }) + invalidCmd := m.SaveDashboardCommand{ + FolderId: fakeDash.FolderId, + IsFolder: true, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "folderId": fakeDash.FolderId, + "title": fakeDash.Title, + }), + } + Convey("Should return an error", func() { + postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, invalidCmd, func(sc *scenarioContext) { + CallPostDashboard(sc) + So(sc.resp.Code, ShouldEqual, 400) + }) + }) + }) + }) + }) + + Convey("Given a dashboard with a parent folder which has an acl", t, func() { + fakeDash := m.NewDashboard("Child dash") + fakeDash.Id = 1 + fakeDash.FolderId = 1 + fakeDash.HasAcl = true + setting.ViewersCanEdit = false + + aclMockResp := []*m.DashboardAclInfoDTO{ + { + DashboardId: 1, + Permission: m.PERMISSION_EDIT, + UserId: 200, + }, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = aclMockResp + return nil + }) + + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = fakeDash + return nil + }) + + bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { + query.Result = []*m.Team{} + return nil + }) + + cmd := m.SaveDashboardCommand{ + FolderId: fakeDash.FolderId, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": fakeDash.Id, + "folderId": fakeDash.FolderId, + "title": fakeDash.Title, + }), + } + + Convey("When user is an Org Viewer and has no permissions for this dashboard", func() { + role := m.ROLE_VIEWER + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + sc.handlerFunc = GetDashboard + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + Convey("Should be denied access", func() { + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + CallDeleteDashboard(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + CallGetDashboardVersion(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + CallGetDashboardVersions(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { + CallPostDashboard(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + Convey("When user is an Org Editor and has no permissions for this dashboard", func() { + role := m.ROLE_EDITOR + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + sc.handlerFunc = GetDashboard + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + Convey("Should be denied access", func() { + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + CallDeleteDashboard(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + CallGetDashboardVersion(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + CallGetDashboardVersions(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { + CallPostDashboard(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + Convey("When user is an Org Viewer but has an edit permission", func() { + role := m.ROLE_VIEWER + + mockResult := []*m.DashboardAclInfoDTO{ + {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should be able to get dashboard with edit rights", func() { + So(dash.Meta.CanEdit, ShouldBeTrue) + So(dash.Meta.CanSave, ShouldBeTrue) + So(dash.Meta.CanAdmin, ShouldBeFalse) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + CallDeleteDashboard(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + CallGetDashboardVersion(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + CallGetDashboardVersions(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { + CallPostDashboard(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + + Convey("When user is an Org Viewer and viewers can edit", func() { + role := m.ROLE_VIEWER + setting.ViewersCanEdit = true + + mockResult := []*m.DashboardAclInfoDTO{ + {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should be able to get dashboard with edit rights but can save should be false", func() { + So(dash.Meta.CanEdit, ShouldBeTrue) + So(dash.Meta.CanSave, ShouldBeFalse) + So(dash.Meta.CanAdmin, ShouldBeFalse) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + CallDeleteDashboard(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + Convey("When user is an Org Viewer but has an admin permission", func() { + role := m.ROLE_VIEWER + + mockResult := []*m.DashboardAclInfoDTO{ + {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should be able to get dashboard with edit rights", func() { + So(dash.Meta.CanEdit, ShouldBeTrue) + So(dash.Meta.CanSave, ShouldBeTrue) + So(dash.Meta.CanAdmin, ShouldBeTrue) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + CallDeleteDashboard(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + CallGetDashboardVersion(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + CallGetDashboardVersions(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + + postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { + CallPostDashboard(sc) + So(sc.resp.Code, ShouldEqual, 200) + }) + }) + + Convey("When user is an Org Editor but has a view permission", func() { + role := m.ROLE_EDITOR + + mockResult := []*m.DashboardAclInfoDTO{ + {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, + } + + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should not be able to edit or save dashboard", func() { + So(dash.Meta.CanEdit, ShouldBeFalse) + So(dash.Meta.CanSave, ShouldBeFalse) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + CallDeleteDashboard(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + CallGetDashboardVersion(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + CallGetDashboardVersions(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + + postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { + CallPostDashboard(sc) + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + }) +} + +func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { + sc.handlerFunc = GetDashboard + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sc.resp.Code, ShouldEqual, 200) + + dash := dtos.DashboardFullWithMeta{} + err := json.NewDecoder(sc.resp.Body).Decode(&dash) + So(err, ShouldBeNil) + + return dash +} + +func CallGetDashboardVersion(sc *scenarioContext) { + bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error { + query.Result = &m.DashboardVersion{} + return nil + }) + + sc.handlerFunc = GetDashboardVersion + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() +} + +func CallGetDashboardVersions(sc *scenarioContext) { + bus.AddHandler("test", func(query *m.GetDashboardVersionsQuery) error { + query.Result = []*m.DashboardVersionDTO{} + return nil + }) + + sc.handlerFunc = GetDashboardVersions + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() +} + +func CallDeleteDashboard(sc *scenarioContext) { + bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error { + return nil + }) + + sc.handlerFunc = DeleteDashboard + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() +} + +func CallPostDashboard(sc *scenarioContext) { + bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error { + return nil + }) + + bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error { + cmd.Result = &m.Dashboard{Id: 2, Slug: "Dash", Version: 2} + return nil + }) + + bus.AddHandler("test", func(cmd *alerting.UpdateDashboardAlertsCommand) error { + return nil + }) + + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() +} + +func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + sc := &scenarioContext{ + url: url, + } + viewsPath, _ := filepath.Abs("../../public/views") + + sc.m = macaron.New() + sc.m.Use(macaron.Renderer(macaron.RenderOptions{ + Directory: viewsPath, + Delims: macaron.Delims{Left: "[[", Right: "]]"}, + })) + + sc.m.Use(middleware.GetContextHandler()) + sc.m.Use(middleware.Sessioner(&session.Options{})) + + sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = role + + return PostDashboard(c, cmd) + }) + + fakeRepo = &fakeDashboardRepo{} + dashboards.SetRepository(fakeRepo) + + sc.m.Post(routePattern, sc.defaultHandler) + + fn(sc) + }) +} diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 5ae752bea91..72336693363 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -56,6 +56,10 @@ func TestDataSourcesProxy(t *testing.T) { } func loggedInUserScenario(desc string, url string, fn scenarioFunc) { + loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn) +} + +func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() @@ -77,7 +81,7 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) { sc.context = c sc.context.UserId = TestUserID sc.context.OrgId = TestOrgID - sc.context.OrgRole = models.ROLE_EDITOR + sc.context.OrgRole = role if sc.handlerFunc != nil { return sc.handlerFunc(sc.context) } @@ -85,7 +89,12 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) { return nil }) - sc.m.Get(url, sc.defaultHandler) + switch method { + case "GET": + sc.m.Get(routePattern, sc.defaultHandler) + case "DELETE": + sc.m.Delete(routePattern, sc.defaultHandler) + } fn(sc) }) diff --git a/pkg/api/dtos/acl.go b/pkg/api/dtos/acl.go new file mode 100644 index 00000000000..6c74e68ce0d --- /dev/null +++ b/pkg/api/dtos/acl.go @@ -0,0 +1,16 @@ +package dtos + +import ( + m "github.com/grafana/grafana/pkg/models" +) + +type UpdateDashboardAclCommand struct { + Items []DashboardAclUpdateItem `json:"items"` +} + +type DashboardAclUpdateItem struct { + UserId int64 `json:"userId"` + TeamId int64 `json:"teamId"` + Role *m.RoleType `json:"role,omitempty"` + Permission m.PermissionType `json:"permission"` +} diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index 9ef9a96edc4..0be0537527b 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -7,20 +7,25 @@ import ( ) type DashboardMeta struct { - IsStarred bool `json:"isStarred,omitempty"` - IsHome bool `json:"isHome,omitempty"` - IsSnapshot bool `json:"isSnapshot,omitempty"` - Type string `json:"type,omitempty"` - CanSave bool `json:"canSave"` - CanEdit bool `json:"canEdit"` - CanStar bool `json:"canStar"` - Slug string `json:"slug"` - Expires time.Time `json:"expires"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - UpdatedBy string `json:"updatedBy"` - CreatedBy string `json:"createdBy"` - Version int `json:"version"` + IsStarred bool `json:"isStarred,omitempty"` + IsHome bool `json:"isHome,omitempty"` + IsSnapshot bool `json:"isSnapshot,omitempty"` + Type string `json:"type,omitempty"` + CanSave bool `json:"canSave"` + CanEdit bool `json:"canEdit"` + CanAdmin bool `json:"canAdmin"` + CanStar bool `json:"canStar"` + Slug string `json:"slug"` + Expires time.Time `json:"expires"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + UpdatedBy string `json:"updatedBy"` + CreatedBy string `json:"createdBy"` + Version int `json:"version"` + HasAcl bool `json:"hasAcl"` + IsFolder bool `json:"isFolder"` + FolderId int64 `json:"folderId"` + FolderTitle string `json:"folderTitle"` } type DashboardFullWithMeta struct { diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index b813c78f2bb..8c7f505277d 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -7,9 +7,10 @@ type IndexViewData struct { AppSubUrl string GoogleAnalyticsId string GoogleTagManagerId string - MainNavLinks []*NavLink + NavTree []*NavLink BuildVersion string BuildCommit string + Theme string NewGrafanaVersionExists bool NewGrafanaVersion string } @@ -20,10 +21,16 @@ type PluginCss struct { } type NavLink struct { - Text string `json:"text,omitempty"` - Icon string `json:"icon,omitempty"` - Img string `json:"img,omitempty"` - Url string `json:"url,omitempty"` - Divider bool `json:"divider,omitempty"` - Children []*NavLink `json:"children,omitempty"` + Id string `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Description string `json:"description,omitempty"` + SubTitle string `json:"subTitle,omitempty"` + Icon string `json:"icon,omitempty"` + Img string `json:"img,omitempty"` + Url string `json:"url,omitempty"` + Target string `json:"target,omitempty"` + Divider bool `json:"divider,omitempty"` + HideFromMenu bool `json:"hideFromMenu,omitempty"` + HideFromTabs bool `json:"hideFromTabs,omitempty"` + Children []*NavLink `json:"children,omitempty"` } diff --git a/pkg/api/dtos/invite.go b/pkg/api/dtos/invite.go index 3f002a8b157..09d53f576b4 100644 --- a/pkg/api/dtos/invite.go +++ b/pkg/api/dtos/invite.go @@ -6,7 +6,7 @@ type AddInviteForm struct { LoginOrEmail string `json:"loginOrEmail" binding:"Required"` Name string `json:"name"` Role m.RoleType `json:"role" binding:"Required"` - SkipEmails bool `json:"skipEmails"` + SendEmail bool `json:"sendEmail"` } type InviteInfo struct { diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index e2aa12249aa..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" @@ -27,6 +28,7 @@ type CurrentUser struct { Email string `json:"email"` Name string `json:"name"` LightTheme bool `json:"lightTheme"` + OrgCount int `json:"orgCount"` OrgId int64 `json:"orgId"` OrgName string `json:"orgName"` OrgRole m.RoleType `json:"orgRole"` @@ -56,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/frontendsettings.go b/pkg/api/frontendsettings.go index 563f940904e..591dcc62344 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -143,7 +143,6 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro "alertingEnabled": setting.AlertingEnabled, "googleAnalyticsId": setting.GoogleAnalyticsId, "disableLoginForm": setting.DisableLoginForm, - "disableSignoutMenu": setting.DisableSignoutMenu, "externalUserMngInfo": setting.ExternalUserMngInfo, "externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl, "externalUserMngLinkName": setting.ExternalUserMngLinkName, diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 89456d20d8c..0366b9aedad 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -95,7 +95,7 @@ func (hs *HttpServer) Start(ctx context.Context) error { func (hs *HttpServer) Shutdown(ctx context.Context) error { err := hs.httpSrv.Shutdown(ctx) - hs.log.Info("stopped http server") + hs.log.Info("Stopped HTTP server") return err } diff --git a/pkg/api/index.go b/pkg/api/index.go index bf7a9fc1759..1b836356189 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -50,6 +50,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Login: c.Login, Email: c.Email, Name: c.Name, + OrgCount: c.OrgCount, OrgId: c.OrgId, OrgName: c.OrgName, OrgRole: c.OrgRole, @@ -61,6 +62,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { HelpFlags1: c.HelpFlags1, }, Settings: settings, + Theme: prefs.Theme, AppUrl: appUrl, AppSubUrl: appSubUrl, GoogleAnalyticsId: setting.GoogleAnalyticsId, @@ -82,52 +84,77 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { themeUrlParam := c.Query("theme") if themeUrlParam == "light" { data.User.LightTheme = true - } - - dashboardChildNavs := []*dtos.NavLink{ - {Text: "Home", Url: setting.AppSubUrl + "/"}, - {Text: "Playlists", Url: setting.AppSubUrl + "/playlists"}, - {Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots"}, + data.Theme = "light" } if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { - dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true}) - dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"}) - dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"}) + data.NavTree = append(data.NavTree, &dtos.NavLink{ + Text: "Create", + Id: "create", + Icon: "fa fa-fw fa-plus", + Url: setting.AppSubUrl + "/dashboard/new", + Children: []*dtos.NavLink{ + {Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"}, + {Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"}, + {Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, + }, + }) } - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ + dashboardChildNavs := []*dtos.NavLink{ + {Text: "Home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true}, + {Divider: true, HideFromTabs: true}, + {Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "gicon gicon-manage"}, + {Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "gicon gicon-playlists"}, + {Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "gicon gicon-snapshots"}, + } + + data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Dashboards", - Icon: "icon-gf icon-gf-dashboard", + Id: "dashboards", + SubTitle: "Manage dashboards & folders", + Icon: "gicon gicon-dashboard", Url: setting.AppSubUrl + "/", Children: dashboardChildNavs, }) - if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) { - alertChildNavs := []*dtos.NavLink{ - {Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"}, - {Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications"}, + if c.IsSignedIn { + profileNode := &dtos.NavLink{ + Text: c.SignedInUser.NameOrFallback(), + SubTitle: c.SignedInUser.Login, + Id: "profile", + Img: data.User.GravatarUrl, + Url: setting.AppSubUrl + "/profile", + HideFromMenu: true, + Children: []*dtos.NavLink{ + {Text: "Preferences", Id: "profile-settings", Url: setting.AppSubUrl + "/profile", Icon: "gicon gicon-preferences"}, + {Text: "Change Password", Id: "change-password", Url: setting.AppSubUrl + "/profile/password", Icon: "fa fa-fw fa-lock", HideFromMenu: true}, + }, } - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ - Text: "Alerting", - Icon: "icon-gf icon-gf-alert", - Url: setting.AppSubUrl + "/alerting/list", - Children: alertChildNavs, - }) + if !setting.DisableSignoutMenu { + // add sign out first + profileNode.Children = append(profileNode.Children, &dtos.NavLink{ + Text: "Sign out", Id: "sign-out", Url: setting.AppSubUrl + "/logout", Icon: "fa fa-fw fa-sign-out", Target: "_self", + }) + } + + data.NavTree = append(data.NavTree, profileNode) } - if c.OrgRole == m.ROLE_ADMIN { - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ - Text: "Data Sources", - Icon: "icon-gf icon-gf-datasources", - Url: setting.AppSubUrl + "/datasources", - }) + if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) { + alertChildNavs := []*dtos.NavLink{ + {Text: "Alert Rules", Id: "alert-list", Url: setting.AppSubUrl + "/alerting/list", Icon: "gicon gicon-alert-rules"}, + {Text: "Notification channels", Id: "channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "gicon gicon-alert-notification-channel"}, + } - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ - Text: "Plugins", - Icon: "icon-gf icon-gf-apps", - Url: setting.AppSubUrl + "/plugins", + data.NavTree = append(data.NavTree, &dtos.NavLink{ + Text: "Alerting", + SubTitle: "Alert rules & notifications", + Id: "alerting", + Icon: "gicon gicon-alert", + Url: setting.AppSubUrl + "/alerting/list", + Children: alertChildNavs, }) } @@ -140,6 +167,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if plugin.Pinned { appLink := &dtos.NavLink{ Text: plugin.Name, + Id: "plugin-page-" + plugin.Id, Url: plugin.DefaultNavUrl, Img: plugin.Info.Logos.Small, } @@ -168,29 +196,106 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN { appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true}) - appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) + appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) } if len(appLink.Children) > 0 { - data.MainNavLinks = append(data.MainNavLinks, appLink) + data.NavTree = append(data.NavTree, appLink) } } } - if c.IsGrafanaAdmin { - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ - Text: "Admin", - Icon: "fa fa-fw fa-cogs", - Url: setting.AppSubUrl + "/admin", + if c.OrgRole == m.ROLE_ADMIN { + cfgNode := &dtos.NavLink{ + Id: "cfg", + Text: "Configuration", + SubTitle: "Organization: " + c.OrgName, + Icon: "gicon gicon-cog", + Url: setting.AppSubUrl + "/datasources", Children: []*dtos.NavLink{ - {Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"}, - {Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"}, - {Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"}, - {Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"}, + { + Text: "Data Sources", + Icon: "gicon gicon-datasources", + Description: "Add and configure data sources", + Id: "datasources", + Url: setting.AppSubUrl + "/datasources", + }, + { + Text: "Users", + Id: "users", + Description: "Manage org members", + Icon: "gicon gicon-user", + Url: setting.AppSubUrl + "/org/users", + }, + { + Text: "Teams", + Id: "teams", + Description: "Manage org groups", + Icon: "gicon gicon-team", + Url: setting.AppSubUrl + "/org/teams", + }, + { + Text: "Plugins", + Id: "plugins", + Description: "View and configure plugins", + Icon: "gicon gicon-plugins", + Url: setting.AppSubUrl + "/plugins", + }, + { + Text: "Preferences", + Id: "org-settings", + Description: "Organization preferences", + Icon: "gicon gicon-preferences", + Url: setting.AppSubUrl + "/org", + }, + + { + Text: "API Keys", + Id: "apikeys", + Description: "Create & manage API keys", + Icon: "gicon gicon-apikeys", + Url: setting.AppSubUrl + "/org/apikeys", + }, }, - }) + } + + if c.IsGrafanaAdmin { + cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{ + Divider: true, HideFromTabs: true, + }) + cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{ + Text: "Server Admin", + HideFromTabs: true, + SubTitle: "Manage all users & orgs", + Id: "admin", + Icon: "gicon gicon-shield", + Url: setting.AppSubUrl + "/admin/users", + Children: []*dtos.NavLink{ + {Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"}, + {Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"}, + {Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"}, + {Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"}, + {Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"}, + }, + }) + } + + data.NavTree = append(data.NavTree, cfgNode) } + data.NavTree = append(data.NavTree, &dtos.NavLink{ + Text: "Help", + Id: "help", + Url: "#", + Icon: "gicon gicon-question", + HideFromMenu: true, + Children: []*dtos.NavLink{ + {Text: "Keyboard shortcuts", Url: "/shortcuts", Icon: "fa fa-fw fa-keyboard-o", Target: "_self"}, + {Text: "Community site", Url: "http://community.grafana.com", Icon: "fa fa-fw fa-comment", Target: "_blank"}, + {Text: "Documentation", Url: "http://docs.grafana.org", Icon: "fa fa-fw fa-file", Target: "_blank"}, + }, + }) + return &data, nil } diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 864e464133d..57d9913d2eb 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -61,7 +61,7 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response } // send invite email - if !inviteDto.SkipEmails && util.IsEmail(inviteDto.LoginOrEmail) { + if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) { emailCmd := m.SendEmailCommand{ To: []string{inviteDto.LoginOrEmail}, Template: "new_user_invite.html", @@ -99,7 +99,7 @@ func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dto return ApiError(500, "Error while trying to create org user", err) } else { - if !inviteDto.SkipEmails && util.IsEmail(user.Email) { + if inviteDto.SendEmail && util.IsEmail(user.Email) { emailCmd := m.SendEmailCommand{ To: []string{user.Email}, Template: "invited_to_org.html", diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 02c376eed30..57a15bd8db5 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.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" @@ -31,10 +32,6 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response { userToAdd := userQuery.Result - // if userToAdd.Id == c.UserId { - // return ApiError(400, "Cannot add yourself as user", nil) - // } - cmd.UserId = userToAdd.Id if err := bus.Dispatch(&cmd); err != nil { @@ -64,6 +61,10 @@ func getOrgUsersHelper(orgId int64) Response { return ApiError(500, "Failed to get account user", err) } + for _, user := range query.Result { + user.AvatarUrl = dtos.GetGravatarUrl(user.Email) + } + return Json(200, query.Result) } diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index a6c2da26dd8..040aef0474e 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -130,7 +130,7 @@ func GetPlaylistItems(c *middleware.Context) Response { func GetPlaylistDashboards(c *middleware.Context) Response { playlistId := c.ParamsInt64(":id") - playlists, err := LoadPlaylistDashboards(c.OrgId, c.UserId, playlistId) + playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId) if err != nil { return ApiError(500, "Could not load dashboards", err) } diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index 29a806ce23d..1d059e06be5 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -34,18 +34,18 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i return result, nil } -func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice { +func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice { result := make(dtos.PlaylistDashboardsSlice, 0) if len(dashboardByTag) > 0 { for _, tag := range dashboardByTag { searchQuery := search.Query{ - Title: "", - Tags: []string{tag}, - UserId: userId, - Limit: 100, - IsStarred: false, - OrgId: orgId, + Title: "", + Tags: []string{tag}, + SignedInUser: signedInUser, + Limit: 100, + IsStarred: false, + OrgId: orgId, } if err := bus.Dispatch(&searchQuery); err == nil { @@ -64,7 +64,7 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashb return result } -func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) { +func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistId int64) (dtos.PlaylistDashboardsSlice, error) { playlistItems, _ := LoadPlaylistItems(playlistId) dashboardByIds := make([]int64, 0) @@ -89,7 +89,7 @@ func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashb var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder) result = append(result, k...) - result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag, dashboardTagOrder)...) + result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...) sort.Sort(result) return result, nil diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index faac8c03c62..5f4ec632c4d 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -135,9 +135,24 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { req.Header.Add("Authorization", dsAuth) } - // clear cookie headers + // clear cookie header, except for whitelisted cookies + var keptCookies []*http.Cookie + if proxy.ds.JsonData != nil { + if keepCookies := proxy.ds.JsonData.Get("keepCookies"); keepCookies != nil { + keepCookieNames := keepCookies.MustStringArray() + for _, c := range req.Cookies() { + for _, v := range keepCookieNames { + if c.Name == v { + keptCookies = append(keptCookies, c) + } + } + } + } + } req.Header.Del("Cookie") - req.Header.Del("Set-Cookie") + for _, c := range keptCookies { + req.AddCookie(c) + } // clear X-Forwarded Host/Port/Proto headers req.Header.Del("X-Forwarded-Host") diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 0a900ad3a6d..a7a869b2a9f 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -149,6 +149,58 @@ func TestDSRouteRule(t *testing.T) { }) }) + Convey("When proxying a data source with no keepCookies specified", func() { + plugin := &plugins.DataSourcePlugin{} + + json, _ := simplejson.NewJson([]byte(`{"keepCookies": []}`)) + + ds := &m.DataSource{ + Type: m.DS_GRAPHITE, + Url: "http://graphite:8086", + JsonData: json, + } + + ctx := &middleware.Context{} + proxy := NewDataSourceProxy(ds, plugin, ctx, "") + + requestUrl, _ := url.Parse("http://grafana.com/sub") + req := http.Request{URL: requestUrl, Header: make(http.Header)} + cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test" + req.Header.Set("Cookie", cookies) + + proxy.getDirector()(&req) + + Convey("Should clear all cookies", func() { + So(req.Header.Get("Cookie"), ShouldEqual, "") + }) + }) + + Convey("When proxying a data source with keep cookies specified", func() { + plugin := &plugins.DataSourcePlugin{} + + json, _ := simplejson.NewJson([]byte(`{"keepCookies": ["JSESSION_ID"]}`)) + + ds := &m.DataSource{ + Type: m.DS_GRAPHITE, + Url: "http://graphite:8086", + JsonData: json, + } + + ctx := &middleware.Context{} + proxy := NewDataSourceProxy(ds, plugin, ctx, "") + + requestUrl, _ := url.Parse("http://grafana.com/sub") + req := http.Request{URL: requestUrl, Header: make(http.Header)} + cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test" + req.Header.Set("Cookie", cookies) + + proxy.getDirector()(&req) + + Convey("Should keep named cookies", func() { + So(req.Header.Get("Cookie"), ShouldEqual, "JSESSION_ID=test") + }) + }) + Convey("When interpolating string", func() { data := templateData{ SecureJsonData: map[string]string{ diff --git a/pkg/api/render.go b/pkg/api/render.go index 5284c7831bb..65733cfab15 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -10,25 +10,33 @@ import ( ) func RenderToPng(c *middleware.Context) { - queryReader := util.NewUrlQueryReader(c.Req.URL) + queryReader, err := util.NewUrlQueryReader(c.Req.URL) + if err != nil { + c.Handle(400, "Render parameters error", err) + return + } queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery) renderOpts := &renderer.RenderOpts{ Path: c.Params("*") + queryParams, Width: queryReader.Get("width", "800"), Height: queryReader.Get("height", "400"), - OrgId: c.OrgId, Timeout: queryReader.Get("timeout", "60"), + OrgId: c.OrgId, + UserId: c.UserId, + OrgRole: c.OrgRole, Timezone: queryReader.Get("tz", ""), + Encoding: queryReader.Get("encoding", ""), } 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/search.go b/pkg/api/search.go index c68dc51e986..fee062a5599 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -14,27 +14,38 @@ func Search(c *middleware.Context) { tags := c.QueryStrings("tag") starred := c.Query("starred") limit := c.QueryInt("limit") + dashboardType := c.Query("type") if limit == 0 { limit = 1000 } - dbids := make([]int, 0) + dbids := make([]int64, 0) for _, id := range c.QueryStrings("dashboardIds") { - dashboardId, err := strconv.Atoi(id) + dashboardId, err := strconv.ParseInt(id, 10, 64) if err == nil { dbids = append(dbids, dashboardId) } } + folderIds := make([]int64, 0) + for _, id := range c.QueryStrings("folderIds") { + folderId, err := strconv.ParseInt(id, 10, 64) + if err == nil { + folderIds = append(folderIds, folderId) + } + } + searchQuery := search.Query{ Title: query, Tags: tags, - UserId: c.UserId, + SignedInUser: c.SignedInUser, Limit: limit, IsStarred: starred == "true", OrgId: c.OrgId, DashboardIds: dbids, + Type: dashboardType, + FolderIds: folderIds, } err := bus.Dispatch(&searchQuery) diff --git a/pkg/api/team.go b/pkg/api/team.go new file mode 100644 index 00000000000..af537224d41 --- /dev/null +++ b/pkg/api/team.go @@ -0,0 +1,97 @@ +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" + "github.com/grafana/grafana/pkg/util" +) + +// POST /api/teams +func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response { + cmd.OrgId = c.OrgId + if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrTeamNameTaken { + return ApiError(409, "Team name taken", err) + } + return ApiError(500, "Failed to create Team", err) + } + + return Json(200, &util.DynMap{ + "teamId": cmd.Result.Id, + "message": "Team created", + }) +} + +// PUT /api/teams/:teamId +func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response { + cmd.Id = c.ParamsInt64(":teamId") + if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrTeamNameTaken { + return ApiError(400, "Team name taken", err) + } + return ApiError(500, "Failed to update Team", err) + } + + return ApiSuccess("Team updated") +} + +// DELETE /api/teams/:teamId +func DeleteTeamById(c *middleware.Context) Response { + if err := bus.Dispatch(&m.DeleteTeamCommand{Id: c.ParamsInt64(":teamId")}); err != nil { + if err == m.ErrTeamNotFound { + return ApiError(404, "Failed to delete Team. ID not found", nil) + } + return ApiError(500, "Failed to update Team", err) + } + return ApiSuccess("Team deleted") +} + +// GET /api/teams/search +func SearchTeams(c *middleware.Context) Response { + perPage := c.QueryInt("perpage") + if perPage <= 0 { + perPage = 1000 + } + page := c.QueryInt("page") + if page < 1 { + page = 1 + } + + query := m.SearchTeamsQuery{ + Query: c.Query("query"), + Name: c.Query("name"), + Page: page, + Limit: perPage, + OrgId: c.OrgId, + } + + if err := bus.Dispatch(&query); err != nil { + 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 + + return Json(200, query.Result) +} + +// GET /api/teams/:teamId +func GetTeamById(c *middleware.Context) Response { + query := m.GetTeamByIdQuery{Id: c.ParamsInt64(":teamId")} + + if err := bus.Dispatch(&query); err != nil { + if err == m.ErrTeamNotFound { + return ApiError(404, "Team not found", err) + } + + return ApiError(500, "Failed to get Team", err) + } + + return Json(200, &query.Result) +} diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go new file mode 100644 index 00000000000..412e142edb7 --- /dev/null +++ b/pkg/api/team_members.go @@ -0,0 +1,49 @@ +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" + "github.com/grafana/grafana/pkg/util" +) + +// GET /api/teams/:teamId/members +func GetTeamMembers(c *middleware.Context) Response { + query := m.GetTeamMembersQuery{TeamId: c.ParamsInt64(":teamId")} + + if err := bus.Dispatch(&query); err != nil { + 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) +} + +// POST /api/teams/:teamId/members +func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response { + cmd.TeamId = c.ParamsInt64(":teamId") + cmd.OrgId = c.OrgId + + if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrTeamMemberAlreadyAdded { + return ApiError(400, "User is already added to this team", err) + } + return ApiError(500, "Failed to add Member to Team", err) + } + + return Json(200, &util.DynMap{ + "message": "Member added to Team", + }) +} + +// DELETE /api/teams/:teamId/members/:userId +func RemoveTeamMember(c *middleware.Context) Response { + if err := bus.Dispatch(&m.RemoveTeamMemberCommand{TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil { + return ApiError(500, "Failed to remove Member from Team", err) + } + return ApiSuccess("Team Member removed") +} diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go new file mode 100644 index 00000000000..0bf06d723c8 --- /dev/null +++ b/pkg/api/team_test.go @@ -0,0 +1,71 @@ +package api + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestTeamApiEndpoint(t *testing.T) { + Convey("Given two teams", t, func() { + mockResult := models.SearchTeamQueryResult{ + Teams: []*models.SearchTeamDto{ + {Name: "team1"}, + {Name: "team2"}, + }, + TotalCount: 2, + } + + Convey("When searching with no parameters", func() { + loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchTeamsQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil + }) + + sc.handlerFunc = SearchTeams + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(sentLimit, ShouldEqual, 1000) + So(sendPage, ShouldEqual, 1) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + + So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2) + So(len(respJSON.Get("teams").MustArray()), ShouldEqual, 2) + }) + }) + + Convey("When searching with page and perpage parameters", func() { + loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchTeamsQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil + }) + + sc.handlerFunc = SearchTeams + sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() + + So(sentLimit, ShouldEqual, 10) + So(sendPage, ShouldEqual, 2) + }) + }) + }) +} diff --git a/pkg/api/user.go b/pkg/api/user.go index 7e8bad91ab0..9a041d30272 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.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" @@ -219,7 +220,7 @@ func SearchUsers(c *middleware.Context) Response { return Json(200, query.Result.Users) } -// GET /api/search +// GET /api/users/search func SearchUsersWithPaging(c *middleware.Context) Response { query, err := searchUser(c) if err != nil { @@ -247,6 +248,10 @@ func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) { return nil, err } + for _, user := range query.Result.Users { + user.AvatarUrl = dtos.GetGravatarUrl(user.Email) + } + query.Result.Page = page query.Result.PerPage = perPage diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 183e4b047cd..ab0e12f2d9f 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -14,8 +14,8 @@ import ( "net/http" _ "net/http/pprof" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" _ "github.com/grafana/grafana/pkg/services/alerting/conditions" @@ -30,7 +30,7 @@ import ( _ "github.com/grafana/grafana/pkg/tsdb/testdata" ) -var version = "4.6.0" +var version = "5.0.0" var commit = "NA" var buildstamp string var build_date string @@ -40,9 +40,6 @@ var homePath = flag.String("homepath", "", "path to grafana install/home path, d var pidFile = flag.String("pidfile", "", "path to pid file") var exitChan = make(chan int) -func init() { -} - func main() { v := flag.Bool("v", false, "prints current version and exits") profile := flag.Bool("profile", false, "Turn on pprof profiling") @@ -82,12 +79,28 @@ func main() { setting.BuildStamp = buildstampInt64 metrics.M_Grafana_Version.WithLabelValues(version).Set(1) - + shutdownCompleted := make(chan int) server := NewGrafanaServer() - server.Start() + + go listenToSystemSignals(server, shutdownCompleted) + + go func() { + code := 0 + if err := server.Start(); err != nil { + log.Error2("Startup failed", "error", err) + code = 1 + } + + exitChan <- code + }() + + code := <-shutdownCompleted + log.Info2("Grafana shutdown completed.", "code", code) + log.Close() + os.Exit(code) } -func listenToSystemSignals(server models.GrafanaServer) { +func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) { signalChan := make(chan os.Signal, 1) ignoreChan := make(chan os.Signal, 1) code := 0 @@ -97,10 +110,12 @@ func listenToSystemSignals(server models.GrafanaServer) { select { case sig := <-signalChan: - // Stops trace if profiling has been enabled - trace.Stop() + trace.Stop() // Stops trace if profiling has been enabled server.Shutdown(0, fmt.Sprintf("system signal: %s", sig)) + shutdownCompleted <- 0 case code = <-exitChan: + trace.Stop() // Stops trace if profiling has been enabled server.Shutdown(code, "startup error") + shutdownCompleted <- code } } diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 1d3ac092734..b84c3d4e3d6 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -3,13 +3,14 @@ package main import ( "context" "flag" + "fmt" "io/ioutil" + "net" "os" "path/filepath" "strconv" "time" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/services/provisioning" "golang.org/x/sync/errgroup" @@ -18,7 +19,6 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/cleanup" @@ -31,7 +31,7 @@ import ( "github.com/grafana/grafana/pkg/tracing" ) -func NewGrafanaServer() models.GrafanaServer { +func NewGrafanaServer() *GrafanaServerImpl { rootCtx, shutdownFn := context.WithCancel(context.Background()) childRoutines, childCtx := errgroup.WithContext(rootCtx) @@ -52,9 +52,7 @@ type GrafanaServerImpl struct { httpServer *api.HttpServer } -func (g *GrafanaServerImpl) Start() { - go listenToSystemSignals(g) - +func (g *GrafanaServerImpl) Start() error { g.initLogging() g.writePIDFile() @@ -66,17 +64,13 @@ func (g *GrafanaServerImpl) Start() { social.NewOAuthService() plugins.Init() - if err := provisioning.StartUp(setting.DatasourcesPath); err != nil { - logger.Error("Failed to provision Grafana from config", "error", err) - g.Shutdown(1, "Startup failed") - return + if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil { + return fmt.Errorf("Failed to provision Grafana from config. error: %v", err) } closer, err := tracing.Init(setting.Cfg) if err != nil { - g.log.Error("Tracing settings is not valid", "error", err) - g.Shutdown(1, "Startup failed") - return + return fmt.Errorf("Tracing settings is not valid. error: %v", err) } defer closer.Close() @@ -91,12 +85,12 @@ func (g *GrafanaServerImpl) Start() { g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) }) if err = notifications.Init(); err != nil { - g.log.Error("Notification service failed to initialize", "error", err) - g.Shutdown(1, "Startup failed") - return + return fmt.Errorf("Notification service failed to initialize. error: %v", err) } - g.startHttpServer() + sendSystemdNotification("READY=1") + + return g.startHttpServer() } func initSql() { @@ -120,16 +114,16 @@ func (g *GrafanaServerImpl) initLogging() { setting.LogConfigurationInfo() } -func (g *GrafanaServerImpl) startHttpServer() { +func (g *GrafanaServerImpl) startHttpServer() error { g.httpServer = api.NewHttpServer() err := g.httpServer.Start(g.context) if err != nil { - g.log.Error("Fail to start server", "error", err) - g.Shutdown(1, "Startup failed") - return + return fmt.Errorf("Fail to start server. error: %v", err) } + + return nil } func (g *GrafanaServerImpl) Shutdown(code int, reason string) { @@ -142,10 +136,9 @@ func (g *GrafanaServerImpl) Shutdown(code int, reason string) { g.shutdownFn() err = g.childRoutines.Wait() - - g.log.Info("Shutdown completed", "reason", err) - log.Close() - os.Exit(code) + if err != nil && err != context.Canceled { + g.log.Error("Server shutdown completed with an error", "error", err) + } } func (g *GrafanaServerImpl) writePIDFile() { @@ -169,3 +162,28 @@ func (g *GrafanaServerImpl) writePIDFile() { g.log.Info("Writing PID file", "path", *pidFile, "pid", pid) } + +func sendSystemdNotification(state string) error { + notifySocket := os.Getenv("NOTIFY_SOCKET") + + if notifySocket == "" { + return fmt.Errorf("NOTIFY_SOCKET environment variable empty or unset.") + } + + socketAddr := &net.UnixAddr{ + Name: notifySocket, + Net: "unixgram", + } + + conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) + + if err != nil { + return err + } + + _, err = conn.Write([]byte(state)) + + conn.Close() + + return err +} 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/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index d5980231f0e..25d77557342 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -16,17 +16,22 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) type RenderOpts struct { - Path string - Width string - Height string - Timeout string - OrgId int64 - Timezone string + Path string + Width string + Height string + Timeout string + OrgId int64 + UserId int64 + OrgRole models.RoleType + Timezone string + IsAlertContext bool + Encoding string } var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter") @@ -74,7 +79,11 @@ func RenderToPng(params *RenderOpts) (string, error) { pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20))) pngPath = pngPath + ".png" - renderKey := middleware.AddRenderAuthKey(params.OrgId) + orgRole := params.OrgRole + if params.IsAlertContext { + orgRole = models.ROLE_ADMIN + } + renderKey := middleware.AddRenderAuthKey(params.OrgId, params.UserId, orgRole) defer middleware.RemoveRenderAuthKey(renderKey) timeout, err := strconv.Atoi(params.Timeout) @@ -95,6 +104,10 @@ func RenderToPng(params *RenderOpts) (string, error) { "renderKey=" + renderKey, } + if params.Encoding != "" { + cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", params.Encoding)}, cmdArgs...) + } + cmd := exec.Command(binPath, cmdArgs...) stdout, err := cmd.StdoutPipe() diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 946ebae6b87..259d800f0a9 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -87,7 +87,7 @@ func initContextWithAnonymousUser(ctx *Context) bool { ctx.IsSignedIn = false ctx.AllowAnonymous = true - ctx.SignedInUser = &m.SignedInUser{} + ctx.SignedInUser = &m.SignedInUser{IsAnonymous: true} ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole) ctx.OrgId = orgQuery.Result.Id ctx.OrgName = orgQuery.Result.Name diff --git a/pkg/middleware/org_redirect.go b/pkg/middleware/org_redirect.go index a5f90d60e47..9dd764be1bb 100644 --- a/pkg/middleware/org_redirect.go +++ b/pkg/middleware/org_redirect.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" @@ -41,7 +42,7 @@ func OrgRedirect() macaron.Handler { return } - newUrl := setting.ToAbsUrl(fmt.Sprintf("%s?%s", c.Req.URL.Path, c.Req.URL.Query().Encode())) - c.Redirect(newUrl, 302) + newURL := setting.ToAbsUrl(fmt.Sprintf("%s?%s", strings.TrimPrefix(c.Req.URL.Path, "/"), c.Req.URL.Query().Encode())) + c.Redirect(newURL, 302) } } diff --git a/pkg/middleware/render_auth.go b/pkg/middleware/render_auth.go index 3a57660c9bf..d2f9c1b2b1a 100644 --- a/pkg/middleware/render_auth.go +++ b/pkg/middleware/render_auth.go @@ -33,14 +33,15 @@ func initContextWithRenderAuth(ctx *Context) bool { type renderContextFunc func(key string) (string, error) -func AddRenderAuthKey(orgId int64) string { +func AddRenderAuthKey(orgId int64, userId int64, orgRole m.RoleType) string { renderKeysLock.Lock() key := util.GetRandomString(32) renderKeys[key] = &m.SignedInUser{ OrgId: orgId, - OrgRole: m.ROLE_VIEWER, + OrgRole: orgRole, + UserId: userId, } renderKeysLock.Unlock() diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go new file mode 100644 index 00000000000..fa7ad00de7f --- /dev/null +++ b/pkg/models/dashboard_acl.go @@ -0,0 +1,95 @@ +package models + +import ( + "errors" + "time" +) + +type PermissionType int + +const ( + PERMISSION_VIEW PermissionType = 1 << iota + PERMISSION_EDIT + PERMISSION_ADMIN +) + +func (p PermissionType) String() string { + names := map[int]string{ + int(PERMISSION_VIEW): "View", + int(PERMISSION_EDIT): "Edit", + int(PERMISSION_ADMIN): "Admin", + } + return names[int(p)] +} + +// Typed errors +var ( + ErrDashboardAclInfoMissing = errors.New("User id and team id cannot both be empty for a dashboard permission.") + ErrDashboardPermissionDashboardEmpty = errors.New("Dashboard Id must be greater than zero for a dashboard permission.") +) + +// Dashboard ACL model +type DashboardAcl struct { + Id int64 + OrgId int64 + DashboardId int64 + + UserId int64 + TeamId int64 + Role *RoleType // pointer to be nullable + Permission PermissionType + + Created time.Time + Updated time.Time +} + +type DashboardAclInfoDTO struct { + Id int64 `json:"id"` + OrgId int64 `json:"-"` + DashboardId int64 `json:"dashboardId"` + + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + + UserId int64 `json:"userId"` + UserLogin string `json:"userLogin"` + UserEmail string `json:"userEmail"` + TeamId int64 `json:"teamId"` + Team string `json:"team"` + Role *RoleType `json:"role,omitempty"` + Permission PermissionType `json:"permission"` + PermissionName string `json:"permissionName"` +} + +// +// COMMANDS +// + +type UpdateDashboardAclCommand struct { + DashboardId int64 + Items []*DashboardAcl +} + +type SetDashboardAclCommand struct { + DashboardId int64 + OrgId int64 + UserId int64 + TeamId int64 + Permission PermissionType + + Result DashboardAcl +} + +type RemoveDashboardAclCommand struct { + AclId int64 + OrgId int64 +} + +// +// QUERIES +// +type GetDashboardAclInfoListQuery struct { + DashboardId int64 + OrgId int64 + Result []*DashboardAclInfoDTO +} diff --git a/pkg/models/dashboard_acl_test.go b/pkg/models/dashboard_acl_test.go new file mode 100644 index 00000000000..35357ff1cc9 --- /dev/null +++ b/pkg/models/dashboard_acl_test.go @@ -0,0 +1,21 @@ +package models + +import ( + "testing" + + "fmt" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestDashboardAclModel(t *testing.T) { + + Convey("When printing a PermissionType", t, func() { + view := PERMISSION_VIEW + printed := fmt.Sprint(view) + + Convey("Should output a friendly name", func() { + So(printed, ShouldEqual, "View") + }) + }) +} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 0463e9c209b..091f27ec413 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -11,11 +11,14 @@ import ( // Typed errors var ( - ErrDashboardNotFound = errors.New("Dashboard not found") - ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") - ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists") - ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") - ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") + ErrDashboardNotFound = errors.New("Dashboard not found") + ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") + ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists") + ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") + ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") + ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder") + ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard") + ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data") ) type UpdatePluginDashboardError struct { @@ -47,6 +50,9 @@ type Dashboard struct { UpdatedBy int64 CreatedBy int64 + FolderId int64 + IsFolder bool + HasAcl bool Title string Data *simplejson.Json @@ -64,6 +70,15 @@ func NewDashboard(title string) *Dashboard { return dash } +// NewDashboardFolder creates a new dashboard folder +func NewDashboardFolder(title string) *Dashboard { + folder := NewDashboard(title) + folder.Data.Set("schemaVersion", 16) + folder.Data.Set("editable", true) + folder.Data.Set("hideControls", true) + return folder +} + // GetTags turns the tags in data json into go string array func (dash *Dashboard) GetTags() []string { return dash.Data.Get("tags").MustStringArray() @@ -111,6 +126,8 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash.UpdatedBy = userId dash.OrgId = cmd.OrgId dash.PluginId = cmd.PluginId + dash.IsFolder = cmd.IsFolder + dash.FolderId = cmd.FolderId dash.UpdateSlug() return dash } @@ -122,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)) } // @@ -138,12 +159,16 @@ type SaveDashboardCommand struct { OrgId int64 `json:"-"` RestoredFrom int `json:"-"` PluginId string `json:"-"` + FolderId int64 `json:"folderId"` + IsFolder bool `json:"isFolder"` + + UpdatedAt time.Time Result *Dashboard } type DeleteDashboardCommand struct { - Slug string + Id int64 OrgId int64 } diff --git a/pkg/models/dashboards_test.go b/pkg/models/dashboards_test.go index ee16508dc8a..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") @@ -28,4 +34,27 @@ func TestDashboardModel(t *testing.T) { }) }) + Convey("Given a new dashboard folder", t, func() { + json := simplejson.New() + json.Set("title", "test dash") + + cmd := &SaveDashboardCommand{Dashboard: json, IsFolder: true} + dash := cmd.GetDashboardModel() + + Convey("Should set IsFolder to true", func() { + So(dash.IsFolder, ShouldBeTrue) + }) + }) + + Convey("Given a child dashboard", t, func() { + json := simplejson.New() + json.Set("title", "test dash") + + cmd := &SaveDashboardCommand{Dashboard: json, FolderId: 1} + dash := cmd.GetDashboardModel() + + Convey("Should set FolderId", func() { + So(dash.FolderId, ShouldEqual, 1) + }) + }) } diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index 67b081fd1ce..9379625d458 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -18,25 +18,25 @@ var ( type RoleType string const ( - ROLE_VIEWER RoleType = "Viewer" - ROLE_EDITOR RoleType = "Editor" - ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor" - ROLE_ADMIN RoleType = "Admin" + ROLE_VIEWER RoleType = "Viewer" + ROLE_EDITOR RoleType = "Editor" + ROLE_ADMIN RoleType = "Admin" ) func (r RoleType) IsValid() bool { - return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR + return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR } func (r RoleType) Includes(other RoleType) bool { if r == ROLE_ADMIN { return true } - if r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR { + + if r == ROLE_EDITOR { return other != ROLE_ADMIN } - return r == other + return false } func (r *RoleType) UnmarshalJSON(data []byte) error { @@ -106,6 +106,7 @@ type OrgUserDTO struct { OrgId int64 `json:"orgId"` UserId int64 `json:"userId"` Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` Login string `json:"login"` Role string `json:"role"` LastSeenAt time.Time `json:"lastSeenAt"` diff --git a/pkg/models/server.go b/pkg/models/server.go deleted file mode 100644 index 4d683835256..00000000000 --- a/pkg/models/server.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - -type GrafanaServer interface { - Start() - Shutdown(code int, reason string) -} diff --git a/pkg/models/team.go b/pkg/models/team.go new file mode 100644 index 00000000000..d2912f431b8 --- /dev/null +++ b/pkg/models/team.go @@ -0,0 +1,80 @@ +package models + +import ( + "errors" + "time" +) + +// Typed errors +var ( + ErrTeamNotFound = errors.New("Team not found") + ErrTeamNameTaken = errors.New("Team name is taken") +) + +// Team model +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"` +} + +// --------------------- +// COMMANDS + +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 + Email string +} + +type DeleteTeamCommand struct { + Id int64 +} + +type GetTeamByIdQuery struct { + Id int64 + Result *Team +} + +type GetTeamsByUserQuery struct { + UserId int64 `json:"userId"` + Result []*Team `json:"teams"` +} + +type SearchTeamsQuery struct { + Query string + Name string + Limit int + Page int + OrgId int64 + + Result SearchTeamQueryResult +} + +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"` +} + +type SearchTeamQueryResult struct { + TotalCount int64 `json:"totalCount"` + Teams []*SearchTeamDto `json:"teams"` + Page int `json:"page"` + PerPage int `json:"perPage"` +} diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go new file mode 100644 index 00000000000..9970678a1ae --- /dev/null +++ b/pkg/models/team_member.go @@ -0,0 +1,56 @@ +package models + +import ( + "errors" + "time" +) + +// Typed errors +var ( + ErrTeamMemberAlreadyAdded = errors.New("User is already added to this team") +) + +// TeamMember model +type TeamMember struct { + Id int64 + OrgId int64 + TeamId int64 + UserId int64 + + Created time.Time + Updated time.Time +} + +// --------------------- +// COMMANDS + +type AddTeamMemberCommand struct { + UserId int64 `json:"userId" binding:"Required"` + OrgId int64 `json:"-"` + TeamId int64 `json:"-"` +} + +type RemoveTeamMemberCommand struct { + UserId int64 + TeamId int64 +} + +// ---------------------- +// QUERIES + +type GetTeamMembersQuery struct { + TeamId int64 + Result []*TeamMemberDTO +} + +// ---------------------- +// 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"` + AvatarUrl string `json:"avatarUrl"` +} diff --git a/pkg/models/user.go b/pkg/models/user.go index 77938b63a0b..d5b912e0a9c 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -160,7 +160,9 @@ type SignedInUser struct { Name string Email string ApiKeyId int64 + OrgCount int IsGrafanaAdmin bool + IsAnonymous bool HelpFlags1 HelpFlags1 LastSeenAt time.Time } @@ -169,10 +171,28 @@ func (u *SignedInUser) ShouldUpdateLastSeenAt() bool { return u.UserId > 0 && time.Since(u.LastSeenAt) > time.Minute*5 } +func (u *SignedInUser) NameOrFallback() string { + if u.Name != "" { + return u.Name + } else if u.Login != "" { + return u.Login + } else { + return u.Email + } +} + type UpdateUserLastSeenAtCommand struct { UserId int64 } +func (user *SignedInUser) HasRole(role RoleType) bool { + if user.IsGrafanaAdmin { + return true + } + + return user.OrgRole.Includes(role) +} + type UserProfileDTO struct { Id int64 `json:"id"` Email string `json:"email"` @@ -188,6 +208,7 @@ type UserSearchHitDTO struct { Name string `json:"name"` Login string `json:"login"` Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` IsAdmin bool `json:"isAdmin"` LastSeenAt time.Time `json:"lastSeenAt"` LastSeenAtAge string `json:"lastSeenAtAge"` diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index 1b3e4bac182..bf516818e3c 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -69,6 +69,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { UserId: cmd.UserId, Overwrite: cmd.Overwrite, PluginId: cmd.PluginId, + FolderId: dashboard.FolderId, } if err := bus.Dispatch(&saveCmd); err != nil { diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index d2897fad1cd..78df94309f8 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -13,16 +13,9 @@ import ( ) func TestDashboardImport(t *testing.T) { - - Convey("When importing plugin dashboard", t, func() { - setting.Cfg = ini.Empty() - sec, _ := setting.Cfg.NewSection("plugin.test-app") - sec.NewKey("path", "../../tests/test-app") - err := Init() - - So(err, ShouldBeNil) - + pluginScenario("When importing a plugin dashboard", t, func() { var importedDash *m.Dashboard + bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error { importedDash = cmd.GetDashboardModel() cmd.Result = importedDash @@ -39,7 +32,7 @@ func TestDashboardImport(t *testing.T) { }, } - err = ImportDashboard(&cmd) + err := ImportDashboard(&cmd) So(err, ShouldBeNil) Convey("should install dashboard", func() { @@ -59,16 +52,16 @@ func TestDashboardImport(t *testing.T) { Convey("When evaling dashboard template", t, func() { template, _ := simplejson.NewJson([]byte(`{ - "__inputs": [ - { - "name": "DS_NAME", - "type": "datasource" - } - ], - "test": { - "prop": "${DS_NAME}" - } - }`)) + "__inputs": [ + { + "name": "DS_NAME", + "type": "datasource" + } + ], + "test": { + "prop": "${DS_NAME}" + } + }`)) evaluator := &DashTemplateEvaluator{ template: template, @@ -92,3 +85,16 @@ func TestDashboardImport(t *testing.T) { }) } + +func pluginScenario(desc string, t *testing.T, fn func()) { + Convey("Given a plugin", t, func() { + setting.Cfg = ini.Empty() + sec, _ := setting.Cfg.NewSection("plugin.test-app") + sec.NewKey("path", "../../tests/test-app") + err := Init() + + So(err, ShouldBeNil) + + Convey(desc, fn) + }) +} diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index 5d26766b8e5..37e3d8c0076 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -15,6 +15,7 @@ type PluginDashboardInfoDTO struct { Imported bool `json:"imported"` ImportedUri string `json:"importedUri"` Slug string `json:"slug"` + DashboardId int64 `json:"dashboardId"` ImportedRevision int64 `json:"importedRevision"` Revision int64 `json:"revision"` Description string `json:"description"` @@ -60,6 +61,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT // find existing dashboard for _, existingDash := range query.Result { if existingDash.Slug == dashboard.Slug { + res.DashboardId = existingDash.Id res.Imported = true res.ImportedUri = "db/" + existingDash.Slug res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1) @@ -74,8 +76,9 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT for _, dash := range query.Result { if _, exists := existingMatches[dash.Id]; !exists { result = append(result, &PluginDashboardInfoDTO{ - Slug: dash.Slug, - Removed: true, + Slug: dash.Slug, + DashboardId: dash.Id, + Removed: true, }) } } diff --git a/pkg/plugins/dashboards_updater.go b/pkg/plugins/dashboards_updater.go index 52a623e73dd..4c40e536d14 100644 --- a/pkg/plugins/dashboards_updater.go +++ b/pkg/plugins/dashboards_updater.go @@ -75,7 +75,7 @@ func syncPluginDashboards(pluginDef *PluginBase, orgId int64) { if dash.Removed { plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug) - deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Slug: dash.Slug} + deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Id: dash.DashboardId} if err := bus.Dispatch(&deleteCmd); err != nil { plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err) return @@ -124,7 +124,7 @@ func handlePluginStateChanged(event *m.PluginStateChangedEvent) error { return err } else { for _, dash := range query.Result { - deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Slug: dash.Slug} + deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id} plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index e92edd2af12..f5663deb8ca 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -75,14 +75,6 @@ func (c *EvalContext) ShouldUpdateAlertState() bool { return c.Rule.State != c.PrevAlertState } -func (c *EvalContext) ShouldSendNotification() bool { - if (c.PrevAlertState == m.AlertStatePending) && (c.Rule.State == m.AlertStateOK) { - return false - } - - return true -} - func (a *EvalContext) GetDurationMs() float64 { return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000) } diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 5cff2996ca6..019ca1ed01f 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -28,21 +28,5 @@ func TestAlertingEvalContext(t *testing.T) { So(ctx.ShouldUpdateAlertState(), ShouldBeFalse) }) }) - - Convey("Should send notifications", func() { - Convey("pending -> ok", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Rule.State = models.AlertStateOK - - So(ctx.ShouldSendNotification(), ShouldBeFalse) - }) - - Convey("ok -> alerting", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.State = models.AlertStateAlerting - - So(ctx.ShouldSendNotification(), ShouldBeTrue) - }) - }) }) } diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index 79b2f231b41..457e02000fa 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -39,6 +39,11 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { break } + if i == 0 { + firing = cr.Firing + noDataFound = cr.NoDataFound + } + // calculating Firing based on operator if cr.Operator == "or" { firing = firing || cr.Firing diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index cf6422a5250..c942e24818f 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -36,6 +36,16 @@ func TestAlertingEvaluationHandler(t *testing.T) { So(context.ConditionEvals, ShouldEqual, "true = true") }) + Convey("Show return triggered with single passing condition2", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{&conditionStub{firing: true, operator: "and"}}, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, true) + So(context.ConditionEvals, ShouldEqual, "true = true") + }) + Convey("Show return false with not passing asdf", func() { context := NewEvalContext(context.TODO(), &Rule{ Conditions: []Condition{ @@ -131,6 +141,33 @@ func TestAlertingEvaluationHandler(t *testing.T) { So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true") }) + Convey("Should return false if no condition is firing using OR operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: false, operator: "or"}, + &conditionStub{firing: false, operator: "or"}, + &conditionStub{firing: false, operator: "or"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, false) + So(context.ConditionEvals, ShouldEqual, "[[false OR false] OR false] = false") + }) + + Convey("Should retuasdfrn no data if one condition has nodata", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{operator: "or", noData: false}, + &conditionStub{operator: "or", noData: false}, + &conditionStub{operator: "or", noData: false}, + }, + }) + + handler.Eval(context) + So(context.NoDataFound, ShouldBeFalse) + }) + Convey("Should return no data if one condition has nodata", func() { context := NewEvalContext(context.TODO(), &Rule{ Conditions: []Condition{ diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index b8579ffdc6a..a609824cbc8 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -69,6 +69,90 @@ func copyJson(in *simplejson.Json) (*simplejson.Json, error) { return simplejson.NewJson(rawJson) } +func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) ([]*m.Alert, error) { + alerts := make([]*m.Alert, 0) + + for _, panelObj := range jsonWithPanels.Get("panels").MustArray() { + panel := simplejson.NewFromAny(panelObj) + jsonAlert, hasAlert := panel.CheckGet("alert") + + if !hasAlert { + continue + } + + panelId, err := panel.Get("id").Int64() + if err != nil { + return nil, fmt.Errorf("panel id is required. err %v", err) + } + + // backward compatibility check, can be removed later + enabled, hasEnabled := jsonAlert.CheckGet("enabled") + if hasEnabled && enabled.MustBool() == false { + continue + } + + frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString()) + if err != nil { + return nil, ValidationError{Reason: "Could not parse frequency"} + } + + alert := &m.Alert{ + DashboardId: e.Dash.Id, + OrgId: e.OrgId, + PanelId: panelId, + Id: jsonAlert.Get("id").MustInt64(), + Name: jsonAlert.Get("name").MustString(), + Handler: jsonAlert.Get("handler").MustInt64(), + Message: jsonAlert.Get("message").MustString(), + Frequency: frequency, + } + + for _, condition := range jsonAlert.Get("conditions").MustArray() { + jsonCondition := simplejson.NewFromAny(condition) + + jsonQuery := jsonCondition.Get("query") + queryRefId := jsonQuery.Get("params").MustArray()[0].(string) + panelQuery := findPanelQueryByRefId(panel, queryRefId) + + if panelQuery == nil { + reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId) + return nil, ValidationError{Reason: reason} + } + + dsName := "" + if panelQuery.Get("datasource").MustString() != "" { + dsName = panelQuery.Get("datasource").MustString() + } else if panel.Get("datasource").MustString() != "" { + dsName = panel.Get("datasource").MustString() + } + + if datasource, err := e.lookupDatasourceId(dsName); err != nil { + return nil, err + } else { + jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id) + } + + if interval, err := panel.Get("interval").String(); err == nil { + panelQuery.Set("interval", interval) + } + + jsonQuery.Set("model", panelQuery.Interface()) + } + + alert.Settings = jsonAlert + + // validate + _, err = NewRuleFromDBAlert(alert) + if err == nil && alert.ValidToSave() { + alerts = append(alerts, alert) + } else { + return nil, err + } + } + + return alerts, nil +} + func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { e.log.Debug("GetAlerts") @@ -78,86 +162,27 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { } alerts := make([]*m.Alert, 0) - for _, rowObj := range dashboardJson.Get("rows").MustArray() { - row := simplejson.NewFromAny(rowObj) - for _, panelObj := range row.Get("panels").MustArray() { - panel := simplejson.NewFromAny(panelObj) - jsonAlert, hasAlert := panel.CheckGet("alert") - - if !hasAlert { - continue - } - - panelId, err := panel.Get("id").Int64() + // We extract alerts from rows to be backwards compatible + // with the old dashboard json model. + rows := dashboardJson.Get("rows").MustArray() + if len(rows) > 0 { + for _, rowObj := range rows { + row := simplejson.NewFromAny(rowObj) + a, err := e.GetAlertFromPanels(row) if err != nil { - return nil, fmt.Errorf("panel id is required. err %v", err) - } - - // backward compatibility check, can be removed later - enabled, hasEnabled := jsonAlert.CheckGet("enabled") - if hasEnabled && enabled.MustBool() == false { - continue - } - - frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString()) - if err != nil { - return nil, ValidationError{Reason: "Could not parse frequency"} - } - - alert := &m.Alert{ - DashboardId: e.Dash.Id, - OrgId: e.OrgId, - PanelId: panelId, - Id: jsonAlert.Get("id").MustInt64(), - Name: jsonAlert.Get("name").MustString(), - Handler: jsonAlert.Get("handler").MustInt64(), - Message: jsonAlert.Get("message").MustString(), - Frequency: frequency, - } - - for _, condition := range jsonAlert.Get("conditions").MustArray() { - jsonCondition := simplejson.NewFromAny(condition) - - jsonQuery := jsonCondition.Get("query") - queryRefId := jsonQuery.Get("params").MustArray()[0].(string) - panelQuery := findPanelQueryByRefId(panel, queryRefId) - - if panelQuery == nil { - reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId) - return nil, ValidationError{Reason: reason} - } - - dsName := "" - if panelQuery.Get("datasource").MustString() != "" { - dsName = panelQuery.Get("datasource").MustString() - } else if panel.Get("datasource").MustString() != "" { - dsName = panel.Get("datasource").MustString() - } - - if datasource, err := e.lookupDatasourceId(dsName); err != nil { - return nil, err - } else { - jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id) - } - - if interval, err := panel.Get("interval").String(); err == nil { - panelQuery.Set("interval", interval) - } - - jsonQuery.Set("model", panelQuery.Interface()) - } - - alert.Settings = jsonAlert - - // validate - _, err = NewRuleFromDBAlert(alert) - if err == nil && alert.ValidToSave() { - alerts = append(alerts, alert) - } else { return nil, err } + + alerts = append(alerts, a...) } + } else { + a, err := e.GetAlertFromPanels(dashboardJson) + if err != nil { + return nil, err + } + + alerts = append(alerts, a...) } e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts)) diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index b7f83404452..71f3026025d 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -1,12 +1,12 @@ package alerting import ( + "io/ioutil" "testing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" ) @@ -18,10 +18,6 @@ func TestAlertRuleExtraction(t *testing.T) { return &FakeCondition{}, nil }) - setting.NewConfigContext(&setting.CommandLineArgs{ - HomePath: "../../../", - }) - // mock data defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true} graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"} @@ -45,70 +41,8 @@ func TestAlertRuleExtraction(t *testing.T) { return nil }) - json := ` - { - "id": 57, - "title": "Graphite 4", - "originalTitle": "Graphite 4", - "tags": ["graphite"], - "rows": [ - { - "panels": [ - { - "title": "Active desktop users", - "editable": true, - "type": "graph", - "id": 3, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)" - } - ], - "datasource": null, - "alert": { - "name": "name1", - "message": "desc1", - "handler": 1, - "frequency": "60s", - "conditions": [ - { - "type": "query", - "query": {"params": ["A", "5m", "now"]}, - "reducer": {"type": "avg", "params": []}, - "evaluator": {"type": ">", "params": [100]} - } - ] - } - }, - { - "title": "Active mobile users", - "id": 4, - "targets": [ - {"refId": "A", "target": ""}, - {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} - ], - "datasource": "graphite2", - "alert": { - "name": "name2", - "message": "desc2", - "handler": 0, - "frequency": "60s", - "severity": "warning", - "conditions": [ - { - "type": "query", - "query": {"params": ["B", "5m", "now"]}, - "reducer": {"type": "avg", "params": []}, - "evaluator": {"type": ">", "params": [100]} - } - ] - } - } - ] - } - ] - }` + json, err := ioutil.ReadFile("./test-data/graphite-alert.json") + So(err, ShouldBeNil) Convey("Extractor should not modify the original json", func() { dashJson, err := simplejson.NewJson([]byte(json)) @@ -201,69 +135,8 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Panels missing id should return error", func() { - panelWithoutId := ` - { - "id": 57, - "title": "Graphite 4", - "originalTitle": "Graphite 4", - "tags": ["graphite"], - "rows": [ - { - "panels": [ - { - "title": "Active desktop users", - "editable": true, - "type": "graph", - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)" - } - ], - "datasource": null, - "alert": { - "name": "name1", - "message": "desc1", - "handler": 1, - "frequency": "60s", - "conditions": [ - { - "type": "query", - "query": {"params": ["A", "5m", "now"]}, - "reducer": {"type": "avg", "params": []}, - "evaluator": {"type": ">", "params": [100]} - } - ] - } - }, - { - "title": "Active mobile users", - "id": 4, - "targets": [ - {"refId": "A", "target": ""}, - {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} - ], - "datasource": "graphite2", - "alert": { - "name": "name2", - "message": "desc2", - "handler": 0, - "frequency": "60s", - "severity": "warning", - "conditions": [ - { - "type": "query", - "query": {"params": ["B", "5m", "now"]}, - "reducer": {"type": "avg", "params": []}, - "evaluator": {"type": ">", "params": [100]} - } - ] - } - } - ] - } - ] - }` + panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json") + So(err, ShouldBeNil) dashJson, err := simplejson.NewJson([]byte(panelWithoutId)) So(err, ShouldBeNil) @@ -277,294 +150,31 @@ func TestAlertRuleExtraction(t *testing.T) { }) }) + Convey("Parse alerts from dashboard without rows", func() { + json, err := ioutil.ReadFile("./test-data/v5-dashboard.json") + So(err, ShouldBeNil) + + dashJson, err := simplejson.NewJson(json) + So(err, ShouldBeNil) + dash := m.NewDashboardFromJson(dashJson) + extractor := NewDashAlertExtractor(dash, 1) + + alerts, err := extractor.GetAlerts() + + Convey("Get rules without error", func() { + So(err, ShouldBeNil) + }) + + Convey("Should have 2 alert rule", func() { + So(len(alerts), ShouldEqual, 2) + }) + }) + Convey("Parse and validate dashboard containing influxdb alert", func() { + json, err := ioutil.ReadFile("./test-data/influxdb-alert.json") + So(err, ShouldBeNil) - json2 := `{ - "id": 4, - "title": "Influxdb", - "tags": [ - "apa" - ], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "sharedCrosshair": false, - "rows": [ - { - "collapse": false, - "editable": true, - "height": "450px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 10 - ], - "type": "gt" - }, - "query": { - "params": [ - "B", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "frequency": "3s", - "handler": 1, - "name": "Influxdb", - "noDataState": "no_data", - "notifications": [ - { - "id": 6 - } - ] - }, - "alerting": {}, - "aliasColors": { - "logins.count.count": "#890F02" - }, - "bars": false, - "datasource": "InfluxDB", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 1, - "interval": ">10s", - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "groupBy": [ - { - "params": [ - "$interval" - ], - "type": "time" - }, - { - "params": [ - "datacenter" - ], - "type": "tag" - }, - { - "params": [ - "none" - ], - "type": "fill" - } - ], - "hide": false, - "measurement": "logins.count", - "policy": "default", - "query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)", - "rawQuery": true, - "refId": "B", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "count" - } - ] - ], - "tags": [] - }, - { - "dsType": "influxdb", - "groupBy": [ - { - "params": [ - "$interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": true, - "measurement": "cpu", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ], - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "sum" - } - ] - ], - "tags": [] - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "line": true, - "op": "gt", - "value": 10 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Panel Title", - "tooltip": { - "msResolution": false, - "ordering": "alphabetical", - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "editable": true, - "error": false, - "id": 2, - "isNew": true, - "limit": 10, - "links": [], - "show": "current", - "span": 2, - "stateFilter": [ - "alerting" - ], - "title": "Alert status", - "type": "alertlist" - } - ], - "title": "Row" - } - ], - "time": { - "from": "now-5m", - "to": "now" - }, - "timepicker": { - "now": true, - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 120, - "links": [], - "gnetId": null - }` - - dashJson, err := simplejson.NewJson([]byte(json2)) + dashJson, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) extractor := NewDashAlertExtractor(dash, 1) diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 0955155575c..18f969ba1b9 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -15,7 +15,7 @@ type Notifier interface { Notify(evalContext *EvalContext) error GetType() string NeedsImage() bool - PassesFilter(rule *Rule) bool + ShouldNotify(evalContext *EvalContext) bool GetNotifierId() int64 GetIsDefault() bool diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index be74d41cc6c..47c9e0c590e 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -24,7 +24,7 @@ type NotifierPlugin struct { } type NotificationService interface { - Send(context *EvalContext) error + SendIfNeeded(context *EvalContext) error } func NewNotificationService() NotificationService { @@ -41,14 +41,12 @@ func newNotificationService() *notificationService { } } -func (n *notificationService) Send(context *EvalContext) error { - notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context) +func (n *notificationService) SendIfNeeded(context *EvalContext) error { + notifiers, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context) if err != nil { return err } - n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers)) - if len(notifiers) == 0 { return nil } @@ -67,7 +65,7 @@ func (n *notificationService) sendNotifications(context *EvalContext, notifiers for _, notifier := range notifiers { not := notifier //avoid updating scope variable in go routine - n.log.Info("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) + n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() g.Go(func() error { return not.Notify(context) }) } @@ -82,10 +80,11 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { } renderOpts := &renderer.RenderOpts{ - Width: "800", - Height: "400", - Timeout: "30", - OrgId: context.Rule.OrgId, + Width: "800", + Height: "400", + Timeout: "30", + OrgId: context.Rule.OrgId, + IsAlertContext: true, } if slug, err := context.GetDashboardSlug(); err != nil { @@ -109,7 +108,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { return nil } -func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) { +func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) { query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds} if err := bus.Dispatch(query); err != nil { @@ -121,7 +120,7 @@ func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, if not, err := n.createNotifierFor(notification); err != nil { return nil, err } else { - if shouldUseNotification(not, context) { + if not.ShouldNotify(context) { result = append(result, not) } } @@ -139,18 +138,6 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not return notifierPlugin.Factory(model) } -func shouldUseNotification(notifier Notifier, context *EvalContext) bool { - if !context.Firing { - return true - } - - if context.Error != nil { - return true - } - - return notifier.PassesFilter(context.Rule) -} - type NotifierFactory func(notification *m.AlertNotification) (Notifier, error) var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin) diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go deleted file mode 100644 index 5e378dec890..00000000000 --- a/pkg/services/alerting/notifier_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package alerting - -import ( - "testing" - - "fmt" - - "github.com/grafana/grafana/pkg/models" - m "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" -) - -type FakeNotifier struct { - FakeMatchResult bool -} - -func (fn *FakeNotifier) GetType() string { - return "FakeNotifier" -} - -func (fn *FakeNotifier) NeedsImage() bool { - return true -} - -func (n *FakeNotifier) GetNotifierId() int64 { - return 0 -} - -func (n *FakeNotifier) GetIsDefault() bool { - return false -} - -func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil } - -func (fn *FakeNotifier) PassesFilter(rule *Rule) bool { - return fn.FakeMatchResult -} - -func TestAlertNotificationExtraction(t *testing.T) { - - Convey("Notifier tests", t, func() { - Convey("none firing alerts", func() { - ctx := &EvalContext{ - Firing: false, - Rule: &Rule{ - State: m.AlertStateAlerting, - }, - } - notifier := &FakeNotifier{FakeMatchResult: false} - - So(shouldUseNotification(notifier, ctx), ShouldBeTrue) - }) - - Convey("execution error cannot be ignored", func() { - ctx := &EvalContext{ - Firing: true, - Error: fmt.Errorf("I used to be a programmer just like you"), - Rule: &Rule{ - State: m.AlertStateOK, - }, - } - notifier := &FakeNotifier{FakeMatchResult: false} - - So(shouldUseNotification(notifier, ctx), ShouldBeTrue) - }) - - Convey("firing alert that match", func() { - ctx := &EvalContext{ - Firing: true, - Rule: &Rule{ - State: models.AlertStateAlerting, - }, - } - notifier := &FakeNotifier{FakeMatchResult: true} - - So(shouldUseNotification(notifier, ctx), ShouldBeTrue) - }) - - Convey("firing alert that dont match", func() { - ctx := &EvalContext{ - Firing: true, - Rule: &Rule{State: m.AlertStateOK}, - } - notifier := &FakeNotifier{FakeMatchResult: false} - - So(shouldUseNotification(notifier, ctx), ShouldBeFalse) - }) - }) -} diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go new file mode 100644 index 00000000000..08f8e8be29c --- /dev/null +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -0,0 +1,96 @@ +package notifiers + +import ( + "time" + + "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" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "prometheus-alertmanager", + Name: "Prometheus Alertmanager", + Description: "Sends alert to Prometheus Alertmanager", + Factory: NewAlertmanagerNotifier, + OptionsTemplate: ` +

Alertmanager settings

+
+ Url + +
+ `, + }) +} + +func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + url := model.Settings.Get("url").MustString() + if url == "" { + return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} + } + + return &AlertmanagerNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + Url: url, + log: log.New("alerting.notifier.prometheus-alertmanager"), + }, nil +} + +type AlertmanagerNotifier struct { + NotifierBase + Url string + log log.Logger +} + +func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool { + return evalContext.Rule.State == m.AlertStateAlerting +} + +func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error { + + alerts := make([]interface{}, 0) + for _, match := range evalContext.EvalMatches { + alertJSON := simplejson.New() + alertJSON.Set("startsAt", evalContext.StartTime.UTC().Format(time.RFC3339)) + + if ruleUrl, err := evalContext.GetRuleUrl(); err == nil { + alertJSON.Set("generatorURL", ruleUrl) + } + + if evalContext.Rule.Message != "" { + alertJSON.SetPath([]string{"annotations", "description"}, evalContext.Rule.Message) + } + + tags := make(map[string]string) + if len(match.Tags) == 0 { + tags["metric"] = match.Metric + } else { + for k, v := range match.Tags { + tags[k] = v + } + } + tags["alertname"] = evalContext.Rule.Name + alertJSON.Set("labels", tags) + + alerts = append(alerts, alertJSON) + } + + bodyJSON := simplejson.NewFromAny(alerts) + body, _ := bodyJSON.MarshalJSON() + + cmd := &m.SendWebhookSync{ + Url: this.Url + "/api/v1/alerts", + HttpMethod: "POST", + Body: string(body), + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send alertmanager", "error", err, "alertmanager", this.Name) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/alertmanager_test.go b/pkg/services/alerting/notifiers/alertmanager_test.go new file mode 100644 index 00000000000..3549b536e48 --- /dev/null +++ b/pkg/services/alerting/notifiers/alertmanager_test.go @@ -0,0 +1,47 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestAlertmanagerNotifier(t *testing.T) { + Convey("Alertmanager notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "alertmanager", + Type: "alertmanager", + Settings: settingsJSON, + } + + _, err := NewAlertmanagerNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("from settings", func() { + json := `{ "url": "http://127.0.0.1:9093/" }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "alertmanager", + Type: "alertmanager", + Settings: settingsJSON, + } + + not, err := NewAlertmanagerNotifier(model) + alertmanagerNotifier := not.(*AlertmanagerNotifier) + + So(err, ShouldBeNil) + So(alertmanagerNotifier.Url, ShouldEqual, "http://127.0.0.1:9093/") + }) + }) + }) +} diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index dc22e1acaa0..601f8fc24b1 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -2,6 +2,7 @@ package notifiers import ( "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" ) @@ -14,7 +15,7 @@ type NotifierBase struct { } func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase { - uploadImage := model.Get("uploadImage").MustBool(true) + uploadImage := model.Get("uploadImage").MustBool(false) return NotifierBase{ Id: id, @@ -25,7 +26,13 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model } } -func (n *NotifierBase) PassesFilter(rule *alerting.Rule) bool { +func defaultShouldNotify(context *alerting.EvalContext) bool { + if context.PrevAlertState == context.Rule.State { + return false + } + if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) { + return false + } return true } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go new file mode 100644 index 00000000000..4225e203a3d --- /dev/null +++ b/pkg/services/alerting/notifiers/base_test.go @@ -0,0 +1,32 @@ +package notifiers + +import ( + "context" + "testing" + + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestBaseNotifier(t *testing.T) { + Convey("Base notifier tests", t, func() { + Convey("should notify", func() { + Convey("pending -> ok", func() { + context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ + State: m.AlertStatePending, + }) + context.Rule.State = m.AlertStateOK + So(defaultShouldNotify(context), ShouldBeFalse) + }) + + Convey("ok -> alerting", func() { + context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ + State: m.AlertStateOK, + }) + context.Rule.State = m.AlertStateAlerting + So(defaultShouldNotify(context), ShouldBeTrue) + }) + }) + }) +} diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index e32b9d34f91..c2029b1173c 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -38,6 +38,10 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) }, nil } +func (this *DingDingNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + type DingDingNotifier struct { NotifierBase Url string diff --git a/pkg/services/alerting/notifiers/dingding_test.go b/pkg/services/alerting/notifiers/dingding_test.go index 3ca267dbf5b..f89bf6382ce 100644 --- a/pkg/services/alerting/notifiers/dingding_test.go +++ b/pkg/services/alerting/notifiers/dingding_test.go @@ -9,7 +9,7 @@ import ( ) func TestDingDingNotifier(t *testing.T) { - Convey("Line notifier tests", t, func() { + Convey("Dingding notifier tests", t, func() { Convey("empty settings should return error", func() { json := `{ }` @@ -25,10 +25,8 @@ func TestDingDingNotifier(t *testing.T) { }) Convey("settings should trigger incident", func() { - json := ` - { - "url": "https://www.google.com" - }` + json := `{ "url": "https://www.google.com" }` + settingsJSON, _ := simplejson.NewJson([]byte(json)) model := &m.AlertNotification{ Name: "dingding_testing", diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 7e8c4b33c0c..f84dc886d83 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -58,6 +58,10 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { }, nil } +func (this *EmailNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Sending alert notification to", "addresses", this.Addresses) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index f1f63d42a04..b65f25b1422 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -75,6 +75,10 @@ type HipChatNotifier struct { log log.Logger } +func (this *HipChatNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index 92f6489106b..e885d44405d 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -57,6 +57,10 @@ type KafkaNotifier struct { log log.Logger } +func (this *KafkaNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error { state := evalContext.Rule.State diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index 4fbaa2d543e..bc0b0c984a4 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -51,6 +51,10 @@ type LineNotifier struct { log log.Logger } +func (this *LineNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index aea02465177..1a812f49ca3 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -62,6 +62,10 @@ type OpsGenieNotifier struct { log log.Logger } +func (this *OpsGenieNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error { var err error diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index c36dde63943..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"} @@ -63,6 +63,10 @@ type PagerdutyNotifier struct { log log.Logger } +func (this *PagerdutyNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve { 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/pushover.go b/pkg/services/alerting/notifiers/pushover.go index ecb4ed42e3e..067c02a4a5d 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -123,12 +123,17 @@ type PushoverNotifier struct { log log.Logger } +func (this *PushoverNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { ruleUrl, err := evalContext.GetRuleUrl() if err != nil { this.log.Error("Failed get rule link", "error", err) return err } + message := evalContext.Rule.Message for idx, evt := range evalContext.EvalMatches { message += fmt.Sprintf("\n%s: %v", evt.Metric, evt.Value) @@ -142,6 +147,9 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { if evalContext.ImagePublicUrl != "" { message += fmt.Sprintf("\nShow graph image", evalContext.ImagePublicUrl) } + if message == "" { + message = "Notification message missing (Set a notification message to replace this text.)" + } q := url.Values{} q.Add("user", this.UserKey) diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index 9f77801d458..7a34d51b493 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -71,6 +71,10 @@ type SensuNotifier struct { log log.Logger } +func (this *SensuNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Sending sensu result") diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index ed1451da419..c5bb9344e30 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -6,6 +6,7 @@ import ( "io" "mime/multipart" "os" + "path/filepath" "time" "github.com/grafana/grafana/pkg/bus" @@ -97,6 +98,10 @@ type SlackNotifier struct { log log.Logger } +func (this *SlackNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) @@ -176,7 +181,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error { if evalContext.ImageOnDiskPath == "" { - evalContext.ImageOnDiskPath = "public/img/mixed_styles.png" + evalContext.ImageOnDiskPath = filepath.Join(setting.HomePath, "public/img/mixed_styles.png") } log.Info("Uploading to slack via file.upload API") headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient) diff --git a/pkg/services/alerting/notifiers/slack_test.go b/pkg/services/alerting/notifiers/slack_test.go index 13f8c7b48b7..dd82973bc95 100644 --- a/pkg/services/alerting/notifiers/slack_test.go +++ b/pkg/services/alerting/notifiers/slack_test.go @@ -78,7 +78,6 @@ func TestSlackNotifier(t *testing.T) { So(slackNotifier.Mention, ShouldEqual, "@carl") So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX") }) - }) }) } diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 200a8594428..605b2742325 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -47,6 +47,10 @@ type TeamsNotifier struct { log log.Logger } +func (this *TeamsNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 7fb029e57c8..62da584d019 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -76,6 +76,10 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) }, nil } +func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + 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) diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index e4ffffc9108..b8455dcfbfd 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -114,6 +114,10 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { }, nil } +func (this *ThreemaNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error { notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID) notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID) diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index 4b4db553cde..a2b770dfd02 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -68,6 +68,10 @@ type VictoropsNotifier struct { log log.Logger } +func (this *VictoropsNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + // Notify sends notification to Victorops via POST to URL endpoint func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 4c97ed2b75e..d2d6ec636b7 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -65,6 +65,10 @@ type WebhookNotifier struct { log log.Logger } +func (this *WebhookNotifier) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Sending webhook") diff --git a/pkg/services/alerting/notifiers/webhook_test.go b/pkg/services/alerting/notifiers/webhook_test.go index eb25130e4e1..b2d944eb6e9 100644 --- a/pkg/services/alerting/notifiers/webhook_test.go +++ b/pkg/services/alerting/notifiers/webhook_test.go @@ -18,7 +18,7 @@ func TestWebhookNotifier(t *testing.T) { settingsJSON, _ := simplejson.NewJson([]byte(json)) model := &m.AlertNotification{ Name: "ops", - Type: "email", + Type: "webhook", Settings: settingsJSON, } @@ -35,7 +35,7 @@ func TestWebhookNotifier(t *testing.T) { settingsJSON, _ := simplejson.NewJson([]byte(json)) model := &m.AlertNotification{ Name: "ops", - Type: "email", + Type: "webhook", Settings: settingsJSON, } @@ -44,7 +44,7 @@ func TestWebhookNotifier(t *testing.T) { So(err, ShouldBeNil) So(webhookNotifier.Name, ShouldEqual, "ops") - So(webhookNotifier.Type, ShouldEqual, "email") + So(webhookNotifier.Type, ShouldEqual, "webhook") So(webhookNotifier.Url, ShouldEqual, "http://google.com") }) }) diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 448b4ace5bb..8f9deb758a6 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -85,11 +85,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { if err := annotationRepo.Save(&item); err != nil { handler.log.Error("Failed to save annotation for new alert state", "error", err) } - - if evalContext.ShouldSendNotification() { - handler.notifier.Send(evalContext) - } } + handler.notifier.SendIfNeeded(evalContext) + return nil } diff --git a/pkg/services/alerting/test-data/graphite-alert.json b/pkg/services/alerting/test-data/graphite-alert.json new file mode 100644 index 00000000000..5f23e224f9a --- /dev/null +++ b/pkg/services/alerting/test-data/graphite-alert.json @@ -0,0 +1,63 @@ +{ + "id": 57, + "title": "Graphite 4", + "originalTitle": "Graphite 4", + "tags": ["graphite"], + "rows": [ + { + "panels": [ + { + "title": "Active desktop users", + "editable": true, + "type": "graph", + "id": 3, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)" + } + ], + "datasource": null, + "alert": { + "name": "name1", + "message": "desc1", + "handler": 1, + "frequency": "60s", + "conditions": [ + { + "type": "query", + "query": {"params": ["A", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] + } + }, + { + "title": "Active mobile users", + "id": 4, + "targets": [ + {"refId": "A", "target": ""}, + {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} + ], + "datasource": "graphite2", + "alert": { + "name": "name2", + "message": "desc2", + "handler": 0, + "frequency": "60s", + "severity": "warning", + "conditions": [ + { + "type": "query", + "query": {"params": ["B", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] + } + } + ] + } + ] + } \ No newline at end of file diff --git a/pkg/services/alerting/test-data/influxdb-alert.json b/pkg/services/alerting/test-data/influxdb-alert.json new file mode 100644 index 00000000000..79ca355c5a1 --- /dev/null +++ b/pkg/services/alerting/test-data/influxdb-alert.json @@ -0,0 +1,282 @@ +{ + "id": 4, + "title": "Influxdb", + "tags": [ + "apa" + ], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": "450px", + "panels": [ + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 10 + ], + "type": "gt" + }, + "query": { + "params": [ + "B", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "frequency": "3s", + "handler": 1, + "name": "Influxdb", + "noDataState": "no_data", + "notifications": [ + { + "id": 6 + } + ] + }, + "alerting": {}, + "aliasColors": { + "logins.count.count": "#890F02" + }, + "bars": false, + "datasource": "InfluxDB", + "editable": true, + "error": false, + "fill": 1, + "grid": {}, + "id": 1, + "interval": ">10s", + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "groupBy": [ + { + "params": [ + "$interval" + ], + "type": "time" + }, + { + "params": [ + "datacenter" + ], + "type": "tag" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "hide": false, + "measurement": "logins.count", + "policy": "default", + "query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)", + "rawQuery": true, + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "count" + } + ] + ], + "tags": [] + }, + { + "groupBy": [ + { + "params": [ + "$interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "hide": true, + "measurement": "cpu", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ], + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 10 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel Title", + "tooltip": { + "msResolution": false, + "ordering": "alphabetical", + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "editable": true, + "error": false, + "id": 2, + "isNew": true, + "limit": 10, + "links": [], + "show": "current", + "span": 2, + "stateFilter": [ + "alerting" + ], + "title": "Alert status", + "type": "alertlist" + } + ], + "title": "Row" + } + ], + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": { + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "schemaVersion": 13, + "version": 120, + "links": [], + "gnetId": null + } \ No newline at end of file diff --git a/pkg/services/alerting/test-data/panels-missing-id.json b/pkg/services/alerting/test-data/panels-missing-id.json new file mode 100644 index 00000000000..dad96a18dc1 --- /dev/null +++ b/pkg/services/alerting/test-data/panels-missing-id.json @@ -0,0 +1,62 @@ +{ + "id": 57, + "title": "Graphite 4", + "originalTitle": "Graphite 4", + "tags": ["graphite"], + "rows": [ + { + "panels": [ + { + "title": "Active desktop users", + "editable": true, + "type": "graph", + "targets": [ + { + "refId": "A", + "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)" + } + ], + "datasource": null, + "alert": { + "name": "name1", + "message": "desc1", + "handler": 1, + "frequency": "60s", + "conditions": [ + { + "type": "query", + "query": {"params": ["A", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] + } + }, + { + "title": "Active mobile users", + "id": 4, + "targets": [ + {"refId": "A", "target": ""}, + {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} + ], + "datasource": "graphite2", + "alert": { + "name": "name2", + "message": "desc2", + "handler": 0, + "frequency": "60s", + "severity": "warning", + "conditions": [ + { + "type": "query", + "query": {"params": ["B", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] + } + } + ] + } + ] + } \ No newline at end of file diff --git a/pkg/services/alerting/test-data/v5-dashboard.json b/pkg/services/alerting/test-data/v5-dashboard.json new file mode 100644 index 00000000000..da7bbd8d048 --- /dev/null +++ b/pkg/services/alerting/test-data/v5-dashboard.json @@ -0,0 +1,60 @@ +{ + "id": 57, + "title": "Graphite 4", + "originalTitle": "Graphite 4", + "tags": ["graphite"], + "panels": [ + { + "title": "Active desktop users", + "editable": true, + "type": "graph", + "id": 3, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)" + } + ], + "datasource": null, + "alert": { + "name": "name1", + "message": "desc1", + "handler": 1, + "frequency": "60s", + "conditions": [ + { + "type": "query", + "query": {"params": ["A", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] + } + }, + { + "title": "Active mobile users", + "id": 4, + "targets": [ + {"refId": "A", "target": ""}, + {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} + ], + "datasource": "graphite2", + "alert": { + "name": "name2", + "message": "desc2", + "handler": 0, + "frequency": "60s", + "severity": "warning", + "conditions": [ + { + "type": "query", + "query": {"params": ["B", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] + } + + } + ] + } \ No newline at end of file diff --git a/pkg/services/dashboards/dashboards.go b/pkg/services/dashboards/dashboards.go new file mode 100644 index 00000000000..4bdba59b18e --- /dev/null +++ b/pkg/services/dashboards/dashboards.go @@ -0,0 +1,82 @@ +package dashboards + +import ( + "time" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" +) + +type Repository interface { + SaveDashboard(*SaveDashboardItem) (*models.Dashboard, error) +} + +var repositoryInstance Repository + +func GetRepository() Repository { + return repositoryInstance +} + +func SetRepository(rep Repository) { + repositoryInstance = rep +} + +type SaveDashboardItem struct { + OrgId int64 + UpdatedAt time.Time + UserId int64 + Message string + Overwrite bool + Dashboard *models.Dashboard +} + +type DashboardRepository struct{} + +func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.Dashboard, error) { + dashboard := json.Dashboard + + if dashboard.Title == "" { + return nil, models.ErrDashboardTitleEmpty + } + + validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{ + OrgId: json.OrgId, + Dashboard: dashboard, + } + + if err := bus.Dispatch(&validateAlertsCmd); err != nil { + return nil, models.ErrDashboardContainsInvalidAlertData + } + + cmd := models.SaveDashboardCommand{ + Dashboard: dashboard.Data, + Message: json.Message, + OrgId: json.OrgId, + Overwrite: json.Overwrite, + UserId: json.UserId, + FolderId: dashboard.FolderId, + IsFolder: dashboard.IsFolder, + } + + if !json.UpdatedAt.IsZero() { + cmd.UpdatedAt = json.UpdatedAt + } + + err := bus.Dispatch(&cmd) + if err != nil { + return nil, err + } + + alertCmd := alerting.UpdateDashboardAlertsCommand{ + OrgId: json.OrgId, + UserId: json.UserId, + Dashboard: cmd.Result, + } + + if err := bus.Dispatch(&alertCmd); err != nil { + return nil, models.ErrDashboardFailedToUpdateAlertData + } + + return cmd.Result, nil +} diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go new file mode 100644 index 00000000000..1b664c11385 --- /dev/null +++ b/pkg/services/guardian/guardian.go @@ -0,0 +1,130 @@ +package guardian + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +type DashboardGuardian struct { + user *m.SignedInUser + dashId int64 + orgId int64 + acl []*m.DashboardAclInfoDTO + groups []*m.Team + log log.Logger +} + +func NewDashboardGuardian(dashId int64, orgId int64, user *m.SignedInUser) *DashboardGuardian { + return &DashboardGuardian{ + user: user, + dashId: dashId, + orgId: orgId, + log: log.New("guardians.dashboard"), + } +} + +func (g *DashboardGuardian) CanSave() (bool, error) { + return g.HasPermission(m.PERMISSION_EDIT) +} + +func (g *DashboardGuardian) CanEdit() (bool, error) { + if setting.ViewersCanEdit { + return g.HasPermission(m.PERMISSION_VIEW) + } + + return g.HasPermission(m.PERMISSION_EDIT) +} + +func (g *DashboardGuardian) CanView() (bool, error) { + return g.HasPermission(m.PERMISSION_VIEW) +} + +func (g *DashboardGuardian) CanAdmin() (bool, error) { + return g.HasPermission(m.PERMISSION_ADMIN) +} + +func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) { + if g.user.OrgRole == m.ROLE_ADMIN { + return true, nil + } + + acl, err := g.GetAcl() + if err != nil { + return false, err + } + + orgRole := g.user.OrgRole + teamAclItems := []*m.DashboardAclInfoDTO{} + + for _, p := range acl { + // user match + if !g.user.IsAnonymous { + if p.UserId == g.user.UserId && p.Permission >= permission { + return true, nil + } + } + + // role match + if p.Role != nil { + if *p.Role == orgRole && p.Permission >= permission { + return true, nil + } + } + + // remember this rule for later + if p.TeamId > 0 { + teamAclItems = append(teamAclItems, p) + } + } + + // do we have group rules? + if len(teamAclItems) == 0 { + return false, nil + } + + // load groups + teams, err := g.getTeams() + if err != nil { + return false, err + } + + // evalute group rules + for _, p := range acl { + for _, ug := range teams { + if ug.Id == p.TeamId && p.Permission >= permission { + return true, nil + } + } + } + + return false, nil +} + +// Returns dashboard acl +func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) { + if g.acl != nil { + return g.acl, nil + } + + query := m.GetDashboardAclInfoListQuery{DashboardId: g.dashId, OrgId: g.orgId} + if err := bus.Dispatch(&query); err != nil { + return nil, err + } + + g.acl = query.Result + return g.acl, nil +} + +func (g *DashboardGuardian) getTeams() ([]*m.Team, error) { + if g.groups != nil { + return g.groups, nil + } + + query := m.GetTeamsByUserQuery{UserId: g.user.UserId} + err := bus.Dispatch(&query) + + g.groups = query.Result + return query.Result, err +} diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go new file mode 100644 index 00000000000..a602ca71df3 --- /dev/null +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -0,0 +1,49 @@ +package dashboards + +import ( + "io/ioutil" + "path/filepath" + "strings" + + yaml "gopkg.in/yaml.v2" +) + +type configReader struct { + path string +} + +func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { + files, err := ioutil.ReadDir(cr.path) + if err != nil { + return nil, err + } + + var dashboards []*DashboardsAsConfig + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml") { + continue + } + + filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name())) + yamlFile, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var datasource []*DashboardsAsConfig + err = yaml.Unmarshal(yamlFile, &datasource) + if err != nil { + return nil, err + } + + dashboards = append(dashboards, datasource...) + } + + for i := range dashboards { + if dashboards[i].OrgId == 0 { + dashboards[i].OrgId = 1 + } + } + + return dashboards, nil +} diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go new file mode 100644 index 00000000000..56c5a5fcf3d --- /dev/null +++ b/pkg/services/provisioning/dashboards/config_reader_test.go @@ -0,0 +1,62 @@ +package dashboards + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +var ( + simpleDashboardConfig string = "./test-configs/dashboards-from-disk" + brokenConfigs string = "./test-configs/broken-configs" +) + +func TestDashboardsAsConfig(t *testing.T) { + Convey("Dashboards as configuration", t, func() { + + Convey("Can read config file", func() { + + cfgProvifer := configReader{path: simpleDashboardConfig} + cfg, err := cfgProvifer.readConfig() + if err != nil { + t.Fatalf("readConfig return an error %v", err) + } + + So(len(cfg), ShouldEqual, 2) + + ds := cfg[0] + + So(ds.Name, ShouldEqual, "general dashboards") + So(ds.Type, ShouldEqual, "file") + So(ds.OrgId, ShouldEqual, 2) + So(ds.Folder, ShouldEqual, "developers") + So(ds.Editable, ShouldBeTrue) + + So(len(ds.Options), ShouldEqual, 1) + So(ds.Options["folder"], ShouldEqual, "/var/lib/grafana/dashboards") + + ds2 := cfg[1] + + So(ds2.Name, ShouldEqual, "default") + So(ds2.Type, ShouldEqual, "file") + So(ds2.OrgId, ShouldEqual, 1) + So(ds2.Folder, ShouldEqual, "") + So(ds2.Editable, ShouldBeFalse) + + So(len(ds2.Options), ShouldEqual, 1) + So(ds2.Options["folder"], ShouldEqual, "/var/lib/grafana/dashboards") + }) + + Convey("Should skip broken config files", func() { + + cfgProvifer := configReader{path: brokenConfigs} + cfg, err := cfgProvifer.readConfig() + if err != nil { + t.Fatalf("readConfig return an error %v", err) + } + + So(len(cfg), ShouldEqual, 0) + + }) + }) +} diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go new file mode 100644 index 00000000000..1ee0f78497d --- /dev/null +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -0,0 +1,48 @@ +package dashboards + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/log" +) + +type DashboardProvisioner struct { + cfgReader *configReader + log log.Logger + ctx context.Context +} + +func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) { + d := &DashboardProvisioner{ + cfgReader: &configReader{path: configDirectory}, + log: log.New("provisioning.dashboard"), + ctx: ctx, + } + + err := d.Provision(ctx) + return d, err +} + +func (provider *DashboardProvisioner) Provision(ctx context.Context) error { + cfgs, err := provider.cfgReader.readConfig() + if err != nil { + return err + } + + for _, cfg := range cfgs { + switch cfg.Type { + case "file": + fileReader, err := NewDashboardFileReader(cfg, provider.log.New("type", cfg.Type, "name", cfg.Name)) + if err != nil { + return err + } + + go fileReader.ReadAndListen(ctx) + default: + return fmt.Errorf("type %s is not supported", cfg.Type) + } + } + + return 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 new file mode 100644 index 00000000000..eb3085296fd --- /dev/null +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -0,0 +1,213 @@ +package dashboards + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/grafana/grafana/pkg/services/dashboards" + + "github.com/grafana/grafana/pkg/bus" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" +) + +var ( + checkDiskForChangesInterval time.Duration = time.Second * 3 + + ErrFolderNameMissing error = errors.New("Folder name missing") +) + +type fileReader struct { + Cfg *DashboardsAsConfig + Path string + log log.Logger + dashboardRepo dashboards.Repository + cache *dashboardCache + createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc +} + +func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) { + path, ok := cfg.Options["folder"].(string) + if !ok { + return nil, fmt.Errorf("Failed to load dashboards. folder param is not a string") + } + + if _, err := os.Stat(path); os.IsNotExist(err) { + log.Error("Cannot read directory", "error", err) + } + + return &fileReader{ + Cfg: cfg, + Path: path, + log: log, + dashboardRepo: dashboards.GetRepository(), + cache: NewDashboardCache(), + createWalk: createWalkFn, + }, nil +} + +func (fr *fileReader) ReadAndListen(ctx context.Context) error { + ticker := time.NewTicker(checkDiskForChangesInterval) + + if err := fr.startWalkingDisk(); err != nil { + fr.log.Error("failed to search for dashboards", "error", err) + } + + running := false + + for { + select { + case <-ticker.C: + if !running { // avoid walking the filesystem in parallel. incase fs is very slow. + running = true + go func() { + if err := fr.startWalkingDisk(); err != nil { + fr.log.Error("failed to search for dashboards", "error", err) + } + running = false + }() + } + case <-ctx.Done(): + return nil + } + } +} + +func (fr *fileReader) startWalkingDisk() error { + if _, err := os.Stat(fr.Path); err != nil { + if os.IsNotExist(err) { + return err + } + } + + 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 + } + if fileInfo.IsDir() { + if strings.HasPrefix(fileInfo.Name(), ".") { + return filepath.SkipDir + } + return nil + } + + if !strings.HasSuffix(fileInfo.Name(), ".json") { + return nil + } + + cachedDashboard, exist := fr.cache.getCache(path) + if exist && cachedDashboard.UpdatedAt == fileInfo.ModTime() { + return nil + } + + dash, err := fr.readDashboardFromFile(path, folderId) + if err != nil { + fr.log.Error("failed to load dashboard from ", "file", path, "error", err) + return nil + } + + // id = 0 indicates ID validation should be avoided before writing to the db. + dash.Dashboard.Id = 0 + + cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug} + err = bus.Dispatch(cmd) + + // if we dont have the dashboard in the db, save it! + if err == models.ErrDashboardNotFound { + fr.log.Debug("saving new dashboard", "file", path) + _, err = fr.dashboardRepo.SaveDashboard(dash) + return err + } + + if err != nil { + fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err) + return nil + } + + // break if db version is newer then fil version + if cmd.Result.Updated.Unix() >= fileInfo.ModTime().Unix() { + return nil + } + + fr.log.Debug("loading dashboard from disk into database.", "file", path) + _, err = fr.dashboardRepo.SaveDashboard(dash) + return err + } +} + +func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardItem, error) { + reader, err := os.Open(path) + if err != nil { + return nil, err + } + defer reader.Close() + + data, err := simplejson.NewFromReader(reader) + if err != nil { + return nil, err + } + + stat, err := os.Stat(path) + if err != nil { + return nil, err + } + + dash, err := createDashboardJson(data, stat.ModTime(), fr.Cfg, folderId) + if err != nil { + return nil, err + } + + 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 new file mode 100644 index 00000000000..16e3e1184b8 --- /dev/null +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -0,0 +1,238 @@ +package dashboards + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" + + "github.com/grafana/grafana/pkg/log" + . "github.com/smartystreets/goconvey/convey" +) + +var ( + defaultDashboards string = "./test-dashboards/folder-one" + brokenDashboards string = "./test-dashboards/broken-dashboards" + oneDashboard string = "./test-dashboards/one-dashboard" + + fakeRepo *fakeDashboardRepo +) + +func TestDashboardFileReader(t *testing.T) { + Convey("Dashboard file reader", t, func() { + bus.ClearBusHandlers() + fakeRepo = &fakeDashboardRepo{} + + bus.AddHandler("test", mockGetDashboardQuery) + dashboards.SetRepository(fakeRepo) + logger := log.New("test.logger") + + Convey("Reading dashboards from disk", func() { + + cfg := &DashboardsAsConfig{ + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "", + Options: map[string]interface{}{}, + } + + 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("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": defaultDashboards, + }, + } + + _, 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 +} + +func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*models.Dashboard, error) { + repo.inserted = append(repo.inserted, json) + return json.Dashboard, nil +} + +func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error { + for _, d := range fakeRepo.getDashboard { + if d.Slug == cmd.Slug { + cmd.Result = d + return nil + } + } + + return models.ErrDashboardNotFound +} diff --git a/pkg/services/provisioning/dashboards/test-configs/broken-configs/commented.yaml b/pkg/services/provisioning/dashboards/test-configs/broken-configs/commented.yaml new file mode 100644 index 00000000000..e40612af508 --- /dev/null +++ b/pkg/services/provisioning/dashboards/test-configs/broken-configs/commented.yaml @@ -0,0 +1,6 @@ +# - name: 'default' +# org_id: 1 +# folder: '' +# type: file +# options: +# folder: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml b/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml new file mode 100644 index 00000000000..a7c4a812092 --- /dev/null +++ b/pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml @@ -0,0 +1,12 @@ +- name: 'general dashboards' + org_id: 2 + folder: 'developers' + editable: true + type: file + options: + folder: /var/lib/grafana/dashboards + +- name: 'default' + type: file + options: + folder: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/test-dashboards/broken-dashboards/empty-json.json b/pkg/services/provisioning/dashboards/test-dashboards/broken-dashboards/empty-json.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/services/provisioning/dashboards/test-dashboards/broken-dashboards/invalid.json b/pkg/services/provisioning/dashboards/test-dashboards/broken-dashboards/invalid.json new file mode 100644 index 00000000000..0c5e34c2da7 --- /dev/null +++ b/pkg/services/provisioning/dashboards/test-dashboards/broken-dashboards/invalid.json @@ -0,0 +1,6 @@ +[] +{ + "title": "Grafana", + + } + \ No newline at end of file diff --git a/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json b/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json new file mode 100644 index 00000000000..5b6765a4ed6 --- /dev/null +++ b/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json @@ -0,0 +1,173 @@ +{ + "title": "Grafana", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "rows": [ + { + "title": "New row", + "height": "150px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 1, + "span": 12, + "editable": true, + "type": "text", + "mode": "html", + "content": "
\n \n
", + "style": {}, + "title": "Welcome to" + } + ] + }, + { + "title": "Welcome to Grafana", + "height": "210px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 2, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n \n
\n
\n \n
\n
", + "style": {}, + "title": "Documentation Links" + }, + { + "id": 3, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n
    \n
  • Ctrl+S saves the current dashboard
  • \n
  • Ctrl+F Opens the dashboard finder
  • \n
  • Ctrl+H Hide/show row controls
  • \n
  • Click and drag graph title to move panel
  • \n
  • Hit Escape to exit graph when in fullscreen or edit mode
  • \n
  • Click the colored icon in the legend to change series color
  • \n
  • Ctrl or Shift + Click legend name to hide other series
  • \n
\n
\n
\n", + "style": {}, + "title": "Tips & Shortcuts" + } + ] + }, + { + "title": "test", + "height": "250px", + "editable": true, + "collapse": false, + "panels": [ + { + "id": 4, + "span": 12, + "type": "graph", + "x-axis": true, + "y-axis": true, + "scale": 1, + "y_formats": [ + "short", + "short" + ], + "grid": { + "max": null, + "min": null, + "leftMax": null, + "rightMax": null, + "leftMin": null, + "rightMin": null, + "threshold1": null, + "threshold2": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "resolution": 100, + "lines": true, + "fill": 1, + "linewidth": 2, + "dashes": false, + "dashLength": 10, + "spaceLength": 10, + "points": false, + "pointradius": 5, + "bars": false, + "stack": true, + "spyable": true, + "options": false, + "legend": { + "show": true, + "values": false, + "min": false, + "max": false, + "current": false, + "total": false, + "avg": false + }, + "interactive": true, + "legend_counts": true, + "timezone": "browser", + "percentage": false, + "nullPointMode": "connected", + "steppedLine": false, + "tooltip": { + "value_type": "cumulative", + "query_as_alias": true + }, + "targets": [ + { + "target": "randomWalk('random walk')", + "function": "mean", + "column": "value" + } + ], + "aliasColors": {}, + "aliasYAxis": {}, + "title": "First Graph (click title to edit)", + "datasource": "graphite", + "renderer": "flot", + "annotate": { + "enable": false + } + } + ] + } + ], + "nav": [ + { + "type": "timepicker", + "collapse": false, + "enable": true, + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "now": true + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "templating": { + "list": [] + }, + "version": 5 + } + \ No newline at end of file diff --git a/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json b/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json new file mode 100644 index 00000000000..5b6765a4ed6 --- /dev/null +++ b/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json @@ -0,0 +1,173 @@ +{ + "title": "Grafana", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "rows": [ + { + "title": "New row", + "height": "150px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 1, + "span": 12, + "editable": true, + "type": "text", + "mode": "html", + "content": "
\n \n
", + "style": {}, + "title": "Welcome to" + } + ] + }, + { + "title": "Welcome to Grafana", + "height": "210px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 2, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n \n
\n
\n \n
\n
", + "style": {}, + "title": "Documentation Links" + }, + { + "id": 3, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n
    \n
  • Ctrl+S saves the current dashboard
  • \n
  • Ctrl+F Opens the dashboard finder
  • \n
  • Ctrl+H Hide/show row controls
  • \n
  • Click and drag graph title to move panel
  • \n
  • Hit Escape to exit graph when in fullscreen or edit mode
  • \n
  • Click the colored icon in the legend to change series color
  • \n
  • Ctrl or Shift + Click legend name to hide other series
  • \n
\n
\n
\n", + "style": {}, + "title": "Tips & Shortcuts" + } + ] + }, + { + "title": "test", + "height": "250px", + "editable": true, + "collapse": false, + "panels": [ + { + "id": 4, + "span": 12, + "type": "graph", + "x-axis": true, + "y-axis": true, + "scale": 1, + "y_formats": [ + "short", + "short" + ], + "grid": { + "max": null, + "min": null, + "leftMax": null, + "rightMax": null, + "leftMin": null, + "rightMin": null, + "threshold1": null, + "threshold2": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "resolution": 100, + "lines": true, + "fill": 1, + "linewidth": 2, + "dashes": false, + "dashLength": 10, + "spaceLength": 10, + "points": false, + "pointradius": 5, + "bars": false, + "stack": true, + "spyable": true, + "options": false, + "legend": { + "show": true, + "values": false, + "min": false, + "max": false, + "current": false, + "total": false, + "avg": false + }, + "interactive": true, + "legend_counts": true, + "timezone": "browser", + "percentage": false, + "nullPointMode": "connected", + "steppedLine": false, + "tooltip": { + "value_type": "cumulative", + "query_as_alias": true + }, + "targets": [ + { + "target": "randomWalk('random walk')", + "function": "mean", + "column": "value" + } + ], + "aliasColors": {}, + "aliasYAxis": {}, + "title": "First Graph (click title to edit)", + "datasource": "graphite", + "renderer": "flot", + "annotate": { + "enable": false + } + } + ] + } + ], + "nav": [ + { + "type": "timepicker", + "collapse": false, + "enable": true, + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "now": true + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "templating": { + "list": [] + }, + "version": 5 + } + \ No newline at end of file diff --git a/pkg/services/provisioning/dashboards/test-dashboards/one-dashboard/dashboard1.json b/pkg/services/provisioning/dashboards/test-dashboards/one-dashboard/dashboard1.json new file mode 100644 index 00000000000..5b6765a4ed6 --- /dev/null +++ b/pkg/services/provisioning/dashboards/test-dashboards/one-dashboard/dashboard1.json @@ -0,0 +1,173 @@ +{ + "title": "Grafana", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "rows": [ + { + "title": "New row", + "height": "150px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 1, + "span": 12, + "editable": true, + "type": "text", + "mode": "html", + "content": "
\n \n
", + "style": {}, + "title": "Welcome to" + } + ] + }, + { + "title": "Welcome to Grafana", + "height": "210px", + "collapse": false, + "editable": true, + "panels": [ + { + "id": 2, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n \n
\n
\n \n
\n
", + "style": {}, + "title": "Documentation Links" + }, + { + "id": 3, + "span": 6, + "type": "text", + "mode": "html", + "content": "
\n\n
\n
\n
    \n
  • Ctrl+S saves the current dashboard
  • \n
  • Ctrl+F Opens the dashboard finder
  • \n
  • Ctrl+H Hide/show row controls
  • \n
  • Click and drag graph title to move panel
  • \n
  • Hit Escape to exit graph when in fullscreen or edit mode
  • \n
  • Click the colored icon in the legend to change series color
  • \n
  • Ctrl or Shift + Click legend name to hide other series
  • \n
\n
\n
\n", + "style": {}, + "title": "Tips & Shortcuts" + } + ] + }, + { + "title": "test", + "height": "250px", + "editable": true, + "collapse": false, + "panels": [ + { + "id": 4, + "span": 12, + "type": "graph", + "x-axis": true, + "y-axis": true, + "scale": 1, + "y_formats": [ + "short", + "short" + ], + "grid": { + "max": null, + "min": null, + "leftMax": null, + "rightMax": null, + "leftMin": null, + "rightMin": null, + "threshold1": null, + "threshold2": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "resolution": 100, + "lines": true, + "fill": 1, + "linewidth": 2, + "dashes": false, + "dashLength": 10, + "spaceLength": 10, + "points": false, + "pointradius": 5, + "bars": false, + "stack": true, + "spyable": true, + "options": false, + "legend": { + "show": true, + "values": false, + "min": false, + "max": false, + "current": false, + "total": false, + "avg": false + }, + "interactive": true, + "legend_counts": true, + "timezone": "browser", + "percentage": false, + "nullPointMode": "connected", + "steppedLine": false, + "tooltip": { + "value_type": "cumulative", + "query_as_alias": true + }, + "targets": [ + { + "target": "randomWalk('random walk')", + "function": "mean", + "column": "value" + } + ], + "aliasColors": {}, + "aliasYAxis": {}, + "title": "First Graph (click title to edit)", + "datasource": "graphite", + "renderer": "flot", + "annotate": { + "enable": false + } + } + ] + } + ], + "nav": [ + { + "type": "timepicker", + "collapse": false, + "enable": true, + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "now": true + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "templating": { + "list": [] + }, + "version": 5 + } + \ No newline at end of file diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go new file mode 100644 index 00000000000..46ca3c9246e --- /dev/null +++ b/pkg/services/provisioning/dashboards/types.go @@ -0,0 +1,37 @@ +package dashboards + +import ( + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/dashboards" + + "github.com/grafana/grafana/pkg/models" +) + +type DashboardsAsConfig struct { + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + OrgId int64 `json:"org_id" yaml:"org_id"` + Folder string `json:"folder" yaml:"folder"` + Editable bool `json:"editable" yaml:"editable"` + Options map[string]interface{} `json:"options" yaml:"options"` +} + +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.FolderId = folderId + if !cfg.Editable { + dash.Dashboard.Data.Set("editable", cfg.Editable) + } + + if dash.Dashboard.Title == "" { + return nil, models.ErrDashboardTitleEmpty + } + + return dash, nil +} diff --git a/pkg/services/provisioning/datasources/datasources.go b/pkg/services/provisioning/datasources/datasources.go index 325dbbbd757..ce631c565d4 100644 --- a/pkg/services/provisioning/datasources/datasources.go +++ b/pkg/services/provisioning/datasources/datasources.go @@ -118,13 +118,19 @@ func (configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) { return nil, err } - datasources = append(datasources, datasource) + if datasource != nil { + datasources = append(datasources, datasource) + } } } defaultCount := 0 - for _, cfg := range datasources { - for _, ds := range cfg.Datasources { + for i := range datasources { + if datasources[i].Datasources == nil { + continue + } + + for _, ds := range datasources[i].Datasources { if ds.OrgId == 0 { ds.OrgId = 1 } @@ -137,7 +143,7 @@ func (configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) { } } - for _, ds := range cfg.DeleteDatasources { + for _, ds := range datasources[i].DeleteDatasources { if ds.OrgId == 0 { ds.OrgId = 1 } diff --git a/pkg/services/provisioning/datasources/test-configs/broken-yaml/commented.yaml b/pkg/services/provisioning/datasources/test-configs/broken-yaml/commented.yaml new file mode 100644 index 00000000000..1bb9cb53b45 --- /dev/null +++ b/pkg/services/provisioning/datasources/test-configs/broken-yaml/commented.yaml @@ -0,0 +1,48 @@ +# # list of datasources that should be deleted from the database +#delete_datasources: +# - name: Graphite +# org_id: 1 + +# # list of datasources to insert/update depending +# # whats available in the datbase +#datasources: +# # name of the datasource. Required +# - name: Graphite +# # datasource type. Required +# type: graphite +# # access mode. direct or proxy. Required +# access: proxy +# # org id. will default to org_id 1 if not specified +# org_id: 1 +# # url +# url: http://localhost:8080 +# # database password, if used +# password: +# # database user, if used +# user: +# # database name, if used +# database: +# # enable/disable basic auth +# basic_auth: +# # basic auth username +# basic_auth_user: +# # basic auth password +# basic_auth_password: +# # enable/disable with credentials headers +# with_credentials: +# # mark as default datasource. Max one per org +# is_default: +# # fields that will be converted to json and stored in json_data +# json_data: +# graphiteVersion: "1.1" +# tlsAuth: true +# tlsAuthWithCACert: true +# # json object of data that will be encrypted. +# secure_json_data: +# tlsCACert: "..." +# tlsClientCert: "..." +# tlsClientKey: "..." +# version: 1 +# # allow users to edit datasources from the UI. +# editable: false + diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 1bea60f03e4..b41ec37b797 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -1,14 +1,35 @@ package provisioning import ( - "github.com/grafana/grafana/pkg/log" + "context" + "path" + "path/filepath" + + "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/datasources" + ini "gopkg.in/ini.v1" ) -var ( - logger log.Logger = log.New("services.provisioning") -) +func Init(ctx context.Context, homePath string, cfg *ini.File) error { + provisioningPath := makeAbsolute(cfg.Section("paths").Key("provisioning").String(), homePath) -func StartUp(datasourcePath string) error { - return datasources.Provision(datasourcePath) + datasourcePath := path.Join(provisioningPath, "datasources") + if err := datasources.Provision(datasourcePath); err != nil { + return err + } + + dashboardPath := path.Join(provisioningPath, "dashboards") + _, err := dashboards.Provision(ctx, dashboardPath) + if err != nil { + return err + } + + return nil +} + +func makeAbsolute(path string, root string) string { + if filepath.IsAbs(path) { + return path + } + return filepath.Join(root, path) } diff --git a/pkg/services/search/handlers.go b/pkg/services/search/handlers.go index a4905d6fa58..247585402ef 100644 --- a/pkg/services/search/handlers.go +++ b/pkg/services/search/handlers.go @@ -1,77 +1,35 @@ package search import ( - "log" - "path/filepath" "sort" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" ) -var jsonDashIndex *JsonDashIndex - func Init() { bus.AddHandler("search", searchHandler) - - jsonIndexCfg, _ := setting.Cfg.GetSection("dashboards.json") - - if jsonIndexCfg == nil { - log.Fatal("Config section missing: dashboards.json") - return - } - - jsonIndexEnabled := jsonIndexCfg.Key("enabled").MustBool(false) - - if jsonIndexEnabled { - jsonFilesPath := jsonIndexCfg.Key("path").String() - if !filepath.IsAbs(jsonFilesPath) { - jsonFilesPath = filepath.Join(setting.HomePath, jsonFilesPath) - } - - jsonDashIndex = NewJsonDashIndex(jsonFilesPath) - go jsonDashIndex.updateLoop() - } } func searchHandler(query *Query) error { - hits := make(HitList, 0) - dashQuery := FindPersistedDashboardsQuery{ Title: query.Title, - UserId: query.UserId, + SignedInUser: query.SignedInUser, IsStarred: query.IsStarred, - OrgId: query.OrgId, DashboardIds: query.DashboardIds, + Type: query.Type, + FolderIds: query.FolderIds, + Tags: query.Tags, + Limit: query.Limit, } if err := bus.Dispatch(&dashQuery); err != nil { return err } + hits := make(HitList, 0) hits = append(hits, dashQuery.Result...) - if jsonDashIndex != nil { - jsonHits, err := jsonDashIndex.Search(query) - if err != nil { - return err - } - - hits = append(hits, jsonHits...) - } - - // filter out results with tag filter - if len(query.Tags) > 0 { - filtered := HitList{} - for _, hit := range hits { - if hasRequiredTags(query.Tags, hit.Tags) { - filtered = append(filtered, hit) - } - } - hits = filtered - } - // sort main result array sort.Sort(hits) @@ -85,7 +43,7 @@ func searchHandler(query *Query) error { } // add isStarred info - if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil { + if err := setIsStarredFlagOnSearchResults(query.SignedInUser.UserId, hits); err != nil { return err } @@ -93,25 +51,6 @@ func searchHandler(query *Query) error { return nil } -func stringInSlice(a string, list []string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false -} - -func hasRequiredTags(queryTags, hitTags []string) bool { - for _, queryTag := range queryTags { - if !stringInSlice(queryTag, hitTags) { - return false - } - } - - return true -} - func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error { query := m.GetUserStarsQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { @@ -126,10 +65,3 @@ func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error { return nil } - -func GetDashboardFromJsonIndex(filename string) *m.Dashboard { - if jsonDashIndex == nil { - return nil - } - return jsonDashIndex.GetDashboard(filename) -} diff --git a/pkg/services/search/handlers_test.go b/pkg/services/search/handlers_test.go index bb355ec146f..fc223b2ef4b 100644 --- a/pkg/services/search/handlers_test.go +++ b/pkg/services/search/handlers_test.go @@ -11,14 +11,15 @@ import ( func TestSearch(t *testing.T) { Convey("Given search query", t, func() { - jsonDashIndex = NewJsonDashIndex("../../../public/dashboards/") - query := Query{Limit: 2000} + query := Query{Limit: 2000, SignedInUser: &m.SignedInUser{IsGrafanaAdmin: true}} bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error { query.Result = HitList{ - &Hit{Id: 16, Title: "CCAA", Tags: []string{"BB", "AA"}}, - &Hit{Id: 10, Title: "AABB", Tags: []string{"CC", "AA"}}, - &Hit{Id: 15, Title: "BBAA", Tags: []string{"EE", "AA", "BB"}}, + &Hit{Id: 16, Title: "CCAA", Type: "dash-db", Tags: []string{"BB", "AA"}}, + &Hit{Id: 10, Title: "AABB", Type: "dash-db", Tags: []string{"CC", "AA"}}, + &Hit{Id: 15, Title: "BBAA", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}}, + &Hit{Id: 25, Title: "bbAAa", Type: "dash-db", Tags: []string{"EE", "AA", "BB"}}, + &Hit{Id: 17, Title: "FOLDER", Type: "dash-folder"}, } return nil }) @@ -28,34 +29,29 @@ func TestSearch(t *testing.T) { return nil }) + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{IsGrafanaAdmin: true} + return nil + }) + Convey("That is empty", func() { err := searchHandler(&query) So(err, ShouldBeNil) Convey("should return sorted results", func() { - So(query.Result[0].Title, ShouldEqual, "AABB") - So(query.Result[1].Title, ShouldEqual, "BBAA") - So(query.Result[2].Title, ShouldEqual, "CCAA") + So(query.Result[0].Title, ShouldEqual, "FOLDER") + So(query.Result[1].Title, ShouldEqual, "AABB") + So(query.Result[2].Title, ShouldEqual, "BBAA") + So(query.Result[3].Title, ShouldEqual, "bbAAa") + So(query.Result[4].Title, ShouldEqual, "CCAA") }) Convey("should return sorted tags", func() { - So(query.Result[1].Tags[0], ShouldEqual, "AA") - So(query.Result[1].Tags[1], ShouldEqual, "BB") - So(query.Result[1].Tags[2], ShouldEqual, "EE") + So(query.Result[3].Tags[0], ShouldEqual, "AA") + So(query.Result[3].Tags[1], ShouldEqual, "BB") + So(query.Result[3].Tags[2], ShouldEqual, "EE") }) }) - Convey("That filters by tag", func() { - query.Tags = []string{"BB", "AA"} - err := searchHandler(&query) - So(err, ShouldBeNil) - - Convey("should return correct results", func() { - So(len(query.Result), ShouldEqual, 2) - So(query.Result[0].Title, ShouldEqual, "BBAA") - So(query.Result[1].Title, ShouldEqual, "CCAA") - }) - - }) }) } diff --git a/pkg/services/search/json_index.go b/pkg/services/search/json_index.go deleted file mode 100644 index bcda432e343..00000000000 --- a/pkg/services/search/json_index.go +++ /dev/null @@ -1,140 +0,0 @@ -package search - -import ( - "os" - "path/filepath" - "strings" - "time" - - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" - m "github.com/grafana/grafana/pkg/models" -) - -type JsonDashIndex struct { - path string - items []*JsonDashIndexItem -} - -type JsonDashIndexItem struct { - TitleLower string - TagsCsv string - Path string - Dashboard *m.Dashboard -} - -func NewJsonDashIndex(path string) *JsonDashIndex { - log.Info("Creating json dashboard index for path: %v", path) - - index := JsonDashIndex{} - index.path = path - index.updateIndex() - return &index -} - -func (index *JsonDashIndex) updateLoop() { - ticker := time.NewTicker(time.Minute) - for { - select { - case <-ticker.C: - if err := index.updateIndex(); err != nil { - log.Error(3, "Failed to update dashboard json index %v", err) - } - } - } -} - -func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) { - results := make([]*Hit, 0) - - if query.IsStarred { - return results, nil - } - - queryStr := strings.ToLower(query.Title) - - for _, item := range index.items { - if len(results) > query.Limit { - break - } - - // add results with matchig title filter - if strings.Contains(item.TitleLower, queryStr) { - results = append(results, &Hit{ - Type: DashHitJson, - Title: item.Dashboard.Title, - Tags: item.Dashboard.GetTags(), - Uri: "file/" + item.Path, - }) - } - } - - return results, nil -} - -func (index *JsonDashIndex) GetDashboard(path string) *m.Dashboard { - for _, item := range index.items { - if item.Path == path { - return item.Dashboard - } - } - - return nil -} - -func (index *JsonDashIndex) updateIndex() error { - var items = make([]*JsonDashIndexItem, 0) - - visitor := func(path string, f os.FileInfo, err error) error { - if err != nil { - return err - } - if f.IsDir() { - if strings.HasPrefix(f.Name(), ".") { - return filepath.SkipDir - } - return nil - } - - if strings.HasSuffix(f.Name(), ".json") { - dash, err := loadDashboardFromFile(path) - if err != nil { - return err - } - - items = append(items, dash) - } - - return nil - } - - if err := filepath.Walk(index.path, visitor); err != nil { - return err - } - - index.items = items - return nil -} - -func loadDashboardFromFile(filename string) (*JsonDashIndexItem, error) { - reader, err := os.Open(filename) - if err != nil { - return nil, err - } - defer reader.Close() - - data, err := simplejson.NewFromReader(reader) - if err != nil { - return nil, err - } - - stat, _ := os.Stat(filename) - - item := &JsonDashIndexItem{} - item.Dashboard = m.NewDashboardFromJson(data) - item.TitleLower = strings.ToLower(item.Dashboard.Title) - item.TagsCsv = strings.Join(item.Dashboard.GetTags(), ",") - item.Path = stat.Name() - - return item, nil -} diff --git a/pkg/services/search/json_index_test.go b/pkg/services/search/json_index_test.go deleted file mode 100644 index 145e1ac1e99..00000000000 --- a/pkg/services/search/json_index_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package search - -import ( - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestJsonDashIndex(t *testing.T) { - - Convey("Given the json dash index", t, func() { - index := NewJsonDashIndex("../../../public/dashboards/") - - Convey("Should be able to update index", func() { - err := index.updateIndex() - So(err, ShouldBeNil) - }) - - Convey("Should be able to search index", func() { - res, err := index.Search(&Query{Title: "", Limit: 20}) - So(err, ShouldBeNil) - - So(len(res), ShouldEqual, 3) - }) - - Convey("Should be able to search index by title", func() { - res, err := index.Search(&Query{Title: "home", Limit: 20}) - So(err, ShouldBeNil) - - So(len(res), ShouldEqual, 1) - So(res[0].Title, ShouldEqual, "Home") - }) - - Convey("Should not return when starred is filtered", func() { - res, err := index.Search(&Query{Title: "", IsStarred: true}) - So(err, ShouldBeNil) - - So(len(res), ShouldEqual, 0) - }) - - }) -} diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index 159637013f5..cf510ed8462 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -1,37 +1,55 @@ package search +import "strings" +import "github.com/grafana/grafana/pkg/models" + type HitType string const ( - DashHitDB HitType = "dash-db" - DashHitHome HitType = "dash-home" - DashHitJson HitType = "dash-json" - DashHitScripted HitType = "dash-scripted" + DashHitDB HitType = "dash-db" + DashHitHome HitType = "dash-home" + DashHitFolder HitType = "dash-folder" ) type Hit struct { - Id int64 `json:"id"` - Title string `json:"title"` - Uri string `json:"uri"` - Type HitType `json:"type"` - Tags []string `json:"tags"` - IsStarred bool `json:"isStarred"` + Id int64 `json:"id"` + Title string `json:"title"` + Uri string `json:"uri"` + Slug string `json:"slug"` + Type HitType `json:"type"` + Tags []string `json:"tags"` + IsStarred bool `json:"isStarred"` + FolderId int64 `json:"folderId,omitempty"` + FolderTitle string `json:"folderTitle,omitempty"` + FolderSlug string `json:"folderSlug,omitempty"` } type HitList []*Hit -func (s HitList) Len() int { return len(s) } -func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title } +func (s HitList) Len() int { return len(s) } +func (s HitList) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s HitList) Less(i, j int) bool { + if s[i].Type == "dash-folder" && s[j].Type == "dash-db" { + return true + } + + if s[i].Type == "dash-db" && s[j].Type == "dash-folder" { + return false + } + + return strings.ToLower(s[i].Title) < strings.ToLower(s[j].Title) +} type Query struct { Title string Tags []string OrgId int64 - UserId int64 + SignedInUser *models.SignedInUser Limit int IsStarred bool - DashboardIds []int + Type string + DashboardIds []int64 + FolderIds []int64 Result HitList } @@ -39,9 +57,14 @@ type Query struct { type FindPersistedDashboardsQuery struct { Title string OrgId int64 - UserId int64 + SignedInUser *models.SignedInUser IsStarred bool - DashboardIds []int + DashboardIds []int64 + Type string + FolderIds []int64 + Tags []string + Limit int + IsBrowse bool Result HitList } diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index daf4f774717..7b27f5b9ca4 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -12,7 +12,7 @@ func TestAlertingDataAccess(t *testing.T) { Convey("Testing Alerting data access", t, func() { InitTestDB(t) - testDash := insertTestDashboard("dashboard with alerts", 1, "alert") + testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert") items := []*m.Alert{ { @@ -192,7 +192,7 @@ func TestAlertingDataAccess(t *testing.T) { err = DeleteDashboard(&m.DeleteDashboardCommand{ OrgId: 1, - Slug: testDash.Slug, + Id: testDash.Id, }) So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index d91b4a08aa6..0b6b60a5e11 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -1,8 +1,6 @@ package sqlstore import ( - "bytes" - "fmt" "time" "github.com/grafana/grafana/pkg/bus" @@ -70,6 +68,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } } + err = setHasAcl(sess, dash) + if err != nil { + return err + } + parentVersion := dash.Version affectedRows := int64(0) @@ -79,9 +82,14 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { dash.Data.Set("version", dash.Version) affectedRows, err = sess.Insert(dash) } else { - dash.Version += 1 + dash.Version++ dash.Data.Set("version", dash.Version) - affectedRows, err = sess.Id(dash.Id).Update(dash) + + if !cmd.UpdatedAt.IsZero() { + dash.Updated = cmd.UpdatedAt + } + + affectedRows, err = sess.MustCols("folder_id", "has_acl").Id(dash.Id).Update(dash) } if err != nil { @@ -110,7 +118,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { return m.ErrDashboardNotFound } - // delete existing tabs + // delete existing tags _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id) if err != nil { return err @@ -125,13 +133,37 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } } } - cmd.Result = dash return err }) } +func setHasAcl(sess *DBSession, dash *m.Dashboard) error { + // check if parent has acl + if dash.FolderId > 0 { + var parent m.Dashboard + if hasParent, err := sess.Where("folder_id=?", dash.FolderId).Get(&parent); err != nil { + return err + } else if hasParent && parent.HasAcl { + dash.HasAcl = true + } + } + + // check if dash has its own acl + if dash.Id > 0 { + if res, err := sess.Query("SELECT 1 from dashboard_acl WHERE dashboard_id =?", dash.Id); err != nil { + return err + } else { + if len(res) > 0 { + dash.HasAcl = true + } + } + } + + return nil +} + func GetDashboard(query *m.GetDashboardQuery) error { dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id} has, err := x.Get(&dashboard) @@ -148,64 +180,76 @@ func GetDashboard(query *m.GetDashboardQuery) error { } type DashboardSearchProjection struct { - Id int64 - Title string - Slug string - Term string + Id int64 + Title string + Slug string + Term string + IsFolder bool + FolderId int64 + FolderSlug string + FolderTitle string } -func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { - var sql bytes.Buffer - params := make([]interface{}, 0) - - sql.WriteString(`SELECT - dashboard.id, - dashboard.title, - dashboard.slug, - dashboard_tag.term - FROM dashboard - LEFT OUTER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id`) - - if query.IsStarred { - sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id") +func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { + limit := query.Limit + if limit == 0 { + limit = 1000 } - sql.WriteString(` WHERE dashboard.org_id=?`) - - params = append(params, query.OrgId) + sb := NewSearchBuilder(query.SignedInUser, limit). + WithTags(query.Tags). + WithDashboardIdsIn(query.DashboardIds) if query.IsStarred { - sql.WriteString(` AND star.user_id=?`) - params = append(params, query.UserId) - } - - if len(query.DashboardIds) > 0 { - sql.WriteString(" AND (") - for i, dashboardId := range query.DashboardIds { - if i != 0 { - sql.WriteString(" OR") - } - - sql.WriteString(" dashboard.id = ?") - params = append(params, dashboardId) - } - sql.WriteString(")") + sb.IsStarred() } if len(query.Title) > 0 { - sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?") - params = append(params, "%"+query.Title+"%") + sb.WithTitle(query.Title) } - sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000")) + if len(query.Type) > 0 { + sb.WithType(query.Type) + } + + if len(query.FolderIds) > 0 { + sb.WithFolderIds(query.FolderIds) + } var res []DashboardSearchProjection - err := x.Sql(sql.String(), params...).Find(&res) + sql, params := sb.ToSql() + err := x.Sql(sql, params...).Find(&res) + if err != nil { + return nil, err + } + + return res, nil +} + +func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { + res, err := findDashboards(query) if err != nil { return err } + makeQueryResult(query, res) + + return nil +} + +func getHitType(item DashboardSearchProjection) search.HitType { + var hitType search.HitType + if item.IsFolder { + hitType = search.DashHitFolder + } else { + hitType = search.DashHitDB + } + + return hitType +} + +func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []DashboardSearchProjection) { query.Result = make([]*search.Hit, 0) hits := make(map[int64]*search.Hit) @@ -213,11 +257,15 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { hit, exists := hits[item.Id] if !exists { hit = &search.Hit{ - Id: item.Id, - Title: item.Title, - Uri: "db/" + item.Slug, - Type: search.DashHitDB, - Tags: []string{}, + Id: item.Id, + Title: item.Title, + Uri: "db/" + item.Slug, + Slug: item.Slug, + Type: getHitType(item), + FolderId: item.FolderId, + FolderTitle: item.FolderTitle, + FolderSlug: item.FolderSlug, + Tags: []string{}, } query.Result = append(query.Result, hit) hits[item.Id] = hit @@ -226,8 +274,6 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { hit.Tags = append(hit.Tags, item.Term) } } - - return err } func GetDashboardTags(query *m.GetDashboardTagsQuery) error { @@ -247,7 +293,7 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { return inTransaction(func(sess *DBSession) error { - dashboard := m.Dashboard{Slug: cmd.Slug, OrgId: cmd.OrgId} + dashboard := m.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId} has, err := sess.Get(&dashboard) if err != nil { return err @@ -261,6 +307,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { "DELETE FROM dashboard WHERE id = ?", "DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?", "DELETE FROM dashboard_version WHERE dashboard_id = ?", + "DELETE FROM dashboard WHERE folder_id = ?", "DELETE FROM annotation WHERE dashboard_id = ?", } @@ -298,8 +345,9 @@ func GetDashboards(query *m.GetDashboardsQuery) error { func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error { var dashboards = make([]*m.Dashboard, 0) + whereExpr := "org_id=? AND plugin_id=? AND is_folder=" + dialect.BooleanStr(false) - err := x.Where("org_id=? AND plugin_id=?", query.OrgId, query.PluginId).Find(&dashboards) + err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards) query.Result = dashboards if err != nil { diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go new file mode 100644 index 00000000000..3b0c89e02ef --- /dev/null +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -0,0 +1,189 @@ +package sqlstore + +import ( + "fmt" + "time" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", SetDashboardAcl) + bus.AddHandler("sql", UpdateDashboardAcl) + bus.AddHandler("sql", RemoveDashboardAcl) + bus.AddHandler("sql", GetDashboardAclInfoList) +} + +func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error { + return inTransaction(func(sess *DBSession) error { + // delete existing items + _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", cmd.DashboardId) + if err != nil { + return err + } + + for _, item := range cmd.Items { + if item.UserId == 0 && item.TeamId == 0 && !item.Role.IsValid() { + return m.ErrDashboardAclInfoMissing + } + + if item.DashboardId == 0 { + return m.ErrDashboardPermissionDashboardEmpty + } + + sess.Nullable("user_id", "team_id") + if _, err := sess.Insert(item); err != nil { + return err + } + } + + // Update dashboard HasAcl flag + dashboard := m.Dashboard{HasAcl: true} + if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil { + return err + } + return nil + }) +} + +func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { + return inTransaction(func(sess *DBSession) error { + if cmd.UserId == 0 && cmd.TeamId == 0 { + return m.ErrDashboardAclInfoMissing + } + + if cmd.DashboardId == 0 { + return m.ErrDashboardPermissionDashboardEmpty + } + + if res, err := sess.Query("SELECT 1 from "+dialect.Quote("dashboard_acl")+" WHERE dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId); err != nil { + return err + } else if len(res) == 1 { + + entity := m.DashboardAcl{ + Permission: cmd.Permission, + Updated: time.Now(), + } + + if _, err := sess.Cols("updated", "permission").Where("dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId).Update(&entity); err != nil { + return err + } + + return nil + } + + entity := m.DashboardAcl{ + OrgId: cmd.OrgId, + TeamId: cmd.TeamId, + UserId: cmd.UserId, + Created: time.Now(), + Updated: time.Now(), + DashboardId: cmd.DashboardId, + Permission: cmd.Permission, + } + + cols := []string{"org_id", "created", "updated", "dashboard_id", "permission"} + + if cmd.UserId != 0 { + cols = append(cols, "user_id") + } + + if cmd.TeamId != 0 { + cols = append(cols, "team_id") + } + + _, err := sess.Cols(cols...).Insert(&entity) + if err != nil { + return err + } + + cmd.Result = entity + + // Update dashboard HasAcl flag + dashboard := m.Dashboard{ + HasAcl: true, + } + + if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil { + return err + } + + return nil + }) +} + +func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error { + return inTransaction(func(sess *DBSession) error { + var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE org_id =? and id=?" + _, err := sess.Exec(rawSQL, cmd.OrgId, cmd.AclId) + if err != nil { + return err + } + + return err + }) +} + +func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { + dashboardFilter := fmt.Sprintf(`IN ( + SELECT %d + UNION + SELECT folder_id from dashboard where id = %d + )`, query.DashboardId, query.DashboardId) + + rawSQL := ` + SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + u.login AS user_login, + u.email AS user_email, + ug.name AS team + FROM` + dialect.Quote("dashboard_acl") + ` as da + LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id + LEFT OUTER JOIN team ug on ug.id = da.team_id + WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ? + + -- Also include default permission if has_acl = 0 + + UNION + SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + '' as user_login, + '' as user_email, + '' as team + FROM dashboard_acl as da, + dashboard as dash + LEFT JOIN dashboard folder on dash.folder_id = folder.id + WHERE + dash.id = ? AND ( + dash.has_acl = ` + dialect.BooleanStr(false) + ` or + folder.has_acl = ` + dialect.BooleanStr(false) + ` + ) AND + da.dashboard_id = -1 + ` + + query.Result = make([]*m.DashboardAclInfoDTO, 0) + err := x.SQL(rawSQL, query.OrgId, query.DashboardId).Find(&query.Result) + + for _, p := range query.Result { + p.PermissionName = p.Permission.String() + } + + return err +} diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/sqlstore/dashboard_acl_test.go new file mode 100644 index 00000000000..bb6363883d6 --- /dev/null +++ b/pkg/services/sqlstore/dashboard_acl_test.go @@ -0,0 +1,236 @@ +package sqlstore + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + + m "github.com/grafana/grafana/pkg/models" +) + +func TestDashboardAclDataAccess(t *testing.T) { + Convey("Testing DB", t, func() { + InitTestDB(t) + Convey("Given a dashboard folder and a user", func() { + currentUser := createUser("viewer", "Viewer", false) + savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp") + childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp") + + Convey("When adding dashboard permission with userId and teamId set to 0", func() { + err := SetDashboardAcl(&m.SetDashboardAclCommand{ + OrgId: 1, + DashboardId: savedFolder.Id, + Permission: m.PERMISSION_EDIT, + }) + So(err, ShouldEqual, m.ErrDashboardAclInfoMissing) + }) + + Convey("Given dashboard folder with default permissions", func() { + Convey("When reading dashboard acl should include acl for parent folder", func() { + query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1} + + err := GetDashboardAclInfoList(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 2) + defaultPermissionsId := -1 + So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId) + So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER) + So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId) + So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR) + }) + }) + + Convey("Given dashboard folder permission", func() { + err := SetDashboardAcl(&m.SetDashboardAclCommand{ + OrgId: 1, + UserId: currentUser.Id, + DashboardId: savedFolder.Id, + Permission: m.PERMISSION_EDIT, + }) + So(err, ShouldBeNil) + + Convey("When reading dashboard acl should include acl for parent folder", func() { + query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1} + + err := GetDashboardAclInfoList(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id) + }) + + Convey("Given child dashboard permission", func() { + err := SetDashboardAcl(&m.SetDashboardAclCommand{ + OrgId: 1, + UserId: currentUser.Id, + DashboardId: childDash.Id, + Permission: m.PERMISSION_EDIT, + }) + So(err, ShouldBeNil) + + Convey("When reading dashboard acl should include acl for parent folder and child", func() { + query := m.GetDashboardAclInfoListQuery{OrgId: 1, DashboardId: childDash.Id} + + err := GetDashboardAclInfoList(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id) + So(query.Result[1].DashboardId, ShouldEqual, childDash.Id) + }) + }) + }) + + Convey("Given child dashboard permission in folder with no permissions", func() { + err := SetDashboardAcl(&m.SetDashboardAclCommand{ + OrgId: 1, + UserId: currentUser.Id, + DashboardId: childDash.Id, + Permission: m.PERMISSION_EDIT, + }) + So(err, ShouldBeNil) + + Convey("When reading dashboard acl should include default acl for parent folder and the child acl", func() { + query := m.GetDashboardAclInfoListQuery{OrgId: 1, DashboardId: childDash.Id} + + err := GetDashboardAclInfoList(&query) + So(err, ShouldBeNil) + + defaultPermissionsId := -1 + So(len(query.Result), ShouldEqual, 3) + So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId) + So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER) + So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId) + So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR) + So(query.Result[2].DashboardId, ShouldEqual, childDash.Id) + }) + }) + + Convey("Should be able to add dashboard permission", func() { + setDashAclCmd := m.SetDashboardAclCommand{ + OrgId: 1, + UserId: currentUser.Id, + DashboardId: savedFolder.Id, + Permission: m.PERMISSION_EDIT, + } + + err := SetDashboardAcl(&setDashAclCmd) + So(err, ShouldBeNil) + + So(setDashAclCmd.Result.Id, ShouldEqual, 3) + + q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} + err = GetDashboardAclInfoList(q1) + So(err, ShouldBeNil) + + So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id) + So(q1.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT) + So(q1.Result[0].PermissionName, ShouldEqual, "Edit") + So(q1.Result[0].UserId, ShouldEqual, currentUser.Id) + So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login) + So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email) + So(q1.Result[0].Id, ShouldEqual, setDashAclCmd.Result.Id) + + Convey("Should update hasAcl field to true for dashboard folder and its children", func() { + q2 := &m.GetDashboardsQuery{DashboardIds: []int64{savedFolder.Id, childDash.Id}} + err := GetDashboards(q2) + So(err, ShouldBeNil) + So(q2.Result[0].HasAcl, ShouldBeTrue) + So(q2.Result[1].HasAcl, ShouldBeTrue) + }) + + Convey("Should be able to update an existing permission", func() { + err := SetDashboardAcl(&m.SetDashboardAclCommand{ + OrgId: 1, + UserId: 1, + DashboardId: savedFolder.Id, + Permission: m.PERMISSION_ADMIN, + }) + + So(err, ShouldBeNil) + + q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} + err = GetDashboardAclInfoList(q3) + So(err, ShouldBeNil) + So(len(q3.Result), ShouldEqual, 1) + So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id) + So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN) + So(q3.Result[0].UserId, ShouldEqual, 1) + + }) + + Convey("Should be able to delete an existing permission", func() { + err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{ + OrgId: 1, + AclId: setDashAclCmd.Result.Id, + }) + + So(err, ShouldBeNil) + + q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} + err = GetDashboardAclInfoList(q3) + So(err, ShouldBeNil) + So(len(q3.Result), ShouldEqual, 0) + }) + }) + + Convey("Given a team", func() { + group1 := m.CreateTeamCommand{Name: "group1 name", OrgId: 1} + err := CreateTeam(&group1) + So(err, ShouldBeNil) + + Convey("Should be able to add a user permission for a team", func() { + setDashAclCmd := m.SetDashboardAclCommand{ + OrgId: 1, + TeamId: group1.Result.Id, + DashboardId: savedFolder.Id, + Permission: m.PERMISSION_EDIT, + } + + err := SetDashboardAcl(&setDashAclCmd) + So(err, ShouldBeNil) + + q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} + err = GetDashboardAclInfoList(q1) + So(err, ShouldBeNil) + So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id) + So(q1.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT) + So(q1.Result[0].TeamId, ShouldEqual, group1.Result.Id) + + Convey("Should be able to delete an existing permission for a team", func() { + err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{ + OrgId: 1, + AclId: setDashAclCmd.Result.Id, + }) + + So(err, ShouldBeNil) + q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} + err = GetDashboardAclInfoList(q3) + So(err, ShouldBeNil) + So(len(q3.Result), ShouldEqual, 0) + }) + }) + + Convey("Should be able to update an existing permission for a team", func() { + err := SetDashboardAcl(&m.SetDashboardAclCommand{ + OrgId: 1, + TeamId: group1.Result.Id, + DashboardId: savedFolder.Id, + Permission: m.PERMISSION_ADMIN, + }) + So(err, ShouldBeNil) + + q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} + err = GetDashboardAclInfoList(q3) + So(err, ShouldBeNil) + So(len(q3.Result), ShouldEqual, 1) + So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id) + So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN) + So(q3.Result[0].TeamId, ShouldEqual, group1.Result.Id) + }) + + }) + }) + }) +} diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index a055500592b..a552bd0546a 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -3,44 +3,39 @@ package sqlstore import ( "testing" + "github.com/go-xorm/xorm" . "github.com/smartystreets/goconvey/convey" - "github.com/gosimple/slug" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/setting" ) -func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard { - cmd := m.SaveDashboardCommand{ - OrgId: orgId, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": nil, - "title": title, - "tags": tags, - }), - } - - err := SaveDashboard(&cmd) - So(err, ShouldBeNil) - - return cmd.Result -} - func TestDashboardDataAccess(t *testing.T) { + var x *xorm.Engine Convey("Testing DB", t, func() { - InitTestDB(t) + x = InitTestDB(t) Convey("Given saved dashboard", func() { - savedDash := insertTestDashboard("test dash 23", 1, "prod", "webapp") - insertTestDashboard("test dash 45", 1, "prod") - insertTestDashboard("test dash 67", 1, "prod", "webapp") + savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp") + savedDash := insertTestDashboard("test dash 23", 1, savedFolder.Id, false, "prod", "webapp") + insertTestDashboard("test dash 45", 1, savedFolder.Id, false, "prod") + insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp") Convey("Should return dashboard model", func() { So(savedDash.Title, ShouldEqual, "test dash 23") So(savedDash.Slug, ShouldEqual, "test-dash-23") So(savedDash.Id, ShouldNotEqual, 0) + So(savedDash.IsFolder, ShouldBeFalse) + So(savedDash.FolderId, ShouldBeGreaterThan, 0) + + So(savedFolder.Title, ShouldEqual, "1 test dash folder") + So(savedFolder.Slug, ShouldEqual, "1-test-dash-folder") + So(savedFolder.Id, ShouldNotEqual, 0) + So(savedFolder.IsFolder, ShouldBeTrue) + So(savedFolder.FolderId, ShouldEqual, 0) }) Convey("Should be able to get dashboard", func() { @@ -54,15 +49,14 @@ func TestDashboardDataAccess(t *testing.T) { So(query.Result.Title, ShouldEqual, "test dash 23") So(query.Result.Slug, ShouldEqual, "test-dash-23") + So(query.Result.IsFolder, ShouldBeFalse) }) Convey("Should be able to delete dashboard", func() { - insertTestDashboard("delete me", 1, "delete this") - - dashboardSlug := slug.Make("delete me") + dash := insertTestDashboard("delete me", 1, 0, false, "delete this") err := DeleteDashboard(&m.DeleteDashboardCommand{ - Slug: dashboardSlug, + Id: dash.Id, OrgId: 1, }) @@ -102,10 +96,11 @@ func TestDashboardDataAccess(t *testing.T) { So(err, ShouldNotBeNil) }) - Convey("Should be able to search for dashboard", func() { + Convey("Should be able to search for dashboard folder", func() { query := search.FindPersistedDashboardsQuery{ - Title: "test dash 23", - OrgId: 1, + Title: "1 test dash folder", + OrgId: 1, + SignedInUser: &m.SignedInUser{OrgId: 1}, } err := SearchDashboards(&query) @@ -113,14 +108,29 @@ func TestDashboardDataAccess(t *testing.T) { So(len(query.Result), ShouldEqual, 1) hit := query.Result[0] - So(len(hit.Tags), ShouldEqual, 2) + So(hit.Type, ShouldEqual, search.DashHitFolder) + }) + + Convey("Should be able to search for a dashboard folder's children", func() { + query := search.FindPersistedDashboardsQuery{ + OrgId: 1, + FolderIds: []int64{savedFolder.Id}, + SignedInUser: &m.SignedInUser{OrgId: 1}, + } + + err := SearchDashboards(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 2) + hit := query.Result[0] + So(hit.Id, ShouldEqual, savedDash.Id) }) Convey("Should be able to search for dashboard by dashboard ids", func() { Convey("should be able to find two dashboards by id", func() { query := search.FindPersistedDashboardsQuery{ - DashboardIds: []int{1, 2}, - OrgId: 1, + DashboardIds: []int64{2, 3}, + SignedInUser: &m.SignedInUser{OrgId: 1}, } err := SearchDashboards(&query) @@ -137,8 +147,8 @@ func TestDashboardDataAccess(t *testing.T) { Convey("DashboardIds that does not exists should not cause errors", func() { query := search.FindPersistedDashboardsQuery{ - DashboardIds: []int{1000}, - OrgId: 1, + DashboardIds: []int64{1000}, + SignedInUser: &m.SignedInUser{OrgId: 1}, } err := SearchDashboards(&query) @@ -161,6 +171,63 @@ func TestDashboardDataAccess(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("Should be able to update dashboard and remove folderId", func() { + cmd := m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": 1, + "title": "folderId", + "tags": []interface{}{}, + }), + Overwrite: true, + FolderId: 2, + } + + err := SaveDashboard(&cmd) + So(err, ShouldBeNil) + So(cmd.Result.FolderId, ShouldEqual, 2) + + cmd = m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": 1, + "title": "folderId", + "tags": []interface{}{}, + }), + FolderId: 0, + Overwrite: true, + } + + err = SaveDashboard(&cmd) + So(err, ShouldBeNil) + + query := m.GetDashboardQuery{ + Slug: cmd.Result.Slug, + OrgId: 1, + } + + err = GetDashboard(&query) + So(err, ShouldBeNil) + So(query.Result.FolderId, ShouldEqual, 0) + }) + + Convey("Should be able to delete a dashboard folder and its children", func() { + deleteCmd := &m.DeleteDashboardCommand{Id: savedFolder.Id} + err := DeleteDashboard(deleteCmd) + So(err, ShouldBeNil) + + query := search.FindPersistedDashboardsQuery{ + OrgId: 1, + FolderIds: []int64{savedFolder.Id}, + SignedInUser: &m.SignedInUser{}, + } + + err = SearchDashboards(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 0) + }) + Convey("Should be able to get dashboard tags", func() { query := m.GetDashboardTagsQuery{OrgId: 1} @@ -171,7 +238,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Given two dashboards, one is starred dashboard by user 10, other starred by user 1", func() { - starredDash := insertTestDashboard("starred dash", 1) + starredDash := insertTestDashboard("starred dash", 1, 0, false) StarDashboard(&m.StarDashboardCommand{ DashboardId: starredDash.Id, UserId: 10, @@ -183,7 +250,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Should be able to search for starred dashboards", func() { - query := search.FindPersistedDashboardsQuery{OrgId: 1, UserId: 10, IsStarred: true} + query := search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: 10, OrgId: 1}, IsStarred: true} err := SearchDashboards(&query) So(err, ShouldBeNil) @@ -192,5 +259,307 @@ func TestDashboardDataAccess(t *testing.T) { }) }) }) + + Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() { + folder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp") + dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp") + childDash := insertTestDashboard("test dash 23", 1, folder.Id, false, "prod", "webapp") + insertTestDashboard("test dash 45", 1, folder.Id, false, "prod") + + currentUser := createUser("viewer", "Viewer", false) + + Convey("and no acls are set", func() { + Convey("should return all dashboards", func() { + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].Id, ShouldEqual, folder.Id) + So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("and acl is set for dashboard folder", func() { + var otherUser int64 = 999 + updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) + + Convey("should not return folder", func() { + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) + }) + + Convey("when the user is given permission", func() { + updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT) + + Convey("should be able to access folder", func() { + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].Id, ShouldEqual, folder.Id) + So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("when the user is an admin", func() { + Convey("should be able to access folder", func() { + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{ + UserId: currentUser.Id, + OrgId: 1, + OrgRole: m.ROLE_ADMIN, + }, + OrgId: 1, + DashboardIds: []int64{folder.Id, dashInRoot.Id}, + } + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].Id, ShouldEqual, folder.Id) + So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) + }) + }) + }) + + Convey("and acl is set for dashboard child and folder has all permissions removed", func() { + var otherUser int64 = 999 + aclId := updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) + removeAcl(aclId) + updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT) + + Convey("should not return folder or child", func() { + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) + }) + + Convey("when the user is given permission to child", func() { + updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT) + + Convey("should be able to search for child dashboard but not folder", func() { + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].Id, ShouldEqual, childDash.Id) + So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("when the user is an admin", func() { + Convey("should be able to search for child dash and folder", func() { + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{ + UserId: currentUser.Id, + OrgId: 1, + OrgRole: m.ROLE_ADMIN, + }, + OrgId: 1, + DashboardIds: []int64{folder.Id, dashInRoot.Id, childDash.Id}, + } + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 3) + So(query.Result[0].Id, ShouldEqual, folder.Id) + So(query.Result[1].Id, ShouldEqual, childDash.Id) + So(query.Result[2].Id, ShouldEqual, dashInRoot.Id) + }) + }) + }) + }) + + Convey("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func() { + folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod") + folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod") + dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod") + childDash1 := insertTestDashboard("child dash 1", 1, folder1.Id, false, "prod") + childDash2 := insertTestDashboard("child dash 2", 1, folder2.Id, false, "prod") + + currentUser := createUser("viewer", "Viewer", false) + var rootFolderId int64 = 0 + + Convey("and one folder is expanded, the other collapsed", func() { + Convey("should return dashboards in root and expanded folder", func() { + query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 4) + So(query.Result[0].Id, ShouldEqual, folder1.Id) + So(query.Result[1].Id, ShouldEqual, folder2.Id) + So(query.Result[2].Id, ShouldEqual, childDash1.Id) + So(query.Result[3].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("and acl is set for one dashboard folder", func() { + var otherUser int64 = 999 + updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT) + + Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() { + movedDash := moveDashboard(1, childDash2.Data, folder1.Id) + So(movedDash.HasAcl, ShouldBeTrue) + + Convey("should not return folder with acl or its children", func() { + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, + OrgId: 1, + DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + } + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() { + movedDash := moveDashboard(1, childDash1.Data, folder2.Id) + So(movedDash.HasAcl, ShouldBeFalse) + + Convey("should return folder without acl and its children", func() { + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, + OrgId: 1, + DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + } + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 4) + So(query.Result[0].Id, ShouldEqual, folder2.Id) + So(query.Result[1].Id, ShouldEqual, childDash1.Id) + So(query.Result[2].Id, ShouldEqual, childDash2.Id) + So(query.Result[3].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("and a dashboard with an acl is moved to the folder without an acl", func() { + updateTestDashboardWithAcl(childDash1.Id, otherUser, m.PERMISSION_EDIT) + movedDash := moveDashboard(1, childDash1.Data, folder2.Id) + So(movedDash.HasAcl, ShouldBeTrue) + + Convey("should return folder without acl but not the dashboard with acl", func() { + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, + OrgId: 1, + DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + } + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 3) + So(query.Result[0].Id, ShouldEqual, folder2.Id) + So(query.Result[1].Id, ShouldEqual, childDash2.Id) + So(query.Result[2].Id, ShouldEqual, dashInRoot.Id) + }) + }) + }) + }) + + Convey("Given a plugin with imported dashboards", func() { + pluginId := "test-app" + + appFolder := insertTestDashboardForPlugin("app-test", 1, 0, true, pluginId) + insertTestDashboardForPlugin("app-dash1", 1, appFolder.Id, false, pluginId) + insertTestDashboardForPlugin("app-dash2", 1, appFolder.Id, false, pluginId) + + Convey("Should return imported dashboard", func() { + query := m.GetDashboardsByPluginIdQuery{ + PluginId: pluginId, + OrgId: 1, + } + + err := GetDashboardsByPluginId(&query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 2) + }) + }) }) } + +func insertTestDashboard(title string, orgId int64, folderId int64, isFolder bool, tags ...interface{}) *m.Dashboard { + cmd := m.SaveDashboardCommand{ + OrgId: orgId, + FolderId: folderId, + IsFolder: isFolder, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + "tags": tags, + }), + } + + err := SaveDashboard(&cmd) + So(err, ShouldBeNil) + + return cmd.Result +} + +func insertTestDashboardForPlugin(title string, orgId int64, folderId int64, isFolder bool, pluginId string) *m.Dashboard { + cmd := m.SaveDashboardCommand{ + OrgId: orgId, + FolderId: folderId, + IsFolder: isFolder, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + }), + PluginId: pluginId, + } + + err := SaveDashboard(&cmd) + So(err, ShouldBeNil) + + return cmd.Result +} + +func createUser(name string, role string, isAdmin bool) m.User { + setting.AutoAssignOrg = true + setting.AutoAssignOrgRole = role + + currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin} + err := CreateUser(¤tUserCmd) + So(err, ShouldBeNil) + + q1 := m.GetUserOrgListQuery{UserId: currentUserCmd.Result.Id} + GetUserOrgList(&q1) + So(q1.Result[0].Role, ShouldEqual, role) + + return currentUserCmd.Result +} + +func updateTestDashboardWithAcl(dashId int64, userId int64, permissions m.PermissionType) int64 { + cmd := &m.SetDashboardAclCommand{ + OrgId: 1, + UserId: userId, + DashboardId: dashId, + Permission: permissions, + } + + err := SetDashboardAcl(cmd) + So(err, ShouldBeNil) + + return cmd.Result.Id +} + +func removeAcl(aclId int64) { + err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{AclId: aclId, OrgId: 1}) + So(err, ShouldBeNil) +} + +func moveDashboard(orgId int64, dashboard *simplejson.Json, newFolderId int64) *m.Dashboard { + cmd := m.SaveDashboardCommand{ + OrgId: orgId, + FolderId: newFolderId, + Dashboard: dashboard, + Overwrite: true, + } + + err := SaveDashboard(&cmd) + So(err, ShouldBeNil) + + return cmd.Result +} diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 484a7e281dc..49c35397094 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -36,6 +36,10 @@ func GetDashboardVersion(query *m.GetDashboardVersionQuery) error { // GetDashboardVersions gets all dashboard versions for the given dashboard ID. func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error { + if query.Limit == 0 { + query.Limit = 1000 + } + err := x.Table("dashboard_version"). Select(`dashboard_version.id, dashboard_version.dashboard_id, diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index a27d4385637..6ed37cd6904 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -29,7 +29,7 @@ func TestGetDashboardVersion(t *testing.T) { InitTestDB(t) Convey("Get a Dashboard ID and version ID", func() { - savedDash := insertTestDashboard("test dash 26", 1, "diff") + savedDash := insertTestDashboard("test dash 26", 1, 0, false, "diff") query := m.GetDashboardVersionQuery{ DashboardId: savedDash.Id, @@ -70,7 +70,7 @@ func TestGetDashboardVersion(t *testing.T) { func TestGetDashboardVersions(t *testing.T) { Convey("Testing dashboard versions retrieval", t, func() { InitTestDB(t) - savedDash := insertTestDashboard("test dash 43", 1, "diff-all") + savedDash := insertTestDashboard("test dash 43", 1, 0, false, "diff-all") Convey("Get all versions for a given Dashboard ID", func() { query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} @@ -110,7 +110,7 @@ func TestDeleteExpiredVersions(t *testing.T) { versionsToWrite := 10 setting.DashboardVersionsToKeep = versionsToKeep - savedDash := insertTestDashboard("test dash 53", 1, "diff-all") + savedDash := insertTestDashboard("test dash 53", 1, 0, false, "diff-all") for i := 0; i < versionsToWrite-1; i++ { updateTestDashboard(savedDash, map[string]interface{}{ "tags": "different-tag", diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index 135867cf0f5..e6f0114ab4d 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" ) -func InitTestDB(t *testing.T) { +func InitTestDB(t *testing.T) *xorm.Engine { x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr) //x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr) //x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr) @@ -27,6 +27,8 @@ func InitTestDB(t *testing.T) { if err := SetEngine(x); err != nil { t.Fatal(err) } + + return x } type Test struct { diff --git a/pkg/services/sqlstore/migrations/dashboard_acl.go b/pkg/services/sqlstore/migrations/dashboard_acl.go new file mode 100644 index 00000000000..cc3b813c12f --- /dev/null +++ b/pkg/services/sqlstore/migrations/dashboard_acl.go @@ -0,0 +1,52 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addDashboardAclMigrations(mg *Migrator) { + dashboardAclV1 := Table{ + Name: "dashboard_acl", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: DB_BigInt}, + {Name: "dashboard_id", Type: DB_BigInt}, + {Name: "user_id", Type: DB_BigInt, Nullable: true}, + {Name: "team_id", Type: DB_BigInt, Nullable: true}, + {Name: "permission", Type: DB_SmallInt, Default: "4"}, + {Name: "role", Type: DB_Varchar, Length: 20, Nullable: true}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + {Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"dashboard_id"}}, + {Cols: []string{"dashboard_id", "user_id"}, Type: UniqueIndex}, + {Cols: []string{"dashboard_id", "team_id"}, Type: UniqueIndex}, + }, + } + + mg.AddMigration("create dashboard acl table", NewAddTableMigration(dashboardAclV1)) + + //------- indexes ------------------ + mg.AddMigration("add index dashboard_acl_dashboard_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[0])) + mg.AddMigration("add unique index dashboard_acl_dashboard_id_user_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[1])) + mg.AddMigration("add unique index dashboard_acl_dashboard_id_team_id", NewAddIndexMigration(dashboardAclV1, dashboardAclV1.Indices[2])) + + const rawSQL = ` +INSERT INTO dashboard_acl + ( + org_id, + dashboard_id, + permission, + role, + created, + updated + ) + VALUES + (-1,-1, 1,'Viewer','2017-06-20','2017-06-20'), + (-1,-1, 2,'Editor','2017-06-20','2017-06-20') + ` + + mg.AddMigration("save default acl rules in dashboard_acl table", new(RawSqlMigration). + Sqlite(rawSQL). + Postgres(rawSQL). + Mysql(rawSQL)) +} diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index ee0cc1d893f..4f1602be931 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -136,4 +136,18 @@ func addDashboardMigration(mg *Migrator) { mg.AddMigration("Update dashboard_tag table charset", NewTableCharsetMigration("dashboard_tag", []*Column{ {Name: "term", Type: DB_NVarchar, Length: 50, Nullable: false}, })) + + // add column to store folder_id for dashboard folder structure + mg.AddMigration("Add column folder_id in dashboard", NewAddColumnMigration(dashboardV2, &Column{ + Name: "folder_id", Type: DB_BigInt, Nullable: false, Default: "0", + })) + + mg.AddMigration("Add column isFolder in dashboard", NewAddColumnMigration(dashboardV2, &Column{ + Name: "is_folder", Type: DB_Bool, Nullable: false, Default: "0", + })) + + // add column to flag if dashboard has an ACL + mg.AddMigration("Add column has_acl in dashboard", NewAddColumnMigration(dashboardV2, &Column{ + Name: "has_acl", Type: DB_Bool, Nullable: false, Default: "0", + })) } diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 4984ff18592..8e9268779ef 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -26,6 +26,8 @@ func AddMigrations(mg *Migrator) { addAnnotationMig(mg) addTestDataMigrations(mg) addDashboardVersionMigration(mg) + addTeamMigrations(mg) + addDashboardAclMigrations(mg) addTagMigration(mg) } diff --git a/pkg/services/sqlstore/migrations/org_mig.go b/pkg/services/sqlstore/migrations/org_mig.go index 12e0a04256a..cf9b19f6f5b 100644 --- a/pkg/services/sqlstore/migrations/org_mig.go +++ b/pkg/services/sqlstore/migrations/org_mig.go @@ -83,4 +83,10 @@ func addOrgMigrations(mg *Migrator) { mg.AddMigration("Update org_user table charset", NewTableCharsetMigration("org_user", []*Column{ {Name: "role", Type: DB_NVarchar, Length: 20}, })) + + const migrateReadOnlyViewersToViewers = `UPDATE org_user SET role = 'Viewer' WHERE role = 'Read Only Editor'` + mg.AddMigration("Migrate all Read Only Viewers to Viewers", new(RawSqlMigration). + Sqlite(migrateReadOnlyViewersToViewers). + Postgres(migrateReadOnlyViewersToViewers). + Mysql(migrateReadOnlyViewersToViewers)) } diff --git a/pkg/services/sqlstore/migrations/team_mig.go b/pkg/services/sqlstore/migrations/team_mig.go new file mode 100644 index 00000000000..eb0641fbc32 --- /dev/null +++ b/pkg/services/sqlstore/migrations/team_mig.go @@ -0,0 +1,53 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addTeamMigrations(mg *Migrator) { + teamV1 := Table{ + Name: "team", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false}, + {Name: "org_id", Type: DB_BigInt}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + {Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id"}}, + {Cols: []string{"org_id", "name"}, Type: UniqueIndex}, + }, + } + + mg.AddMigration("create team table", NewAddTableMigration(teamV1)) + + //------- indexes ------------------ + mg.AddMigration("add index team.org_id", NewAddIndexMigration(teamV1, teamV1.Indices[0])) + mg.AddMigration("add unique index team_org_id_name", NewAddIndexMigration(teamV1, teamV1.Indices[1])) + + teamMemberV1 := Table{ + Name: "team_member", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: DB_BigInt}, + {Name: "team_id", Type: DB_BigInt}, + {Name: "user_id", Type: DB_BigInt}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + {Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id"}}, + {Cols: []string{"org_id", "team_id", "user_id"}, Type: UniqueIndex}, + }, + } + + mg.AddMigration("create team member table", NewAddTableMigration(teamMemberV1)) + + //------- 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/org_test.go b/pkg/services/sqlstore/org_test.go index e7c718fc9a8..59d96c4f8ca 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -154,6 +154,57 @@ func TestAccountDataAccess(t *testing.T) { So(err, ShouldEqual, m.ErrLastOrgAdmin) }) + Convey("Given an org user with dashboard permissions", func() { + ac3cmd := m.CreateUserCommand{Login: "ac3", Email: "ac3@test.com", Name: "ac3 name", IsAdmin: false} + err := CreateUser(&ac3cmd) + So(err, ShouldBeNil) + ac3 := ac3cmd.Result + + orgUserCmd := m.AddOrgUserCommand{ + OrgId: ac1.OrgId, + UserId: ac3.Id, + Role: m.ROLE_VIEWER, + } + + err = AddOrgUser(&orgUserCmd) + So(err, ShouldBeNil) + + query := m.GetOrgUsersQuery{OrgId: ac1.OrgId} + err = GetOrgUsers(&query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 3) + + err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: ac1.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT}) + So(err, ShouldBeNil) + + err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 2, OrgId: ac3.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT}) + So(err, ShouldBeNil) + + Convey("When org user is deleted", func() { + cmdRemove := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac3.Id} + err := RemoveOrgUser(&cmdRemove) + So(err, ShouldBeNil) + + Convey("Should remove dependent permissions for deleted org user", func() { + permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: ac1.OrgId} + err = GetDashboardAclInfoList(permQuery) + So(err, ShouldBeNil) + + So(len(permQuery.Result), ShouldEqual, 0) + }) + + Convey("Should not remove dashboard permissions for same user in another org", func() { + permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 2, OrgId: ac3.OrgId} + err = GetDashboardAclInfoList(permQuery) + So(err, ShouldBeNil) + + So(len(permQuery.Result), ShouldEqual, 1) + So(permQuery.Result[0].OrgId, ShouldEqual, ac3.OrgId) + So(permQuery.Result[0].UserId, ShouldEqual, ac3.Id) + }) + + }) + }) }) }) }) diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index 60800d1cb13..2c2a51fd362 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -88,10 +88,17 @@ func GetOrgUsers(query *m.GetOrgUsersQuery) error { func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error { return inTransaction(func(sess *DBSession) error { - var rawSql = "DELETE FROM org_user WHERE org_id=? and user_id=?" - _, err := sess.Exec(rawSql, cmd.OrgId, cmd.UserId) - if err != nil { - return err + deletes := []string{ + "DELETE FROM org_user WHERE org_id=? and user_id=?", + "DELETE FROM dashboard_acl WHERE org_id=? and user_id = ?", + "DELETE FROM team_member WHERE org_id=? and user_id = ?", + } + + for _, sql := range deletes { + _, err := sess.Exec(sql, cmd.OrgId, cmd.UserId) + if err != nil { + return err + } } return validateOneAdminLeftInOrg(cmd.OrgId, sess) diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go new file mode 100644 index 00000000000..ddf192da5ff --- /dev/null +++ b/pkg/services/sqlstore/search_builder.go @@ -0,0 +1,214 @@ +package sqlstore + +import ( + "bytes" + "strings" + + m "github.com/grafana/grafana/pkg/models" +) + +// SearchBuilder is a builder/object mother that builds a dashboard search query +type SearchBuilder struct { + tags []string + isStarred bool + limit int + signedInUser *m.SignedInUser + whereDashboardIdsIn []int64 + whereTitle string + whereTypeFolder bool + whereTypeDash bool + whereFolderIds []int64 + sql bytes.Buffer + params []interface{} +} + +func NewSearchBuilder(signedInUser *m.SignedInUser, limit int) *SearchBuilder { + searchBuilder := &SearchBuilder{ + signedInUser: signedInUser, + limit: limit, + } + + return searchBuilder +} + +func (sb *SearchBuilder) WithTags(tags []string) *SearchBuilder { + if len(tags) > 0 { + sb.tags = tags + } + + return sb +} + +func (sb *SearchBuilder) IsStarred() *SearchBuilder { + sb.isStarred = true + + return sb +} + +func (sb *SearchBuilder) WithDashboardIdsIn(ids []int64) *SearchBuilder { + if len(ids) > 0 { + sb.whereDashboardIdsIn = ids + } + + return sb +} + +func (sb *SearchBuilder) WithTitle(title string) *SearchBuilder { + sb.whereTitle = title + + return sb +} + +func (sb *SearchBuilder) WithType(queryType string) *SearchBuilder { + if len(queryType) > 0 && queryType == "dash-folder" { + sb.whereTypeFolder = true + } + + if len(queryType) > 0 && queryType == "dash-db" { + sb.whereTypeDash = true + } + + return sb +} + +func (sb *SearchBuilder) WithFolderIds(folderIds []int64) *SearchBuilder { + sb.whereFolderIds = folderIds + return sb +} + +// ToSql builds the sql and returns it as a string, together with the params. +func (sb *SearchBuilder) ToSql() (string, []interface{}) { + sb.params = make([]interface{}, 0) + + sb.buildSelect() + + if len(sb.tags) > 0 { + sb.buildTagQuery() + } else { + sb.buildMainQuery() + } + + sb.sql.WriteString(` + LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id + LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`) + + sb.sql.WriteString(" ORDER BY dashboard.title ASC LIMIT 5000") + + return sb.sql.String(), sb.params +} + +func (sb *SearchBuilder) buildSelect() { + sb.sql.WriteString( + `SELECT + dashboard.id, + dashboard.title, + dashboard.slug, + dashboard_tag.term, + dashboard.is_folder, + dashboard.folder_id, + folder.slug as folder_slug, + folder.title as folder_title + FROM `) +} + +func (sb *SearchBuilder) buildTagQuery() { + sb.sql.WriteString( + `( + SELECT + dashboard.id FROM dashboard + LEFT OUTER JOIN dashboard_tag ON dashboard_tag.dashboard_id = dashboard.id + `) + + if sb.isStarred { + sb.sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id") + } + + sb.sql.WriteString(` WHERE dashboard_tag.term IN (?` + strings.Repeat(",?", len(sb.tags)-1) + `) AND `) + for _, tag := range sb.tags { + sb.params = append(sb.params, tag) + } + + sb.buildSearchWhereClause() + + // this ends the inner select (tag filtered part) + sb.sql.WriteString(` + GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ? + LIMIT ?) as ids + INNER JOIN dashboard on ids.id = dashboard.id + `) + + sb.params = append(sb.params, len(sb.tags)) + sb.params = append(sb.params, sb.limit) +} + +func (sb *SearchBuilder) buildMainQuery() { + sb.sql.WriteString(`( SELECT dashboard.id FROM dashboard `) + + if sb.isStarred { + sb.sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id") + } + + sb.sql.WriteString(` WHERE `) + sb.buildSearchWhereClause() + + sb.sql.WriteString(` + LIMIT ?) as ids + INNER JOIN dashboard on ids.id = dashboard.id + `) + sb.params = append(sb.params, sb.limit) +} + +func (sb *SearchBuilder) buildSearchWhereClause() { + sb.sql.WriteString(` dashboard.org_id=?`) + sb.params = append(sb.params, sb.signedInUser.OrgId) + + if sb.isStarred { + sb.sql.WriteString(` AND star.user_id=?`) + sb.params = append(sb.params, sb.signedInUser.UserId) + } + + if len(sb.whereDashboardIdsIn) > 0 { + sb.sql.WriteString(` AND dashboard.id IN (?` + strings.Repeat(",?", len(sb.whereDashboardIdsIn)-1) + `)`) + for _, dashboardId := range sb.whereDashboardIdsIn { + sb.params = append(sb.params, dashboardId) + } + } + + if sb.signedInUser.OrgRole != m.ROLE_ADMIN { + allowedDashboardsSubQuery := ` AND (dashboard.has_acl = ` + dialect.BooleanStr(false) + ` OR dashboard.id in ( + SELECT distinct d.id AS DashboardId + FROM dashboard AS d + LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id + LEFT JOIN team_member as ugm on ugm.team_id = da.team_id + LEFT JOIN org_user ou on ou.role = da.role + WHERE + d.has_acl = ` + dialect.BooleanStr(true) + ` and + (da.user_id = ? or ugm.user_id = ? or ou.id is not null) + and d.org_id = ? + ) + )` + + sb.sql.WriteString(allowedDashboardsSubQuery) + sb.params = append(sb.params, sb.signedInUser.UserId, sb.signedInUser.UserId, sb.signedInUser.OrgId) + } + + if len(sb.whereTitle) > 0 { + sb.sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?") + sb.params = append(sb.params, "%"+sb.whereTitle+"%") + } + + if sb.whereTypeFolder { + sb.sql.WriteString(" AND dashboard.is_folder = " + dialect.BooleanStr(true)) + } + + if sb.whereTypeDash { + sb.sql.WriteString(" AND dashboard.is_folder = " + dialect.BooleanStr(false)) + } + + if len(sb.whereFolderIds) > 0 { + sb.sql.WriteString(` AND dashboard.folder_id IN (?` + strings.Repeat(",?", len(sb.whereFolderIds)-1) + `) `) + for _, id := range sb.whereFolderIds { + sb.params = append(sb.params, id) + } + } +} diff --git a/pkg/services/sqlstore/search_builder_test.go b/pkg/services/sqlstore/search_builder_test.go new file mode 100644 index 00000000000..32ccbc583f5 --- /dev/null +++ b/pkg/services/sqlstore/search_builder_test.go @@ -0,0 +1,37 @@ +package sqlstore + +import ( + "testing" + + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + . "github.com/smartystreets/goconvey/convey" +) + +func TestSearchBuilder(t *testing.T) { + dialect = migrator.NewDialect("sqlite3") + + Convey("Testing building a search", t, func() { + signedInUser := &m.SignedInUser{ + OrgId: 1, + UserId: 1, + } + sb := NewSearchBuilder(signedInUser, 1000) + + Convey("When building a normal search", func() { + sql, params := sb.IsStarred().WithTitle("test").ToSql() + So(sql, ShouldStartWith, "SELECT") + So(sql, ShouldContainSubstring, "INNER JOIN dashboard on ids.id = dashboard.id") + So(sql, ShouldEndWith, "ORDER BY dashboard.title ASC LIMIT 5000") + So(len(params), ShouldBeGreaterThan, 0) + }) + + Convey("When building a search with tag filter", func() { + sql, params := sb.WithTags([]string{"tag1", "tag2"}).ToSql() + So(sql, ShouldStartWith, "SELECT") + So(sql, ShouldContainSubstring, "LEFT OUTER JOIN dashboard_tag") + So(sql, ShouldEndWith, "ORDER BY dashboard.title ASC LIMIT 5000") + So(len(params), ShouldBeGreaterThan, 0) + }) + }) +} diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 707dc6da175..8558fb4506d 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/setting" @@ -102,6 +103,7 @@ func SetEngine(engine *xorm.Engine) (err error) { // Init repo instances annotations.SetRepository(&SqlAnnotationRepo{}) + dashboards.SetRepository(&dashboards.DashboardRepository{}) return nil } diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go new file mode 100644 index 00000000000..98bb1a36eb9 --- /dev/null +++ b/pkg/services/sqlstore/team.go @@ -0,0 +1,256 @@ +package sqlstore + +import ( + "bytes" + "fmt" + "time" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", CreateTeam) + bus.AddHandler("sql", UpdateTeam) + bus.AddHandler("sql", DeleteTeam) + bus.AddHandler("sql", SearchTeams) + bus.AddHandler("sql", GetTeamById) + bus.AddHandler("sql", GetTeamsByUser) + + bus.AddHandler("sql", AddTeamMember) + bus.AddHandler("sql", RemoveTeamMember) + bus.AddHandler("sql", GetTeamMembers) +} + +func CreateTeam(cmd *m.CreateTeamCommand) error { + return inTransaction(func(sess *DBSession) error { + + if isNameTaken, err := isTeamNameTaken(cmd.Name, 0, sess); err != nil { + return err + } else if isNameTaken { + return m.ErrTeamNameTaken + } + + team := m.Team{ + Name: cmd.Name, + Email: cmd.Email, + OrgId: cmd.OrgId, + Created: time.Now(), + Updated: time.Now(), + } + + _, err := sess.Insert(&team) + + cmd.Result = team + + return err + }) +} + +func UpdateTeam(cmd *m.UpdateTeamCommand) error { + return inTransaction(func(sess *DBSession) error { + + if isNameTaken, err := isTeamNameTaken(cmd.Name, cmd.Id, sess); err != nil { + return err + } else if isNameTaken { + return m.ErrTeamNameTaken + } + + 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 { + return err + } + + if affectedRows == 0 { + return m.ErrTeamNotFound + } + + return nil + }) +} + +func DeleteTeam(cmd *m.DeleteTeamCommand) error { + return inTransaction(func(sess *DBSession) error { + if res, err := sess.Query("SELECT 1 from team WHERE id=?", cmd.Id); err != nil { + return err + } else if len(res) != 1 { + return m.ErrTeamNotFound + } + + deletes := []string{ + "DELETE FROM team_member WHERE team_id = ?", + "DELETE FROM team WHERE id = ?", + "DELETE FROM dashboard_acl WHERE team_id = ?", + } + + for _, sql := range deletes { + _, err := sess.Exec(sql, cmd.Id) + if err != nil { + return err + } + } + return nil + }) +} + +func isTeamNameTaken(name string, existingId int64, sess *DBSession) (bool, error) { + var team m.Team + exists, err := sess.Where("name=?", name).Get(&team) + + if err != nil { + return false, nil + } + + if exists && existingId != team.Id { + return true, nil + } + + return false, nil +} + +func SearchTeams(query *m.SearchTeamsQuery) error { + query.Result = m.SearchTeamQueryResult{ + Teams: make([]*m.SearchTeamDto, 0), + } + queryWithWildcards := "%" + query.Query + "%" + + var sql bytes.Buffer + params := make([]interface{}, 0) + + 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 = ?`) + + params = append(params, query.OrgId) + + if query.Query != "" { + sql.WriteString(` and team.name ` + dialect.LikeStr() + ` ?`) + params = append(params, queryWithWildcards) + } + + if query.Name != "" { + sql.WriteString(` and team.name = ?`) + params = append(params, query.Name) + } + + sql.WriteString(` order by team.name asc`) + + if query.Limit != 0 { + sql.WriteString(` limit ? offset ?`) + offset := query.Limit * (query.Page - 1) + params = append(params, query.Limit, offset) + } + + if err := x.Sql(sql.String(), params...).Find(&query.Result.Teams); err != nil { + return err + } + + team := m.Team{} + countSess := x.Table("team") + if query.Query != "" { + countSess.Where(`name `+dialect.LikeStr()+` ?`, queryWithWildcards) + } + + if query.Name != "" { + countSess.Where("name=?", query.Name) + } + + count, err := countSess.Count(&team) + query.Result.TotalCount = count + + return err +} + +func GetTeamById(query *m.GetTeamByIdQuery) error { + var team m.Team + exists, err := x.Id(query.Id).Get(&team) + if err != nil { + return err + } + + if !exists { + return m.ErrTeamNotFound + } + + query.Result = &team + return nil +} + +func GetTeamsByUser(query *m.GetTeamsByUserQuery) error { + query.Result = make([]*m.Team, 0) + + sess := x.Table("team") + sess.Join("INNER", "team_member", "team.id=team_member.team_id") + sess.Where("team_member.user_id=?", query.UserId) + + err := sess.Find(&query.Result) + if err != nil { + return err + } + + return nil +} + +func AddTeamMember(cmd *m.AddTeamMemberCommand) error { + return inTransaction(func(sess *DBSession) error { + if res, err := sess.Query("SELECT 1 from team_member WHERE team_id=? and user_id=?", cmd.TeamId, cmd.UserId); err != nil { + return err + } else if len(res) == 1 { + return m.ErrTeamMemberAlreadyAdded + } + + if res, err := sess.Query("SELECT 1 from team WHERE id=?", cmd.TeamId); err != nil { + return err + } else if len(res) != 1 { + return m.ErrTeamNotFound + } + + entity := m.TeamMember{ + OrgId: cmd.OrgId, + TeamId: cmd.TeamId, + UserId: cmd.UserId, + Created: time.Now(), + Updated: time.Now(), + } + + _, err := sess.Insert(&entity) + return err + }) +} + +func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error { + return inTransaction(func(sess *DBSession) error { + var rawSql = "DELETE FROM team_member WHERE team_id=? and user_id=?" + _, err := sess.Exec(rawSql, cmd.TeamId, cmd.UserId) + if err != nil { + return err + } + + return err + }) +} + +func GetTeamMembers(query *m.GetTeamMembersQuery) error { + query.Result = make([]*m.TeamMemberDTO, 0) + sess := x.Table("team_member") + sess.Join("INNER", "user", fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user"))) + sess.Where("team_member.team_id=?", query.TeamId) + sess.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login") + sess.Asc("user.login", "user.email") + + err := sess.Find(&query.Result) + return err +} diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go new file mode 100644 index 00000000000..dbae4545266 --- /dev/null +++ b/pkg/services/sqlstore/team_test.go @@ -0,0 +1,116 @@ +package sqlstore + +import ( + "fmt" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + m "github.com/grafana/grafana/pkg/models" +) + +func TestTeamCommandsAndQueries(t *testing.T) { + + Convey("Testing Team commands & queries", t, func() { + InitTestDB(t) + + Convey("Given saved users and two teams", func() { + var userIds []int64 + for i := 0; i < 5; i++ { + userCmd := &m.CreateUserCommand{ + Email: fmt.Sprint("user", i, "@test.com"), + Name: fmt.Sprint("user", i), + Login: fmt.Sprint("loginuser", i), + } + err := CreateUser(userCmd) + So(err, ShouldBeNil) + userIds = append(userIds, userCmd.Result.Id) + } + + 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) + err = CreateTeam(&group2) + So(err, ShouldBeNil) + + Convey("Should be able to create teams and add users", func() { + query := &m.SearchTeamsQuery{Name: "group1 name", Page: 1, Limit: 10} + err = SearchTeams(query) + So(err, ShouldBeNil) + So(query.Page, ShouldEqual, 1) + + 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) + + q1 := &m.GetTeamMembersQuery{TeamId: team1.Id} + err = GetTeamMembers(q1) + So(err, ShouldBeNil) + So(q1.Result[0].TeamId, ShouldEqual, team1.Id) + So(q1.Result[0].Login, ShouldEqual, "loginuser0") + }) + + Convey("Should be able to search for teams", func() { + query := &m.SearchTeamsQuery{Query: "group", Page: 1} + err = SearchTeams(query) + So(err, ShouldBeNil) + So(len(query.Result.Teams), ShouldEqual, 2) + So(query.Result.TotalCount, ShouldEqual, 2) + + query2 := &m.SearchTeamsQuery{Query: ""} + err = SearchTeams(query2) + So(err, ShouldBeNil) + So(len(query2.Result.Teams), ShouldEqual, 2) + }) + + Convey("Should be able to return all teams a user is member of", func() { + groupId := group2.Result.Id + err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[0]}) + + query := &m.GetTeamsByUserQuery{UserId: userIds[0]} + err = GetTeamsByUser(query) + 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() { + err = RemoveTeamMember(&m.RemoveTeamMemberCommand{TeamId: group1.Result.Id, UserId: userIds[0]}) + So(err, ShouldBeNil) + + q1 := &m.GetTeamMembersQuery{TeamId: group1.Result.Id} + err = GetTeamMembers(q1) + So(err, ShouldBeNil) + So(len(q1.Result), ShouldEqual, 0) + }) + + Convey("Should be able to remove a group with users and permissions", func() { + groupId := group2.Result.Id + err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[1]}) + So(err, ShouldBeNil) + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[2]}) + So(err, ShouldBeNil) + err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: 1, Permission: m.PERMISSION_EDIT, TeamId: groupId}) + + err = DeleteTeam(&m.DeleteTeamCommand{Id: groupId}) + So(err, ShouldBeNil) + + query := &m.GetTeamByIdQuery{Id: groupId} + err = GetTeamById(query) + So(err, ShouldEqual, m.ErrTeamNotFound) + + permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1} + err = GetDashboardAclInfoList(permQuery) + So(err, ShouldBeNil) + + So(len(permQuery.Result), ShouldEqual, 0) + }) + }) + }) +} diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 3781d83dd96..73ea07f031f 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -350,6 +350,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { u.name as name, u.help_flags1 as help_flags1, u.last_seen_at as last_seen_at, + (SELECT COUNT(*) FROM org_user where org_user.user_id = u.id) as org_count, org.name as org_name, org_user.role as org_role, org.id as org_id @@ -400,7 +401,7 @@ func SearchUsers(query *m.SearchUsersQuery) error { } if query.Query != "" { - whereConditions = append(whereConditions, "(email LIKE ? OR name LIKE ? OR login like ?)") + whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)") whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards) } @@ -438,6 +439,10 @@ func DeleteUser(cmd *m.DeleteUserCommand) error { deletes := []string{ "DELETE FROM star WHERE user_id = ?", "DELETE FROM " + dialect.Quote("user") + " WHERE id = ?", + "DELETE FROM org_user WHERE user_id = ?", + "DELETE FROM dashboard_acl WHERE user_id = ?", + "DELETE FROM preferences WHERE user_id = ?", + "DELETE FROM team_member WHERE user_id = ?", } for _, sql := range deletes { diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index decb4682552..a65b7226eb6 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -6,7 +6,7 @@ import ( . "github.com/smartystreets/goconvey/convey" - "github.com/grafana/grafana/pkg/models" + m "github.com/grafana/grafana/pkg/models" ) func TestUserDataAccess(t *testing.T) { @@ -14,80 +14,134 @@ func TestUserDataAccess(t *testing.T) { Convey("Testing DB", t, func() { InitTestDB(t) - var err error - for i := 0; i < 5; i++ { - err = CreateUser(&models.CreateUserCommand{ - Email: fmt.Sprint("user", i, "@test.com"), - Name: fmt.Sprint("user", i), - Login: fmt.Sprint("loginuser", i), + Convey("Given 5 users", func() { + var err error + var cmd *m.CreateUserCommand + users := []m.User{} + for i := 0; i < 5; i++ { + cmd = &m.CreateUserCommand{ + Email: fmt.Sprint("user", i, "@test.com"), + Name: fmt.Sprint("user", i), + Login: fmt.Sprint("loginuser", i), + } + err = CreateUser(cmd) + So(err, ShouldBeNil) + users = append(users, cmd.Result) + } + + Convey("Can return the first page of users and a total count", func() { + query := m.SearchUsersQuery{Query: "", Page: 1, Limit: 3} + err = SearchUsers(&query) + + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 3) + So(query.Result.TotalCount, ShouldEqual, 5) }) - So(err, ShouldBeNil) - } - Convey("Can return the first page of users and a total count", func() { - query := models.SearchUsersQuery{Query: "", Page: 1, Limit: 3} - err = SearchUsers(&query) + Convey("Can return the second page of users and a total count", func() { + query := m.SearchUsersQuery{Query: "", Page: 2, Limit: 3} + err = SearchUsers(&query) - So(err, ShouldBeNil) - So(len(query.Result.Users), ShouldEqual, 3) - So(query.Result.TotalCount, ShouldEqual, 5) - }) + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 2) + So(query.Result.TotalCount, ShouldEqual, 5) + }) - Convey("Can return the second page of users and a total count", func() { - query := models.SearchUsersQuery{Query: "", Page: 2, Limit: 3} - err = SearchUsers(&query) + Convey("Can return list of users matching query on user name", func() { + query := m.SearchUsersQuery{Query: "use", Page: 1, Limit: 3} + err = SearchUsers(&query) - So(err, ShouldBeNil) - So(len(query.Result.Users), ShouldEqual, 2) - So(query.Result.TotalCount, ShouldEqual, 5) - }) + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 3) + So(query.Result.TotalCount, ShouldEqual, 5) - Convey("Can return list of users matching query on user name", func() { - query := models.SearchUsersQuery{Query: "use", Page: 1, Limit: 3} - err = SearchUsers(&query) + query = m.SearchUsersQuery{Query: "ser1", Page: 1, Limit: 3} + err = SearchUsers(&query) - So(err, ShouldBeNil) - So(len(query.Result.Users), ShouldEqual, 3) - So(query.Result.TotalCount, ShouldEqual, 5) + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 1) + So(query.Result.TotalCount, ShouldEqual, 1) - query = models.SearchUsersQuery{Query: "ser1", Page: 1, Limit: 3} - err = SearchUsers(&query) + query = m.SearchUsersQuery{Query: "USER1", Page: 1, Limit: 3} + err = SearchUsers(&query) - So(err, ShouldBeNil) - So(len(query.Result.Users), ShouldEqual, 1) - So(query.Result.TotalCount, ShouldEqual, 1) + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 1) + So(query.Result.TotalCount, ShouldEqual, 1) - query = models.SearchUsersQuery{Query: "USER1", Page: 1, Limit: 3} - err = SearchUsers(&query) + query = m.SearchUsersQuery{Query: "idontexist", Page: 1, Limit: 3} + err = SearchUsers(&query) - So(err, ShouldBeNil) - So(len(query.Result.Users), ShouldEqual, 1) - So(query.Result.TotalCount, ShouldEqual, 1) + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 0) + So(query.Result.TotalCount, ShouldEqual, 0) + }) - query = models.SearchUsersQuery{Query: "idontexist", Page: 1, Limit: 3} - err = SearchUsers(&query) + Convey("Can return list of users matching query on email", func() { + query := m.SearchUsersQuery{Query: "ser1@test.com", Page: 1, Limit: 3} + err = SearchUsers(&query) - So(err, ShouldBeNil) - So(len(query.Result.Users), ShouldEqual, 0) - So(query.Result.TotalCount, ShouldEqual, 0) - }) + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 1) + So(query.Result.TotalCount, ShouldEqual, 1) + }) - Convey("Can return list of users matching query on email", func() { - query := models.SearchUsersQuery{Query: "ser1@test.com", Page: 1, Limit: 3} - err = SearchUsers(&query) + Convey("Can return list of users matching query on login name", func() { + query := m.SearchUsersQuery{Query: "loginuser1", Page: 1, Limit: 3} + err = SearchUsers(&query) - So(err, ShouldBeNil) - So(len(query.Result.Users), ShouldEqual, 1) - So(query.Result.TotalCount, ShouldEqual, 1) - }) + So(err, ShouldBeNil) + So(len(query.Result.Users), ShouldEqual, 1) + So(query.Result.TotalCount, ShouldEqual, 1) + }) - Convey("Can return list of users matching query on login name", func() { - query := models.SearchUsersQuery{Query: "loginuser1", Page: 1, Limit: 3} - err = SearchUsers(&query) + Convey("when a user is an org member and has been assigned permissions", func() { + err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[0].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId}) + So(err, ShouldBeNil) - So(err, ShouldBeNil) - So(len(query.Result.Users), ShouldEqual, 1) - So(query.Result.TotalCount, ShouldEqual, 1) + err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT}) + So(err, ShouldBeNil) + + err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"}) + So(err, ShouldBeNil) + + Convey("when the user is deleted", func() { + err = DeleteUser(&m.DeleteUserCommand{UserId: users[0].Id}) + So(err, ShouldBeNil) + + Convey("Should delete connected org users and permissions", func() { + query := &m.GetOrgUsersQuery{OrgId: 1} + err = GetOrgUsersForTest(query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 1) + + permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1} + err = GetDashboardAclInfoList(permQuery) + So(err, ShouldBeNil) + + So(len(permQuery.Result), ShouldEqual, 0) + + prefsQuery := &m.GetPreferencesQuery{OrgId: users[0].OrgId, UserId: users[0].Id} + err = GetPreferences(prefsQuery) + So(err, ShouldBeNil) + + So(prefsQuery.Result.OrgId, ShouldEqual, 0) + So(prefsQuery.Result.UserId, ShouldEqual, 0) + }) + }) + }) }) }) } + +func GetOrgUsersForTest(query *m.GetOrgUsersQuery) error { + query.Result = make([]*m.OrgUserDTO, 0) + sess := x.Table("org_user") + sess.Join("LEFT ", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user"))) + sess.Where("org_user.org_id=?", query.OrgId) + sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role") + + err := sess.Find(&query.Result) + return err +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 2caf7366727..8cdb94bd413 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -50,12 +50,12 @@ var ( BuildStamp int64 // Paths - LogsPath string - HomePath string - DataPath string - PluginsPath string - DatasourcesPath string - CustomInitPath = "conf/custom.ini" + LogsPath string + HomePath string + DataPath string + PluginsPath string + ProvisioningPath string + CustomInitPath = "conf/custom.ini" // Log settings. LogModes []string @@ -106,6 +106,7 @@ var ( ExternalUserMngLinkUrl string ExternalUserMngLinkName string ExternalUserMngInfo string + ViewersCanEdit bool // Http auth AdminUser string @@ -474,8 +475,7 @@ func NewConfigContext(args *CommandLineArgs) error { Env = Cfg.Section("").Key("app_mode").MustString("development") InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name") PluginsPath = makeAbsolute(Cfg.Section("paths").Key("plugins").String(), HomePath) - DatasourcesPath = makeAbsolute(Cfg.Section("paths").Key("datasources").String(), HomePath) - + ProvisioningPath = makeAbsolute(Cfg.Section("paths").Key("provisioning").String(), HomePath) server := Cfg.Section("server") AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server) @@ -541,13 +541,14 @@ func NewConfigContext(args *CommandLineArgs) error { AllowUserSignUp = users.Key("allow_sign_up").MustBool(true) AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true) AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) - AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"}) + AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"}) VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false) LoginHint = users.Key("login_hint").String() DefaultTheme = users.Key("default_theme").String() ExternalUserMngLinkUrl = users.Key("external_manage_link_url").String() ExternalUserMngLinkName = users.Key("external_manage_link_name").String() ExternalUserMngInfo = users.Key("external_manage_info").String() + ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false) // auth auth := Cfg.Section("auth") @@ -599,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 @@ -609,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 } @@ -670,6 +671,6 @@ func LogConfigurationInfo() { logger.Info("Path Data", "path", DataPath) logger.Info("Path Logs", "path", LogsPath) logger.Info("Path Plugins", "path", PluginsPath) - logger.Info("Path Datasources", "path", DatasourcesPath) + logger.Info("Path Provisioning", "path", ProvisioningPath) logger.Info("App mode " + Env) } diff --git a/pkg/social/github_oauth.go b/pkg/social/github_oauth.go index c2a109a43e8..7e348e2363a 100644 --- a/pkg/social/github_oauth.go +++ b/pkg/social/github_oauth.go @@ -58,12 +58,12 @@ func (s *SocialGithub) IsTeamMember(client *http.Client) bool { return false } -func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool { +func (s *SocialGithub) IsOrganizationMember(client *http.Client, organizationsUrl string) bool { if len(s.allowedOrganizations) == 0 { return true } - organizations, err := s.FetchOrganizations(client) + organizations, err := s.FetchOrganizations(client, organizationsUrl) if err != nil { return false } @@ -167,12 +167,12 @@ func (s *SocialGithub) HasMoreRecords(headers http.Header) (string, bool) { } -func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) { +func (s *SocialGithub) FetchOrganizations(client *http.Client, organizationsUrl string) ([]string, error) { type Record struct { Login string `json:"login"` } - response, err := HttpGet(client, fmt.Sprintf(s.apiUrl+"/orgs")) + response, err := HttpGet(client, organizationsUrl) if err != nil { return nil, fmt.Errorf("Error getting organizations: %s", err) } @@ -193,10 +193,12 @@ func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) } func (s *SocialGithub) UserInfo(client *http.Client) (*BasicUserInfo, error) { + var data struct { - Id int `json:"id"` - Login string `json:"login"` - Email string `json:"email"` + Id int `json:"id"` + Login string `json:"login"` + Email string `json:"email"` + OrganizationsUrl string `json:"organizations_url"` } response, err := HttpGet(client, s.apiUrl) @@ -219,7 +221,7 @@ func (s *SocialGithub) UserInfo(client *http.Client) (*BasicUserInfo, error) { return nil, ErrMissingTeamMembership } - if !s.IsOrganizationMember(client) { + if !s.IsOrganizationMember(client, data.OrganizationsUrl) { return nil, ErrMissingOrganizationMembership } diff --git a/pkg/tsdb/cloudwatch/credentials.go b/pkg/tsdb/cloudwatch/credentials.go index 784f3b729ac..06848323fbb 100644 --- a/pkg/tsdb/cloudwatch/credentials.go +++ b/pkg/tsdb/cloudwatch/credentials.go @@ -11,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds" + "github.com/aws/aws-sdk-go/aws/defaults" "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudwatch" @@ -128,10 +129,10 @@ func remoteCredProvider(sess *session.Session) credentials.Provider { func ecsCredProvider(sess *session.Session, uri string) credentials.Provider { const host = `169.254.170.2` - c := ec2metadata.New(sess) + d := defaults.Get() return endpointcreds.NewProviderClient( - c.Client.Config, - c.Client.Handlers, + *d.Config, + d.Handlers, fmt.Sprintf("http://%s%s", host, uri), func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute }) } @@ -141,6 +142,11 @@ func ec2RoleProvider(sess *session.Session) credentials.Provider { } func (e *CloudWatchExecutor) getDsInfo(region string) *DatasourceInfo { + defaultRegion := e.DataSource.JsonData.Get("defaultRegion").MustString() + if region == "default" { + region = defaultRegion + } + authType := e.DataSource.JsonData.Get("authType").MustString() assumeRoleArn := e.DataSource.JsonData.Get("assumeRoleArn").MustString() accessKey := "" diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index b9d4d5b6a80..1e1e855b123 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -127,7 +127,7 @@ func init() { "AWS/Events": {"RuleName"}, "AWS/Firehose": {"DeliveryStreamName"}, "AWS/IoT": {"Protocol"}, - "AWS/Kinesis": {"StreamName", "ShardID"}, + "AWS/Kinesis": {"StreamName", "ShardId"}, "AWS/KinesisAnalytics": {"Flow", "Id", "Application"}, "AWS/Lambda": {"FunctionName", "Resource", "Version", "Alias"}, "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 7cadf055ff6..73b173813af 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -17,7 +17,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb" - opentracing "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go" ) type GraphiteExecutor struct { @@ -158,7 +158,7 @@ func formatTimeRange(input string) string { if input == "now" { return input } - return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1) + return strings.Replace(strings.Replace(strings.Replace(input, "now", "", -1), "m", "min", -1), "M", "mon", -1) } func fixIntervalFormat(target string) string { diff --git a/pkg/tsdb/graphite/graphite_test.go b/pkg/tsdb/graphite/graphite_test.go index c1a2736293b..1704a9b5f55 100644 --- a/pkg/tsdb/graphite/graphite_test.go +++ b/pkg/tsdb/graphite/graphite_test.go @@ -18,14 +18,14 @@ func TestGraphiteFunctions(t *testing.T) { Convey("formatting time range for now-1m", func() { timeRange := formatTimeRange("now-1m") - So(timeRange, ShouldEqual, "now-1min") + So(timeRange, ShouldEqual, "-1min") }) Convey("formatting time range for now-1M", func() { timeRange := formatTimeRange("now-1M") - So(timeRange, ShouldEqual, "now-1mon") + So(timeRange, ShouldEqual, "-1mon") }) diff --git a/pkg/tsdb/influxdb/model_parser_test.go b/pkg/tsdb/influxdb/model_parser_test.go index f8759afd3ba..7be9cae9702 100644 --- a/pkg/tsdb/influxdb/model_parser_test.go +++ b/pkg/tsdb/influxdb/model_parser_test.go @@ -20,7 +20,6 @@ func TestInfluxdbQueryParser(t *testing.T) { Convey("can parse influxdb json model", func() { json := ` { - "dsType": "influxdb", "groupBy": [ { "params": [ @@ -123,7 +122,6 @@ func TestInfluxdbQueryParser(t *testing.T) { Convey("can part raw query json model", func() { json := ` { - "dsType": "influxdb", "groupBy": [ { "params": [ diff --git a/pkg/tsdb/influxdb/response_parser.go b/pkg/tsdb/influxdb/response_parser.go index b7db6182241..8de8dcbb464 100644 --- a/pkg/tsdb/influxdb/response_parser.go +++ b/pkg/tsdb/influxdb/response_parser.go @@ -50,6 +50,7 @@ func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResul result = append(result, &tsdb.TimeSeries{ Name: rp.formatSerieName(row, column, query), Points: points, + Tags: row.Tags, }) } } diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 288787589ce..086eb96655f 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -89,7 +89,7 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, if err != nil { return "", fmt.Errorf("error parsing interval %v", args[1]) } - return fmt.Sprintf("(extract(epoch from \"%s\")/%v)::bigint*%v", args[0], interval.Seconds(), interval.Seconds()), nil + return fmt.Sprintf("(extract(epoch from %s)/%v)::bigint*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index ff268805259..ebc5191d46e 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -45,7 +45,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY (extract(epoch from \"time_column\")/300)::bigint*300") + So(sql, ShouldEqual, "GROUP BY (extract(epoch from time_column)/300)::bigint*300 AS time") }) Convey("interpolate __timeTo function", func() { diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index dcb60977edc..a8c96d8119c 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -78,6 +78,15 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro rowLimit := 1000000 rowCount := 0 + timeIndex := -1 + + // check if there is a column named time + for i, col := range columnNames { + switch col { + case "time": + timeIndex = i + } + } for ; rows.Next(); rowCount++ { if rowCount > rowLimit { @@ -89,6 +98,15 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro return err } + // convert column named time to unix timestamp to make + // native datetime postgres types work in annotation queries + if timeIndex != -1 { + switch value := values[timeIndex].(type) { + case time.Time: + values[timeIndex] = float64(value.UnixNano() / 1e9) + } + } + table.Rows = append(table.Rows, values) } @@ -142,8 +160,13 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error { pointsBySeries := make(map[string]*tsdb.TimeSeries) seriesByQueryOrder := list.New() - columnNames, err := rows.Columns() + columnNames, err := rows.Columns() + if err != nil { + return err + } + + columnTypes, err := rows.ColumnTypes() if err != nil { return err } @@ -153,13 +176,21 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co timeIndex := -1 metricIndex := -1 - // check columns of resultset + // check columns of resultset: a column named time is mandatory + // the first text column is treated as metric name unless a column named metric is present for i, col := range columnNames { switch col { case "time": timeIndex = i case "metric": metricIndex = i + default: + if metricIndex == -1 { + switch columnTypes[i].DatabaseTypeName() { + case "UNKNOWN", "TEXT", "VARCHAR", "CHAR": + metricIndex = i + } + } } } 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/pkg/util/url.go b/pkg/util/url.go index ba452596a2b..c82dcef67c5 100644 --- a/pkg/util/url.go +++ b/pkg/util/url.go @@ -9,10 +9,15 @@ type UrlQueryReader struct { values url.Values } -func NewUrlQueryReader(url *url.URL) *UrlQueryReader { - return &UrlQueryReader{ - values: url.Query(), +func NewUrlQueryReader(urlInfo *url.URL) (*UrlQueryReader, error) { + u, err := url.ParseQuery(urlInfo.String()) + if err != nil { + return nil, err } + + return &UrlQueryReader{ + values: u, + }, nil } func (r *UrlQueryReader) Get(name string, def string) string { diff --git a/public/app/app.ts b/public/app/app.ts index 8e345c6abed..45050240602 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -9,7 +9,6 @@ import 'angular-native-dragdrop'; import 'angular-bindonce'; import 'react'; import 'react-dom'; -import 'ngreact'; import 'vendor/bootstrap/bootstrap'; import 'vendor/angular-ui/ui-bootstrap-tpls'; @@ -22,12 +21,12 @@ import _ from 'lodash'; import moment from 'moment'; // add move to lodash for backward compatabiltiy -_.move = function (array, fromIndex, toIndex) { +_.move = function(array, fromIndex, toIndex) { array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); return array; }; -import {coreModule, registerAngularDirectives} from './core/core'; +import { coreModule, registerAngularDirectives } from './core/core'; export class GrafanaApp { registerFunctions: any; @@ -66,24 +65,28 @@ export class GrafanaApp { $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; + 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(); + $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; - }]); + return get(url, config); + }; + return $delegate; + }, + ]); }); this.ngModuleDependencies = [ @@ -96,7 +99,7 @@ export class GrafanaApp { 'pasvaz.bindonce', 'ui.bootstrap', 'ui.bootstrap.tpls', - 'react' + 'react', ]; var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; @@ -114,20 +117,22 @@ export class GrafanaApp { var preBootRequires = [System.import('app/features/all')]; - Promise.all(preBootRequires).then(() => { - // disable tool tip animation - $.fn.tooltip.defaults.animation = false; - // bootstrap the app - angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { - _.each(this.preBootModules, module => { - _.extend(module, this.registerFunctions); - }); + Promise.all(preBootRequires) + .then(() => { + // disable tool tip animation + $.fn.tooltip.defaults.animation = false; + // bootstrap the app + angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { + _.each(this.preBootModules, module => { + _.extend(module, this.registerFunctions); + }); - this.preBootModules = null; + this.preBootModules = null; + }); + }) + .catch(function(err) { + console.log('Application boot failed:', err); }); - }).catch(function(err) { - console.log('Application boot failed:', err); - }); } } diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 7acdc79d55a..83a70fa4c8a 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -1,8 +1,12 @@ import { react2AngularDirective } from 'app/core/utils/react2angular'; import { PasswordStrength } from './components/PasswordStrength'; +import PageHeader from './components/PageHeader/PageHeader'; +import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; +import LoginBackground from './components/Login/LoginBackground'; export function registerAngularDirectives() { - react2AngularDirective('passwordStrength', PasswordStrength, ['password']); - + react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); + react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); + react2AngularDirective('loginBackground', LoginBackground, []); } diff --git a/public/app/core/app_events.ts b/public/app/core/app_events.ts index 4507246d2be..26dd74bcb00 100644 --- a/public/app/core/app_events.ts +++ b/public/app/core/app_events.ts @@ -1,6 +1,4 @@ -/// - -import {Emitter} from './utils/emitter'; +import { Emitter } from './utils/emitter'; var appEvents = new Emitter(); export default appEvents; diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx new file mode 100644 index 00000000000..d62ae892a0a --- /dev/null +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +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' +}; + +describe('CollorPalette', () => { + + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx new file mode 100644 index 00000000000..1583303dfa1 --- /dev/null +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -0,0 +1,34 @@ +import React, { Component } from 'react'; + +export interface IProps { + model: any; +} + +class EmptyListCTA extends Component { + render() { + const { + title, + buttonIcon, + buttonLink, + buttonTitle, + proTip, + proTipLink, + proTipLinkTitle, + proTipTarget + } = this.props.model; + return ( +
+
{title}
+ {buttonTitle} +
+ ProTip: {proTip} + {proTipLinkTitle} +
+
+ ); + } +} + +export default EmptyListCTA; diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap new file mode 100644 index 00000000000..0da3d94aaa8 --- /dev/null +++ b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollorPalette renders correctly 1`] = ` +
+
+ Title +
+ + + Click me + +
+ + ProTip: + This is a tip + + Learn more + +
+
+`; diff --git a/public/app/core/components/Login/LoginBackground.tsx b/public/app/core/components/Login/LoginBackground.tsx new file mode 100644 index 00000000000..83e228ab6e0 --- /dev/null +++ b/public/app/core/components/Login/LoginBackground.tsx @@ -0,0 +1,1240 @@ +import React, { Component } from 'react'; + +const xCount = 50; +const yCount = 50; + +function Cell({ x, y, flipIndex }) { + const index = (y * xCount) + x; + const bgColor1 = getColor(x, y); + return ( +
+ ); +} + +function getRandomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive +} + +export default class LoginBackground extends Component { + cancelInterval: any; + + constructor(props) { + super(props); + + this.state = { + flipIndex: null, + }; + + this.flipElements = this.flipElements.bind(this); + } + + flipElements() { + const elementIndexToFlip = getRandomInt(0, (xCount * yCount) - 1); + this.setState(prevState => { + return { + ...prevState, + flipIndex: elementIndexToFlip, + }; + }); + } + + componentWillMount() { + this.cancelInterval = setInterval(this.flipElements, 3000); + } + + componentWillUnmount() { + clearInterval(this.cancelInterval); + } + + render() { + console.log('re-render!', this.state.flipIndex); + + return ( +
+ {Array.from(Array(yCount)).map((el, y) => { + return ( +
+ {Array.from(Array(xCount)).map((el2, x) => { + return ( + + ); + })} +
+ ); + })} +
+ ); + } +} + +function getColor(x, y) { + const colors = [ + '#14161A', + '#111920', + '#121E27', + '#13212B', + '#122029', + '#101C24', + '#0F1B23', + '#0F1B22', + '#111C24', + '#101A22', + '#101A21', + '#111D25', + '#101E27', + '#101D26', + '#101B23', + '#11191E', + '#131519', + '#131518', + '#101B21', + '#121F29', + '#10232D', + '#11212B', + '#0E1C25', + '#0E1C24', + '#111F29', + '#11222B', + '#101E28', + '#102028', + '#111F2A', + '#11202A', + '#11191F', + '#121417', + '#12191D', + '#101D25', + '#11212C', + '#10242F', + '#0F212B', + '#0F1E27', + '#0F1D26', + '#0F1F29', + '#0F2029', + '#11232E', + '#10212B', + '#10222C', + '#0F202A', + '#112530', + '#10252F', + '#0F242E', + '#10222D', + '#10202A', + '#0F1C24', + '#0F1E28', + '#0F212A', + '#0F222B', + '#14171A', + '#0F1A20', + '#0F1C25', + '#10232E', + '#0E202A', + '#0E1E27', + '#0E1D26', + '#0F202B', + '#11232F', + '#102632', + '#102530', + '#122430', + '#0F1B21', + '#0F212C', + '#0E1F29', + '#112531', + '#0F2734', + '#0F2835', + '#0D1B23', + '#0F1A21', + '#0F1A23', + '#0F1D27', + '#0F222D', + '#102430', + '#102531', + '#10222E', + '#0F232D', + '#0E2633', + '#0E2734', + '#0F2834', + '#0E2835', + '#0F2633', + '#0F2532', + '#0E1A22', + '#0D1C24', + '#0F2735', + '#0F2937', + '#102A38', + '#112938', + '#102A39', + '#0F2A38', + '#102836', + '#0E1B23', + '#0F2938', + '#102A3A', + '#102D3D', + '#0F3040', + '#102D3E', + '#0F2E3E', + '#112C3B', + '#102B3B', + '#102B3A', + '#102D3C', + '#0F2A39', + '#0F2634', + '#0E2029', + '#0E1A21', + '#0F2B39', + '#0F2D3D', + '#0F2F40', + '#0E3142', + '#113445', + '#122431', + '#102E3E', + '#0F3345', + '#0E2F40', + '#0F3143', + '#102C3C', + '#0F2B3A', + '#0F1F28', + '#0F3344', + '#113548', + '#113C51', + '#144258', + '#103A4E', + '#103A4F', + '#103547', + '#10364A', + '#103649', + '#0F3448', + '#102C3A', + '#0F2836', + '#103447', + '#0F384C', + '#123F55', + '#15445A', + '#133F55', + '#103B50', + '#113E54', + '#103446', + '#0F3A4F', + '#0F3548', + '#0D3142', + '#102C3B', + '#0E2937', + '#103D52', + '#0E3544', + '#184C65', + '#154760', + '#14435B', + '#15465F', + '#124159', + '#0F3D53', + '#103C51', + '#0F3447', + '#0E3243', + '#113143', + '#113D53', + '#184B64', + '#184D67', + '#184C66', + '#174A63', + '#15455C', + '#13425A', + '#14445A', + '#10384C', + '#0E3446', + '#10181E', + '#103243', + '#0F384D', + '#14455C', + '#164761', + '#164C66', + '#1D627D', + '#12425A', + '#164A63', + '#14465D', + '#13435A', + '#0A2B38', + '#0F3446', + '#0D2F40', + '#0D2F3F', + '#0F2531', + '#102937', + '#10384B', + '#0F3649', + '#184E68', + '#1A5472', + '#184D68', + '#154A63', + '#19506B', + '#19536F', + '#1A4F69', + '#144760', + '#114058', + '#0E3A4F', + '#0E3547', + '#0C3042', + '#0E1B24', + '#11222C', + '#154C65', + '#1A5776', + '#1B5675', + '#113847', + '#1A5371', + '#194E68', + '#0E2D3D', + '#112D3B', + '#113D52', + '#18516D', + '#1A5979', + '#1B5878', + '#19526E', + '#1A526E', + '#13435B', + '#0F3E55', + '#0B374C', + '#0E3448', + '#0D2E3F', + '#0F2B3B', + '#112E3E', + '#113B50', + '#15465D', + '#1A526F', + '#1E5E81', + '#1D5B7B', + '#1A5777', + '#154456', + '#113949', + '#0D394E', + '#0F3549', + '#0F2C3B', + '#0E2733', + '#112E3D', + '#123D52', + '#10394C', + '#1B5674', + '#1A5370', + '#144861', + '#104058', + '#104159', + '#0E384C', + '#0D2D3D', + '#0E2533', + '#112C3A', + '#1B5979', + '#1B5C7D', + '#1A5675', + '#104057', + '#0F3C51', + '#11425A', + '#0E394D', + '#0C3243', + '#0E2735', + '#112F3E', + '#134158', + '#1D5E7F', + '#1D6083', + '#1C5877', + '#1A5573', + '#184D66', + '#164962', + '#0F3D54', + '#0E3D53', + '#0E3447', + '#0F2A3A', + '#0F2936', + '#101F28', + '#103040', + '#124056', + '#164E69', + '#144B64', + '#164D66', + '#0F3E54', + '#0E3B51', + '#0D3346', + '#0E1F27', + '#124158', + '#164961', + '#0E3C52', + '#19506C', + '#0F2C3C', + '#0E3244', + '#0E2A39', + '#0E2938', + '#113040', + '#134057', + '#1A5471', + '#154B63', + '#1C597A', + '#164760', + '#10374B', + '#0E374C', + '#0E384D', + '#11242F', + '#10394D', + '#18526E', + '#154B65', + '#103F55', + '#0D3345', + '#102532', + '#102029', + '#113142', + '#1B5973', + '#1A516B', + '#1C5979', + '#1C5A7A', + '#184A65', + '#164C65', + '#0D3041', + '#123142', + '#123E54', + '#1B5877', + '#1A5574', + '#1C5878', + '#13435C', + '#0F374B', + '#0C3143', + '#112F40', + '#123C51', + '#174E68', + '#1D5C7D', + '#14465F', + '#0F3F56', + '#0B3041', + '#123243', + '#15435B', + '#19516D', + '#1D5D7E', + '#1C5C7D', + '#184F69', + '#11374B', + '#103E54', + '#0E3143', + '#0F2D3C', + '#11242E', + '#133445', + '#1A5674', + '#1D6184', + '#1F658B', + '#0D3A50', + '#0C374B', + '#154862', + '#164B64', + '#154961', + '#0D384D', + '#102631', + '#113242', + '#134259', + '#185270', + '#1D6386', + '#1E678C', + '#1C5978', + '#0D3549', + '#0F2632', + '#184961', + '#1D5E80', + '#1E6488', + '#1F678D', + '#1E5B7C', + '#164862', + '#19526D', + '#113C52', + '#15455E', + '#0F2F3F', + '#144259', + '#194D67', + '#1D6991', + '#195777', + '#19516C', + '#103F56', + '#144660', + '#0D2E3E', + '#10212A', + '#113141', + '#16455C', + '#1D5B7C', + '#1F6589', + '#1E668C', + '#1E5F81', + '#0F3B50', + '#0D3244', + '#164A64', + '#184E69', + '#0E364A', + '#0E2E3E', + '#10222B', + '#19475E', + '#1B5A7B', + '#1E5D7F', + '#1E678D', + '#1E6184', + '#19506A', + '#1B5370', + '#1B5573', + '#0E3041', + '#122E3E', + '#16455B', + '#195370', + '#1D6489', + '#1D6B93', + '#164A65', + '#154A64', + '#1A5572', + '#1D6082', + '#1F6286', + '#1D6C94', + '#1E709A', + '#174A65', + '#1B526F', + '#1E6589', + '#1D6384', + '#0D3143', + '#0E2F3F', + '#174760', + '#1F6487', + '#1D668C', + '#0D2F41', + '#103B4F', + '#1C5C7E', + '#1F688F', + '#1C5B7C', + '#164D68', + '#1D6285', + '#0D364A', + '#1D5A7A', + '#1E6990', + '#1D6488', + '#18516B', + '#1A506B', + '#0E3B50', + '#0E3548', + '#124259', + '#13455C', + '#14485F', + '#1E5C7D', + '#122D3C', + '#1E6E98', + '#1E6A91', + '#1E6286', + '#1E6C95', + '#1D6990', + '#101F29', + '#174A62', + '#10394E', + '#1D6D96', + '#1E688E', + '#1D6E97', + '#1E6C94', + '#0E394E', + '#112B39', + '#195270', + '#1E668B', + '#1E6386', + '#1D6385', + '#0C3142', + '#1E6083', + '#1E729C', + '#1F709A', + '#1E6F98', + '#1D5F81', + '#1F688D', + '#1C6488', + '#1D6588', + '#1C6A93', + '#1E658B', + '#1F6C95', + '#0D3C52', + '#1C6385', + '#1E5F82', + '#0E3D54', + '#0F3244', + '#18485F', + '#1E6991', + '#1C5B7B', + '#1F6082', + '#0F3346', + '#18536F', + '#114056', + '#1D6B92', + '#1B5776', + '#0F3C52', + '#1E6890', + '#1F688E', + '#0C394E', + '#0F1D25', + '#1F6386', + '#1E688D', + '#1F6488', + '#20668C', + '#1D5978', + '#0F3D52', + '#0F1E26', + '#13465F', + '#0D374C', + '#1B5C7C', + '#0E1A23', + '#0F374A', + '#1B5574', + '#0F394C', + '#0E2A38', + '#102A37', + '#18506B', + '#1E5A7A', + '#0F3245', + '#0E2E3F', + '#1E678E', + '#1C5D7E', + '#1A5A7A', + '#0E2837', + '#102733', + '#0F3B51', + '#15475E', + '#1E6B93', + '#1E648A', + '#194961', + '#0F3A4E', + '#0E1D25', + '#194F69', + '#103345', + '#0F394D', + '#102B39', + '#103E55', + '#1B5572', + '#164861', + '#174861', + '#113B4F', + '#102936', + '#0F3041', + '#174961', + '#113E53', + '#134056', + '#124057', + '#194B63', + '#0E364B', + '#15445B', + '#16475E', + '#102F3F', + '#16485F', + '#0F2E3D', + '#101920', + '#12222C', + '#122C3B', + '#144157', + '#123B50', + '#16465D', + '#184960', + '#112B3A', + '#12232F', + '#132430', + '#113344', + '#11394C', + '#113649', + '#11364A', + '#133F56', + '#121D25', + '#112733', + '#112A38', + '#0F1F2A', + '#113447', + '#113A4E', + '#0F222C', + '#13222B', + '#112836', + '#102F3E', + '#113243', + '#123445', + '#12374B', + '#121E26', + '#122531', + '#11303F', + '#0D1D25', + '#102835', + '#112834', + '#101C23', + '#111C23', + '#12212B', + '#11222D', + '#0E1B22', + '#0E1D27', + '#121C22', + '#12202A', + '#101A20', + '#13191E', + '#111E28', + '#11212D', + '#0F1B24', + '#0F1C23', + '#13181D', + '#15171A', + '#121D23', + '#121F27', + '#111E27', + '#101B22', + '#121F28', + '#111E26', + '#101D24', + '#111C22', + '#12161E', + '#101925', + '#121E2D', + '#112033', + '#111E2F', + '#0F1B29', + '#0F1A28', + '#101B2A', + '#0E1A27', + '#101C2B', + '#111D2D', + '#111D2B', + '#0F1B28', + '#101923', + '#13161D', + '#13161C', + '#0F1A26', + '#101E2F', + '#112235', + '#102031', + '#0F1B2A', + '#112031', + '#102032', + '#101D2E', + '#121F2F', + '#112133', + '#101E30', + '#101F30', + '#102336', + '#101B2C', + '#0F1C2B', + '#111E2E', + '#0F2134', + '#102236', + '#0F2133', + '#101F31', + '#0F2438', + '#102337', + '#102235', + '#102133', + '#11171E', + '#101F2F', + '#102030', + '#102234', + '#102132', + '#12181F', + '#0F1A25', + '#0F2135', + '#0F1F30', + '#0F1C2D', + '#101D2C', + '#0F2033', + '#0E2338', + '#0F2237', + '#0F2236', + '#0B243B', + '#0D2338', + '#0E1A26', + '#0F1D2E', + '#0F2032', + '#0D2339', + '#0B253F', + '#0A253F', + '#0A253E', + '#0C2439', + '#0E1925', + '#0E2135', + '#0F2235', + '#0A243A', + '#08253E', + '#09253E', + '#0A263F', + '#0A243C', + '#0B233B', + '#0E1A28', + '#0D1A26', + '#09253F', + '#0A2743', + '#0B2844', + '#0B2641', + '#0A2744', + '#0A2844', + '#0B2743', + '#092745', + '#0F2337', + '#101D2D', + '#092743', + '#092846', + '#0E2B4C', + '#102E4F', + '#0E2C4D', + '#0B2A49', + '#082947', + '#0D2B4B', + '#0C2A4A', + '#092946', + '#082845', + '#0C2B4B', + '#0F2D4E', + '#103051', + '#133257', + '#0E2D4E', + '#143156', + '#112F51', + '#0B243A', + '#082744', + '#092844', + '#123054', + '#143359', + '#173A64', + '#183F6E', + '#173F6D', + '#153961', + '#163962', + '#133358', + '#15345B', + '#14345A', + '#102F50', + '#0A2948', + '#082844', + '#092641', + '#16375F', + '#193C69', + '#174170', + '#173E6B', + '#163A63', + '#173D69', + '#183D6A', + '#15365E', + '#112E50', + '#0A2A49', + '#082743', + '#0E1927', + '#173C68', + '#13487E', + '#164476', + '#174375', + '#193F6F', + '#173B66', + '#163B65', + '#082A48', + '#0A2641', + '#09243C', + '#174171', + '#14477C', + '#124980', + '#14487F', + '#174374', + '#15467B', + '#184172', + '#17406F', + '#184070', + '#163C67', + '#16355D', + '#123256', + '#0E1B29', + '#0F1923', + '#113052', + '#184274', + '#164579', + '#13477C', + '#193E6D', + '#0A243E', + '#0B233A', + '#0D1A29', + '#0B2742', + '#17365E', + '#163860', + '#124A84', + '#095191', + '#114A83', + '#0D4D8A', + '#0C4D8C', + '#104B85', + '#15477E', + '#174477', + '#183862', + '#0A233A', + '#092947', + '#09243D', + '#173963', + '#194173', + '#085396', + '#085394', + '#114B87', + '#144983', + '#094F8E', + '#075090', + '#0F4C89', + '#215287', + '#0E1A29', + '#184376', + '#0C4D8B', + '#07549A', + '#0A4E8D', + '#0F4C88', + '#0A4E8C', + '#174273', + '#193C6A', + '#0B2948', + '#0B2C4B', + '#0C4E8D', + '#1259A4', + '#0C579E', + '#0D4D8B', + '#095397', + '#085397', + '#085295', + '#144880', + '#173861', + '#15335A', + '#0F2C4D', + '#0C2949', + '#0B4E8D', + '#08559C', + '#07508F', + '#154578', + '#17365F', + '#122F53', + '#111D2C', + '#092A48', + '#08559D', + '#08559E', + '#0C56A1', + '#164271', + '#163E6A', + '#194071', + '#082642', + '#0F1E30', + '#0D2D4D', + '#114C87', + '#0E59A3', + '#135BA6', + '#085498', + '#085497', + '#095192', + '#0E4D8B', + '#0C4E8A', + '#134982', + '#17457B', + '#121F2E', + '#183E6C', + '#153E69', + '#07508E', + '#173F6C', + '#193D6B', + '#112D4F', + '#0A243B', + '#072946', + '#111E2D', + '#0B2740', + '#10497F', + '#17406E', + '#084F8D', + '#104A80', + '#0E2E4F', + '#143358', + '#16365D', + '#0A2742', + '#13477B', + '#154474', + '#104C86', + '#095291', + '#0B4F8E', + '#114A80', + '#095090', + '#075296', + '#163760', + '#2D6DB5', + '#0C2843', + '#0C233A', + '#153A62', + '#14467A', + '#075498', + '#085293', + '#09263F', + '#122030', + '#09559D', + '#0F4B83', + '#08549A', + '#14375D', + '#085499', + '#075499', + '#0A243D', + '#143E68', + '#10497E', + '#074F8E', + '#085496', + '#0C58A3', + '#065499', + '#085190', + '#0A2B4A', + '#104C88', + '#0D4F8E', + '#0F58A2', + '#0B569B', + '#0D58A1', + '#134A81', + '#09559C', + '#0A5293', + '#114B86', + '#0D2C4C', + '#103255', + '#16457A', + '#074F8C', + '#07559C', + '#185DA9', + '#1D61AD', + '#175CA8', + '#16406D', + '#153C65', + '#0E243A', + '#144679', + '#085192', + '#1A5EAC', + '#1D61AE', + '#11497F', + '#12487E', + '#0C243C', + '#123155', + '#0F59A3', + '#1B5FAB', + '#1E61AD', + '#145CA4', + '#0E599F', + '#11497E', + '#094F8D', + '#15345A', + '#134A85', + '#165CA8', + '#2263AF', + '#124466', + '#0A518F', + '#08569D', + '#16416F', + '#0B2B4A', + '#124A83', + '#0C57A2', + '#1E60AD', + '#1E62AE', + '#165DA8', + '#1059A4', + '#15406C', + '#0A4F8E', + '#12365A', + '#0A5191', + '#16355C', + '#1C5EAB', + '#155CA7', + '#085292', + '#174478', + '#153258', + '#111F2F', + '#174272', + '#1159A5', + '#1C5EAC', + '#2F74BB', + '#0C58A2', + '#0D59A3', + '#14477D', + '#132F53', + '#155BA6', + '#195FAA', + '#2366B1', + '#2967B2', + '#14477E', + '#1B5EAB', + '#175DA8', + '#0F4C86', + '#065090', + '#1C5FAC', + '#185CA8', + '#0D58A3', + '#0C4E8C', + '#134981', + '#14416D', + '#0F5AA5', + '#1F63AF', + '#114B88', + '#09508E', + '#0A569D', + '#195DAA', + '#0F1D2F', + '#1059A2', + '#0E599E', + '#2063AF', + '#1F63AE', + '#1A5EAA', + '#0C57A0', + '#195EAA', + '#1A5EA9', + '#0E4E8A', + '#12487D', + '#185DAA', + '#175EAA', + '#0A508E', + '#1559A6', + '#0E58A3', + '#095399', + '#0B4E8B', + '#0B569F', + '#0C57A1', + '#2967B1', + '#2365B0', + '#2163AE', + '#1A5DAA', + '#195EAB', + '#1E5FAC', + '#2564AF', + '#2767B1', + '#2766B1', + '#0D5A9F', + '#2062AE', + '#1F61AD', + '#195FAB', + '#0D4E8D', + '#173760', + '#111D2E', + '#09518F', + '#1A5FAC', + '#135BA7', + '#085291', + '#183761', + '#0B2845', + '#113457', + '#075393', + '#185EA9', + '#2B69B3', + '#2A67B2', + '#2867B1', + '#155DA8', + '#135CA6', + '#135AA5', + '#114980', + '#2566B1', + '#2064AF', + '#2364AF', + '#13365B', + '#154475', + '#08549B', + '#164373', + '#085392', + '#144576', + '#12497E', + '#0E5392', + '#135BA3', + '#0C5395', + '#0C5291', + '#0E579C', + '#0E5290', + '#134C83', + '#2163AC', + '#195CA6', + '#0D4E8C', + '#082945', + '#133256', + '#0E2F50', + '#105AA6', + '#134677', + '#144475', + '#145BA7', + '#154270', + '#1D60AD', + '#09569B', + '#09243E', + '#134A86', + '#0E59A4', + '#0A4E8B', + '#0E4B83', + '#1D5EAC', + '#101C2A', + '#134A84', + '#0E518F', + '#145CA7', + '#0E5699', + '#145BA5', + '#095292', + '#15416E', + '#153D67', + '#153F6B', + '#125AA5', + '#16406E', + '#0E1B27', + '#0D4F8C', + '#0F58A3', + '#114A82', + '#09569C', + '#0C2339', + '#0E1B28', + '#0D59A4', + '#07559D', + '#08569E', + '#095190', + '#0B253E', + '#0C2B49', + '#2264AF', + '#09549A', + '#09569F', + '#163D68', + '#0C263F', + '#143960', + '#183A65', + '#075496', + '#0C579F', + '#085191', + '#102438', + '#075295', + '#082946', + '#102437', + '#0C2642', + '#101C29', + '#0C253E', + '#15355C', + '#0B2E4D', + '#0F3253', + '#154577', + '#16335B', + '#0F1925', + '#0C2742', + '#0B2946', + '#0E2C4B', + '#0E2B48', + '#0E2237', + '#102237', + '#0B253D', + '#0A2946', + '#0C2841', + '#0D2A47', + '#0C2C4A', + '#08253F', + '#08243D', + '#111C2B', + '#0C2844', + '#0C2945', + '#0D243A', + '#122134', + '#0B2642', + '#113154', + '#113255', + '#0A2642', + '#0A2945', + '#0B263F', + '#0D2E4E', + '#0F1E2E', + '#0A2845', + '#0D2439', + '#0F1A29', + '#101C2E', + '#111923', + '#13181F', + '#111D2F', + '#111F30', + '#121E30', + '#121E2E', + '#101B27', + '#101A27', + '#13171F', + ]; + + // let randX = getRandomInt(0, x); + // let randY = getRandomInt(0, y); + // let randIndex = randY * xCount + randX; + + return colors[(y*xCount + x) % colors.length]; +} diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx new file mode 100644 index 00000000000..9b45267a8e5 --- /dev/null +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -0,0 +1,164 @@ +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; +} + +function TabItem(tab: NavModelItem) { + if (tab.hideFromTabs) { + return null; + } + + let tabClasses = classNames({ + "gf-tabs-link": true, + active: tab.active + }); + + return ( +
  • + + + {tab.text} + +
  • + ); +} + +function SelectOption(navItem: NavModelItem) { + if (navItem.hideFromTabs) { + // TODO: Rename hideFromTabs => hideFromNav + return null; + } + + return ( + + ); +} + +function Navigation({ main }: { main: NavModelItem }) { + return ( + + ); +} + +function SelectNav({ + main, + customCss +}: { + main: NavModelItem; + customCss: string; +}) { + const defaultSelectedItem = main.children.find(navItem => { + return navItem.active === true; + }); + + const gotoUrl = evt => { + var element = evt.target; + var url = element.options[element.selectedIndex].value; + appEvents.emit("location-change", { href: url }); + }; + + return ( +
    +
    + ); +} + +function Tabs({ main, customCss }: { main: NavModelItem; customCss: string }) { + return ( +
      {main.children.map(TabItem)}
    + ); +} + +export default class PageHeader extends React.Component { + constructor(props) { + super(props); + } + + renderBreadcrumb(breadcrumbs) { + const breadcrumbsResult = []; + for (let i = 0; i < breadcrumbs.length; i++) { + const bc = breadcrumbs[i]; + if (bc.url) { + breadcrumbsResult.push( + + {bc.title} + + ); + } else { + breadcrumbsResult.push( / {bc.title}); + } + } + return breadcrumbsResult; + } + + renderHeaderTitle(main) { + return ( +
    + + {main.icon && } + {main.img && } + + +
    + {main.text &&

    {main.text}

    } + {main.breadcrumbs && + main.breadcrumbs.length > 0 && ( +

    + {this.renderBreadcrumb(main.breadcrumbs)} +

    + )} + {main.subTitle && ( +
    {main.subTitle}
    + )} + {main.subType && ( +
    + + {main.subType.text} +
    + )} +
    +
    + ); + } + + render() { + const { model } = this.props; + + if (!model) { + return null; + } + + return ( +
    +
    +
    + {this.renderHeaderTitle(model.main)} + {model.main.children && } +
    +
    +
    + ); + } +} diff --git a/public/app/core/components/PasswordStrength.tsx b/public/app/core/components/PasswordStrength.tsx index 1fede3e5cd5..8f92b18445c 100644 --- a/public/app/core/components/PasswordStrength.tsx +++ b/public/app/core/components/PasswordStrength.tsx @@ -11,15 +11,20 @@ export class PasswordStrength extends React.Component { } render() { + const { password } = this.props; let strengthText = "strength: strong like a bull."; let strengthClass = "password-strength-good"; - if (this.props.password.length <= 8) { + if (!password) { + return null; + } + + if (password.length <= 8) { strengthText = "strength: you can do better."; strengthClass = "password-strength-ok"; } - if (this.props.password.length < 4) { + if (password.length < 4) { strengthText = "strength: weak sauce."; strengthClass = "password-strength-bad"; } diff --git a/public/app/core/components/ScrollBar/ScrollBar.tsx b/public/app/core/components/ScrollBar/ScrollBar.tsx new file mode 100644 index 00000000000..49a200b0f3b --- /dev/null +++ b/public/app/core/components/ScrollBar/ScrollBar.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import PerfectScrollbar from 'perfect-scrollbar'; + +export interface Props { + children: any; + className: string; +} + +export default class ScrollBar extends React.Component { + + private container: any; + private ps: PerfectScrollbar; + + constructor(props) { + super(props); + } + + componentDidMount() { + this.ps = new PerfectScrollbar(this.container); + } + + componentDidUpdate() { + this.ps.update(); + } + + componentWillUnmount() { + this.ps.destroy(); + } + + // methods can be invoked by outside + setScrollTop(top) { + if (this.container) { + this.container.scrollTop = top; + this.ps.update(); + + return true; + } + return false; + } + + setScrollLeft(left) { + if (this.container) { + this.container.scrollLeft = left; + this.ps.update(); + + return true; + } + return false; + } + + handleRef = ref => { + this.container = ref; + }; + + render() { + return ( +
    + {this.props.children} +
    + ); + } +} diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 2c2b24c0231..8cbd888bb1b 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -40,10 +40,12 @@ import 'brace/mode/sqlserver'; import 'brace/snippets/sqlserver'; import 'brace/mode/markdown'; import 'brace/snippets/markdown'; +import 'brace/mode/json'; +import 'brace/snippets/json'; -const DEFAULT_THEME_DARK = "ace/theme/grafana-dark"; -const DEFAULT_THEME_LIGHT = "ace/theme/textmate"; -const DEFAULT_MODE = "text"; +const DEFAULT_THEME_DARK = 'ace/theme/grafana-dark'; +const DEFAULT_THEME_LIGHT = 'ace/theme/textmate'; +const DEFAULT_MODE = 'text'; const DEFAULT_MAX_LINES = 10; const DEFAULT_TAB_SIZE = 2; const DEFAULT_BEHAVIOURS = true; @@ -70,7 +72,7 @@ function link(scope, elem, attrs) { behavioursEnabled: behavioursEnabled, highlightActiveLine: false, showPrintMargin: false, - autoScrollEditorIntoView: true // this is needed if editor is inside scrollable page + autoScrollEditorIntoView: true, // this is needed if editor is inside scrollable page }; // Set options @@ -86,12 +88,12 @@ function link(scope, elem, attrs) { setEditorContent(scope.content); // Add classes - elem.addClass("gf-code-editor"); - let textarea = elem.find("textarea"); + elem.addClass('gf-code-editor'); + let textarea = elem.find('textarea'); textarea.addClass('gf-form-input'); if (scope.codeEditorFocus) { - setTimeout(function () { + setTimeout(function() { textarea.focus(); var domEl = textarea[0]; if (domEl.setSelectionRange) { @@ -102,7 +104,7 @@ function link(scope, elem, attrs) { } // Event handlers - editorSession.on('change', (e) => { + editorSession.on('change', e => { scope.$apply(() => { let newValue = codeEditor.getValue(); scope.content = newValue; @@ -123,25 +125,25 @@ function link(scope, elem, attrs) { scope.onChange(); }); - scope.$on("$destroy", () => { + scope.$on('$destroy', () => { codeEditor.destroy(); }); // Keybindings codeEditor.commands.addCommand({ name: 'executeQuery', - bindKey: {win: 'Ctrl-Enter', mac: 'Command-Enter'}, + bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' }, exec: () => { scope.onChange(); - } + }, }); function setLangMode(lang) { - ace.acequire("ace/ext/language_tools"); + ace.acequire('ace/ext/language_tools'); codeEditor.setOptions({ enableBasicAutocompletion: true, enableLiveAutocompletion: true, - enableSnippets: true + enableSnippets: true, }); if (scope.getCompleter()) { @@ -175,13 +177,13 @@ export function codeEditorDirective() { restrict: 'E', template: editorTemplate, scope: { - content: "=", - datasource: "=", - codeEditorFocus: "<", - onChange: "&", - getCompleter: "&" + content: '=', + datasource: '=', + codeEditorFocus: '<', + onChange: '&', + getCompleter: '&', }, - link: link + link: link, }; } diff --git a/public/app/core/components/colorpicker/spectrum_picker.ts b/public/app/core/components/colorpicker/spectrum_picker.ts index 183cebffe2b..6e93a4f39f4 100644 --- a/public/app/core/components/colorpicker/spectrum_picker.ts +++ b/public/app/core/components/colorpicker/spectrum_picker.ts @@ -15,10 +15,10 @@ export function spectrumPicker() { template: '', link: function(scope, element, attrs, ngModel) { scope.ngModel = ngModel; - scope.onColorChange = (color) => { + scope.onColorChange = color => { ngModel.$setViewValue(color); }; - } + }, }; } coreModule.directive('spectrumPicker', spectrumPicker); diff --git a/public/app/core/components/dashboard_selector.ts b/public/app/core/components/dashboard_selector.ts index 7ec9f681520..379fd441a19 100644 --- a/public/app/core/components/dashboard_selector.ts +++ b/public/app/core/components/dashboard_selector.ts @@ -1,5 +1,3 @@ -/// - import coreModule from 'app/core/core_module'; var template = ` @@ -11,15 +9,14 @@ export class DashboardSelectorCtrl { options: any; /** @ngInject */ - constructor(private backendSrv) { - } + constructor(private backendSrv) {} $onInit() { - this.options = [{value: 0, text: 'Default'}]; + this.options = [{ value: 0, text: 'Default' }]; - return this.backendSrv.search({starred: true}).then(res => { + return this.backendSrv.search({ starred: true }).then(res => { res.forEach(dash => { - this.options.push({value: dash.id, text: dash.title}); + this.options.push({ value: dash.id, text: dash.title }); }); }); } @@ -33,8 +30,8 @@ export function dashboardSelector() { controllerAs: 'ctrl', template: template, scope: { - model: '=' - } + model: '=', + }, }; } diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 364d61cfcdb..1fa1dea4338 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -4,8 +4,12 @@ import coreModule from '../../core_module'; function typeaheadMatcher(item) { var str = this.query; - if (str[0] === '/') { str = str.substring(1); } - if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); } + if (str[0] === '/') { + str = str.substring(1); + } + if (str[str.length - 1] === '/') { + str = str.substring(0, str.length - 1); + } return item.toLowerCase().match(str.toLowerCase()); } @@ -35,7 +39,7 @@ export class FormDropdownCtrl { this.cancelBlur = null; // listen to model changes - $scope.$watch("ctrl.model", this.modelChanged.bind(this)); + $scope.$watch('ctrl.model', this.modelChanged.bind(this)); if (this.labelMode) { this.cssClasses = 'gf-form-label ' + this.cssClass; @@ -55,7 +59,7 @@ export class FormDropdownCtrl { // modify typeahead lookup // this = typeahead var typeahead = this.inputElement.data('typeahead'); - typeahead.lookup = function () { + typeahead.lookup = function() { this.query = this.$element.val() || ''; var items = this.source(this.query, $.proxy(this.process, this)); return items ? this.process(items) : items; @@ -80,7 +84,7 @@ export class FormDropdownCtrl { } getOptionsInternal(query) { - var result = this.getOptions({$query: query}); + var result = this.getOptions({ $query: query }); if (this.isPromiseLike(result)) { return result; } @@ -88,7 +92,7 @@ export class FormDropdownCtrl { } isPromiseLike(obj) { - return obj && (typeof obj.then === 'function'); + return obj && typeof obj.then === 'function'; } modelChanged() { @@ -97,8 +101,8 @@ export class FormDropdownCtrl { } else { // if we have text use it if (this.lookupText) { - this.getOptionsInternal("").then(options => { - var item = _.find(options, {value: this.model}); + this.getOptionsInternal('').then(options => { + var item = _.find(options, { value: this.model }); this.updateDisplay(item ? item.text : this.model); }); } else { @@ -140,7 +144,9 @@ export class FormDropdownCtrl { } switchToLink(fromClick) { - if (this.linkMode && !fromClick) { return; } + if (this.linkMode && !fromClick) { + return; + } clearTimeout(this.cancelBlur); this.cancelBlur = null; @@ -164,7 +170,7 @@ export class FormDropdownCtrl { } this.$scope.$apply(() => { - var option = _.find(this.optionCache, {text: text}); + var option = _.find(this.optionCache, { text: text }); if (option) { if (_.isObject(this.model)) { @@ -186,10 +192,9 @@ export class FormDropdownCtrl { // property is synced with outerscope this.$scope.$$postDigest(() => { this.$scope.$apply(() => { - this.onChange({$option: option}); + this.onChange({ $option: option }); }); }); - }); } @@ -199,7 +204,7 @@ export class FormDropdownCtrl { } 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(); @@ -215,7 +220,7 @@ export class FormDropdownCtrl { } } -const template = ` +const template = ` + +import coreModule from 'app/core/core_module'; + +const template = ` +
    + +
    + + +
    +
    +
    +
    +`; + +export function gfPageDirective() { + return { + restrict: 'E', + template: template, + scope: { + model: '=', + }, + transclude: { + header: '?gfPageHeader', + body: 'gfPageBody', + }, + link: function(scope, elem, attrs) { + console.log(scope); + }, + }; +} + +coreModule.directive('gfPage', gfPageDirective); diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 8852da4a436..7124a36cccc 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -1,19 +1,15 @@ -/// - import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; import coreModule from 'app/core/core_module'; -import {profiler} from 'app/core/profiler'; +import { profiler } from 'app/core/profiler'; import appEvents from 'app/core/app_events'; import Drop from 'tether-drop'; export class GrafanaCtrl { - /** @ngInject */ - constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv) { - + constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, globalEventSrv) { $scope.init = function() { $scope.contextSrv = contextSrv; @@ -23,12 +19,13 @@ export class GrafanaCtrl { profiler.init(config, $rootScope); alertSrv.init(); utilSrv.init(); + globalEventSrv.init(); $scope.dashAlerts = alertSrv; }; $scope.initDashboard = function(dashboardData, viewScope) { - $scope.appEvent("dashboard-fetch-end", dashboardData); + $scope.appEvent('dashboard-fetch-end', dashboardData); $controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData); }; @@ -50,13 +47,62 @@ export class GrafanaCtrl { }; $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" + '#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(); @@ -64,43 +110,36 @@ export class GrafanaCtrl { } /** @ngInject */ -export function grafanaAppDirective(playlistSrv, contextSrv) { +export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope) { return { restrict: 'E', controller: GrafanaCtrl, link: (scope, elem) => { - var ignoreSideMenuHide; + var sidemenuOpen; var body = $('body'); // see https://github.com/zenorocha/clipboard.js/issues/155 $.fn.modal.Constructor.prototype.enforceFocus = function() {}; - // handle sidemenu open state - scope.$watch('contextSrv.sidemenu', newVal => { - if (newVal !== undefined) { - body.toggleClass('sidemenu-open', scope.contextSrv.sidemenu); - if (!newVal) { - contextSrv.setPinnedState(false); - } - } - if (contextSrv.sidemenu) { - ignoreSideMenuHide = true; - setTimeout(() => { - ignoreSideMenuHide = false; - }, 300); - } + sidemenuOpen = scope.contextSrv.sidemenu; + body.toggleClass('sidemenu-open', sidemenuOpen); + + appEvents.on('toggle-sidemenu', () => { + body.toggleClass('sidemenu-open'); }); - scope.$watch('contextSrv.pinned', newVal => { - if (newVal !== undefined) { - body.toggleClass('sidemenu-pinned', newVal); - } + appEvents.on('toggle-sidemenu-mobile', () => { + body.toggleClass('sidemenu-open--xs'); + }); + + appEvents.on('toggle-sidemenu-hidden', () => { + body.toggleClass('sidemenu-hidden'); }); // tooltip removal fix // manage page classes var pageClass; - scope.$on("$routeChangeSuccess", function(evt, data) { + scope.$on('$routeChangeSuccess', function(evt, data) { if (pageClass) { body.removeClass(pageClass); } @@ -112,7 +151,10 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { } } - $("#tooltip, .tooltip").remove(); + // clear body class sidemenu states + body.removeClass('sidemenu-open--xs'); + + $('#tooltip, .tooltip').remove(); // check for kiosk url param if (data.params.kiosk) { @@ -134,6 +176,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { var lastActivity = new Date().getTime(); var activeUser = true; var inActiveTimeLimit = 60 * 1000; + var sidemenuHidden = false; function checkForInActiveUser() { if (!activeUser) { @@ -144,9 +187,17 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { return; } - if ((new Date().getTime() - lastActivity) > inActiveTimeLimit) { + if (new Date().getTime() - lastActivity > inActiveTimeLimit) { activeUser = false; body.addClass('user-activity-low'); + // hide sidemenu + if (sidemenuOpen) { + sidemenuHidden = true; + body.removeClass('sidemenu-open'); + $timeout(function() { + $rootScope.$broadcast('render'); + }, 100); + } } } @@ -155,6 +206,15 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { if (!activeUser) { activeUser = true; body.removeClass('user-activity-low'); + + // restore sidemenu + if (sidemenuHidden) { + sidemenuHidden = false; + body.addClass('sidemenu-open'); + $timeout(function() { + $rootScope.$broadcast('render'); + }, 100); + } } } @@ -190,7 +250,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { }, 100); } - if (target.parents('.dash-playlist-actions').length === 0) { + if (target.parents('.navbar-buttons--playlist').length === 0) { playlistSrv.stop(); } @@ -203,30 +263,13 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { } } - // hide menus - var openMenus = body.find('.navbar-page-btn--open'); - if (openMenus.length > 0) { - if (target.parents('.navbar-page-btn--open').length === 0) { - openMenus.removeClass('navbar-page-btn--open'); - } - } - - // hide sidemenu - if (!ignoreSideMenuHide && !contextSrv.pinned && body.find('.sidemenu').length > 0) { - if (target.parents('.sidemenu').length === 0) { - scope.$apply(function() { - scope.contextSrv.toggleSideMenu(); - }); - } - } - // hide popovers var popover = elem.find('.popover'); if (popover.length > 0 && target.parents('.graph-legend').length === 0) { popover.hide(); } }); - } + }, }; } diff --git a/public/app/core/components/help/help.html b/public/app/core/components/help/help.html index c07d57a0ffc..45faa560e40 100644 --- a/public/app/core/components/help/help.html +++ b/public/app/core/components/help/help.html @@ -4,15 +4,6 @@ Shortcuts - - - - - - - - - diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts index bf84b43b5f9..a594b95bb4d 100644 --- a/public/app/core/components/help/help.ts +++ b/public/app/core/components/help/help.ts @@ -11,39 +11,45 @@ export class HelpCtrl { constructor() { this.tabIndex = 0; this.shortcuts = { - 'Global': [ - {keys: ['g', 'h'], description: 'Go to Home Dashboard'}, - {keys: ['g', 'p'], description: 'Go to Profile'}, - {keys: ['s', 'o'], description: 'Open search'}, - {keys: ['s', 's'], description: 'Open search with starred filter'}, - {keys: ['s', 't'], description: 'Open search in tags view'}, - {keys: ['esc'], description: 'Exit edit/setting views'}, + Global: [ + { keys: ['g', 'h'], description: 'Go to Home Dashboard' }, + { keys: ['g', 'p'], description: 'Go to Profile' }, + { keys: ['s', 'o'], description: 'Open search' }, + { keys: ['s', 's'], description: 'Open search with starred filter' }, + { keys: ['s', 't'], description: 'Open search in tags view' }, + { keys: ['esc'], description: 'Exit edit/setting views' }, ], - 'Dashboard': [ - {keys: ['mod+s'], description: 'Save dashboard'}, - {keys: ['mod+h'], description: 'Hide row controls'}, - {keys: ['d', 'r'], description: 'Refresh all panels'}, - {keys: ['d', 's'], description: 'Dashboard settings'}, - {keys: ['d', 'v'], description: 'Toggle in-active / view mode'}, - {keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)'}, - {keys: ['d', 'E'], description: 'Expand all rows'}, - {keys: ['d', 'C'], description: 'Collapse all rows'}, - {keys: ['mod+o'], description: 'Toggle shared graph crosshair'}, + Dashboard: [ + { keys: ['mod+s'], description: 'Save dashboard' }, + { keys: ['mod+h'], description: 'Hide row controls' }, + { keys: ['d', 'r'], description: 'Refresh all panels' }, + { keys: ['d', 's'], description: 'Dashboard settings' }, + { keys: ['d', 'v'], description: 'Toggle in-active / view mode' }, + { keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)' }, + { keys: ['d', 'E'], description: 'Expand all rows' }, + { keys: ['d', 'C'], description: 'Collapse all rows' }, + { keys: ['mod+o'], description: 'Toggle shared graph crosshair' }, ], 'Focused Panel': [ - {keys: ['e'], description: 'Toggle panel edit view'}, - {keys: ['v'], description: 'Toggle panel fullscreen view'}, - {keys: ['p', 's'], description: 'Open Panel Share Modal'}, - {keys: ['p', 'r'], description: 'Remove Panel'}, + { keys: ['e'], description: 'Toggle panel edit view' }, + { keys: ['v'], description: 'Toggle panel fullscreen view' }, + { keys: ['p', 's'], description: 'Open Panel Share Modal' }, + { keys: ['p', 'r'], description: 'Remove Panel' }, ], 'Focused Row': [ - {keys: ['r', 'c'], description: 'Collapse Row'}, - {keys: ['r', 'r'], description: 'Remove Row'}, + { keys: ['r', 'c'], description: 'Collapse Row' }, + { keys: ['r', 'r'], description: 'Remove Row' }, ], 'Time Range': [ - {keys: ['t', 'z'], description: 'Zoom out time range'}, - {keys: ['t', ''], description: 'Move time range back'}, - {keys: ['t', ''], description: 'Move time range forward'}, + { keys: ['t', 'z'], description: 'Zoom out time range' }, + { + keys: ['t', ''], + description: 'Move time range back', + }, + { + keys: ['t', ''], + description: 'Move time range forward', + }, ], }; } diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index 954e84a3baa..2701c7b6983 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -10,10 +10,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 +26,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) => { + _.each(clone, node => { content.appendChild(node); }); - var drop = new Drop({ + let dropOptions = { target: elem[0], content: content, position: position, @@ -43,22 +43,26 @@ export function infoPopover() { tetherOptions: { offset: offset, constraints: [ - { - to: 'window', - attachment: 'together', - pin: true - } - ], - } - }); + { + to: 'window', + attachment: 'together', + pin: true, + }, + ], + }, + }; - 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 7c4429d7c76..5b053792d73 100644 --- a/public/app/core/components/json_explorer/helpers.ts +++ b/public/app/core/components/json_explorer/helpers.ts @@ -5,7 +5,7 @@ * Escapes `"` charachters from string */ function escapeString(str: string): string { - return str.replace('"', '\"'); + return str.replace('"', '"'); } /* @@ -13,7 +13,7 @@ function escapeString(str: string): string { */ export function isObject(value: any): boolean { var type = typeof value; - return !!value && (type === 'object'); + return !!value && type === 'object'; } /* @@ -29,11 +29,11 @@ export function getObjectName(object: Object): string { return 'Object'; } if (typeof object === 'object' && !object.constructor) { - return 'Object'; + return 'Object'; } const funcNameRegex = /function ([^(]*)/; - const results = (funcNameRegex).exec((object).constructor.toString()); + const results = funcNameRegex.exec(object.constructor.toString()); if (results && results.length > 1) { return results[1]; } else { @@ -45,27 +45,33 @@ export function getObjectName(object: Object): string { * Gets type of an object. Returns "null" for null objects */ export function getType(object: Object): string { - if (object === null) { return 'null'; } + if (object === null) { + return 'null'; + } return typeof object; } /* * Generates inline preview for a JavaScript object based on a value */ -export function getValuePreview (object: Object, value: string): string { +export function getValuePreview(object: Object, value: string): string { var type = getType(object); - if (type === 'null' || type === 'undefined') { return type; } + if (type === 'null' || type === 'undefined') { + return type; + } if (type === 'string') { value = '"' + escapeString(value) + '"'; } if (type === 'function') { - // Remove content of the function - return object.toString() + return ( + object + .toString() .replace(/[\r\n]/g, '') - .replace(/\{.*\}/, '') + '{…}'; + .replace(/\{.*\}/, '') + '{…}' + ); } return value; } @@ -97,7 +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 a7079477abc..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'; @@ -19,7 +12,12 @@ const JSON_DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; // When toggleing, don't animated removal or addition of more than a few items const MAX_ANIMATED_TOGGLE_ITEMS = 10; -const requestAnimationFrame = window.requestAnimationFrame || function(cb: ()=>void) { cb(); return 0; }; +const requestAnimationFrame = + window.requestAnimationFrame || + function(cb: () => void) { + cb(); + return 0; + }; export interface JsonExplorerConfig { animateOpen?: boolean; @@ -30,18 +28,16 @@ export interface JsonExplorerConfig { const _defaultConfig: JsonExplorerConfig = { animateOpen: true, animateClose: true, - theme: null + theme: null, }; - /** * @class JsonExplorer * * JsonExplorer allows you to render JSON objects in HTML with a * **collapsible** navigation. -*/ + */ export class JsonExplorer { - // Hold the open state after the toggler is used private _isOpen: boolean = null; @@ -77,9 +73,13 @@ export class JsonExplorer { * * @param {string} [key=undefined] The key that this object in it's parent * context - */ - constructor(public json: any, private open = 1, private config: JsonExplorerConfig = _defaultConfig, private key?: string) { - } + */ + constructor( + public json: any, + private open = 1, + private config: JsonExplorerConfig = _defaultConfig, + private key?: string + ) {} /* * is formatter open? @@ -103,17 +103,17 @@ export class JsonExplorer { * is this a date string? */ 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)); + return ( + this.type === 'string' && + (DATE_STRING_REGEX.test(this.json) || JSON_DATE_REGEX.test(this.json) || PARTIAL_DATE_REGEX.test(this.json)) + ); } /* * is this a URL string? */ private get isUrl(): boolean { - return this.type === 'string' && (this.json.indexOf('http') === 0); + return this.type === 'string' && this.json.indexOf('http') === 0; } /* @@ -174,7 +174,7 @@ export class JsonExplorer { */ private get keys(): string[] { if (this.isObject) { - return Object.keys(this.json).map((key)=> key ? key : '""'); + return Object.keys(this.json).map(key => (key ? key : '""')); } else { return []; } @@ -183,7 +183,7 @@ export class JsonExplorer { /** * Toggles `isOpen` state * - */ + */ toggleOpen() { this.isOpen = !this.isOpen; @@ -198,17 +198,17 @@ export class JsonExplorer { } /** - * Open all children up to a certain depth. - * Allows actions such as expand all/collapse all - * - */ + * Open all children up to a certain depth. + * Allows actions such as expand all/collapse all + * + */ openAtDepth(depth = 1) { if (depth < 0) { return; } this.open = depth; - this.isOpen = (depth !== 0); + this.isOpen = depth !== 0; if (this.element) { this.removeChildren(false); @@ -223,8 +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() { @@ -241,7 +240,7 @@ export class JsonExplorer { }); 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', ']')); @@ -294,7 +293,6 @@ export class JsonExplorer { togglerLink.appendChild(value); // Primitive values } else { - // make a value holder element const value = this.isUrl ? createElement('a') : createElement('span'); @@ -366,15 +364,17 @@ export class JsonExplorer { /** * Appends all the children to children element * Animated option is used when user triggers this via a click - */ + */ appendChildren(animated = false) { const children = this.element.querySelector(`div.${cssClass('children')}`); - if (!children || this.isEmpty) { return; } + if (!children || this.isEmpty) { + return; + } if (animated) { let index = 0; - const addAChild = ()=> { + const addAChild = () => { const key = this.keys[index]; const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key); children.appendChild(formatter.render()); @@ -391,7 +391,6 @@ export class JsonExplorer { }; requestAnimationFrame(addAChild); - } else { this.keys.forEach(key => { const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key); @@ -403,13 +402,13 @@ export class JsonExplorer { /** * Removes all the children from children element * Animated option is used when user triggers this via a click - */ + */ removeChildren(animated = false) { const childrenElement = this.element.querySelector(`div.${cssClass('children')}`) as HTMLDivElement; if (animated) { let childrenRemoved = 0; - const removeAChild = ()=> { + const removeAChild = () => { if (childrenElement && childrenElement.children.length) { childrenElement.removeChild(childrenElement.children[0]); childrenRemoved += 1; diff --git a/public/app/core/components/jsontree/jsontree.ts b/public/app/core/components/jsontree/jsontree.ts index 52fb64e1c87..e127d7b14a9 100644 --- a/public/app/core/components/jsontree/jsontree.ts +++ b/public/app/core/components/jsontree/jsontree.ts @@ -1,22 +1,23 @@ import coreModule from 'app/core/core_module'; -import {JsonExplorer} from '../json_explorer/json_explorer'; +import { JsonExplorer } from '../json_explorer/json_explorer'; -coreModule.directive('jsonTree', [function jsonTreeDirective() { - return{ - restrict: 'E', - scope: { - object: '=', - startExpanded: '@', - rootName: '@', - }, - link: function(scope, elem) { +coreModule.directive('jsonTree', [ + function jsonTreeDirective() { + return { + restrict: 'E', + scope: { + object: '=', + startExpanded: '@', + rootName: '@', + }, + link: function(scope, elem) { + var jsonExp = new JsonExplorer(scope.object, 3, { + animateOpen: true, + }); - var jsonExp = new JsonExplorer(scope.object, 3, { - animateOpen: true - }); - - const html = jsonExp.render(true); - elem.html(html); - } - }; -}]); + const html = jsonExp.render(true); + elem.html(html); + }, + }; + }, +]); diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts index 98f806cd63e..91a3afea250 100644 --- a/public/app/core/components/layout_selector/layout_selector.ts +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -31,7 +31,6 @@ export class LayoutSelectorCtrl { store.set('grafana.list.layout.mode', 'grid'); this.$rootScope.appEvent('layout-mode-changed', 'grid'); } - } /** @ngInject **/ @@ -56,12 +55,16 @@ export function layoutMode($rootScope) { var className = 'card-list-layout-' + layout; elem.addClass(className); - $rootScope.onAppEvent('layout-mode-changed', (evt, newLayout) => { - elem.removeClass(className); - className = 'card-list-layout-' + newLayout; - elem.addClass(className); - }, scope); - } + $rootScope.onAppEvent( + 'layout-mode-changed', + (evt, newLayout) => { + elem.removeClass(className); + className = 'card-list-layout-' + newLayout; + elem.addClass(className); + }, + scope + ); + }, }; } diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html new file mode 100644 index 00000000000..e9d7d743063 --- /dev/null +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -0,0 +1,118 @@ +
    + + +
    +
    +
    + + +
    + + +
    +
    + +
    + + No dashboards matching your query were found. + +
    + +
    +
    + +
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts new file mode 100644 index 00000000000..011e020f588 --- /dev/null +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -0,0 +1,302 @@ +import _ from 'lodash'; +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; +import { SearchSrv } from 'app/core/services/search_srv'; + +export class ManageDashboardsCtrl { + public sections: any[]; + tagFilterOptions: any[]; + selectedTagFilter: any; + query: any; + navModel: any; + canDelete = false; + canMove = false; + hasFilters = false; + selectAllChecked = false; + starredFilterOptions = [{ text: 'Filter by Starred', disabled: true }, { text: 'Yes' }, { text: 'No' }]; + selectedStarredFilter: any; + folderId?: number; + + /** @ngInject */ + constructor(private backendSrv, navModelSrv, private searchSrv: SearchSrv) { + this.query = { + query: '', + mode: 'tree', + tag: [], + starred: false, + skipRecent: true, + skipStarred: true, + }; + + if (this.folderId) { + this.query.folderIds = [this.folderId]; + } + + this.selectedStarredFilter = this.starredFilterOptions[0]; + + this.getDashboards().then(() => { + this.getTags(); + }); + } + + getDashboards() { + return this.searchSrv.search(this.query).then(result => { + return this.initDashboardList(result); + }); + } + + initDashboardList(result: any) { + this.canMove = false; + this.canDelete = false; + this.selectAllChecked = false; + this.hasFilters = this.query.query.length > 0 || this.query.tag.length > 0 || this.query.starred; + + if (!result) { + this.sections = []; + return; + } + + this.sections = result; + + for (let section of this.sections) { + section.checked = false; + + for (let dashboard of section.items) { + dashboard.checked = false; + } + } + + if (this.folderId && this.sections.length > 0) { + this.sections[0].hideHeader = true; + } + } + + selectionChanged() { + let selectedDashboards = 0; + + for (let section of this.sections) { + selectedDashboards += _.filter(section.items, { checked: true }).length; + } + + const selectedFolders = _.filter(this.sections, { checked: true }).length; + this.canMove = selectedDashboards > 0; + this.canDelete = selectedDashboards > 0 || selectedFolders > 0; + } + + getFoldersAndDashboardsToDelete() { + let selectedDashboards = { + folders: [], + dashboards: [], + }; + + for (const section of this.sections) { + if (section.checked && section.id !== 0) { + selectedDashboards.folders.push(section.slug); + } else { + const selected = _.filter(section.items, { checked: true }); + selectedDashboards.dashboards.push(..._.map(selected, 'slug')); + } + } + + return selectedDashboards; + } + + getFolderIds(sections) { + const ids = []; + for (let s of sections) { + if (s.checked) { + ids.push(s.id); + } + } + return ids; + } + + delete() { + const data = this.getFoldersAndDashboardsToDelete(); + const folderCount = data.folders.length; + const dashCount = data.dashboards.length; + let text = 'Do you want to delete the '; + 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`; + } else if (folderCount > 0) { + text += `selected folder${folderCount === 1 ? '' : 's'} and all its dashboards?`; + } else { + text += `selected dashboard${dashCount === 1 ? '' : 's'}?`; + } + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: text, + text2: text2, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + const foldersAndDashboards = data.folders.concat(data.dashboards); + this.deleteFoldersAndDashboards(foldersAndDashboards); + }, + }); + } + + private deleteFoldersAndDashboards(slugs) { + this.backendSrv.deleteDashboards(slugs).then(result => { + const folders = _.filter(result, dash => dash.meta.isFolder); + const folderCount = folders.length; + const dashboards = _.filter(result, dash => !dash.meta.isFolder); + const dashCount = dashboards.length; + + if (result.length > 0) { + let header; + let msg; + + if (folderCount > 0 && dashCount > 0) { + 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`; + } 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`; + } + } else if (dashCount > 0) { + header = `Dashboard${dashCount === 1 ? '' : 's'} Deleted`; + + if (dashCount === 1) { + msg = `${dashboards[0].dashboard.title} has been deleted`; + } else { + msg = `${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`; + } + } + + appEvents.emit('alert-success', [header, msg]); + } + + this.getDashboards(); + }); + } + + getDashboardsToMove() { + let selectedDashboards = []; + + for (const section of this.sections) { + const selected = _.filter(section.items, { checked: true }); + selectedDashboards.push(..._.map(selected, 'slug')); + } + + return selectedDashboards; + } + + moveTo() { + const selectedDashboards = this.getDashboardsToMove(); + + const template = + '' + + '`'; + appEvents.emit('show-modal', { + templateHtml: template, + modalClass: 'modal--narrow', + model: { + dashboards: selectedDashboards, + afterSave: this.getDashboards.bind(this), + }, + }); + } + + getTags() { + return this.searchSrv.getDashboardTags().then(results => { + this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results); + this.selectedTagFilter = this.tagFilterOptions[0]; + }); + } + + filterByTag(tag) { + if (_.indexOf(this.query.tag, tag) === -1) { + this.query.tag.push(tag); + } + + return this.getDashboards(); + } + + onQueryChange() { + return this.getDashboards(); + } + + onTagFilterChange() { + var res = this.filterByTag(this.selectedTagFilter.term); + this.selectedTagFilter = this.tagFilterOptions[0]; + return res; + } + + removeTag(tag, evt) { + this.query.tag = _.without(this.query.tag, tag); + this.getDashboards(); + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + } + + removeStarred() { + this.query.starred = false; + return this.getDashboards(); + } + + onStarredFilterChange() { + this.query.starred = this.selectedStarredFilter.text === 'Yes'; + this.selectedStarredFilter = this.starredFilterOptions[0]; + return this.getDashboards(); + } + + onSelectAllChanged() { + for (let section of this.sections) { + if (!section.hideHeader) { + section.checked = this.selectAllChecked; + } + + section.items = _.map(section.items, item => { + item.checked = this.selectAllChecked; + return item; + }); + } + + this.selectionChanged(); + } + + clearFilters() { + this.query.query = ''; + this.query.tag = []; + 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', + controller: ManageDashboardsCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + folderId: '=', + }, + }; +} + +coreModule.directive('manageDashboards', manageDashboardsDirective); diff --git a/public/app/core/components/navbar/navbar.html b/public/app/core/components/navbar/navbar.html index e160d3b3eed..6d611692efc 100644 --- a/public/app/core/components/navbar/navbar.html +++ b/public/app/core/components/navbar/navbar.html @@ -1,43 +1,12 @@ -
    diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index 94002677de6..0276a7331b3 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -1,105 +1,64 @@ -/// - +import _ from 'lodash'; import config from 'app/core/config'; import $ from 'jquery'; import coreModule from '../../core_module'; +import appEvents from 'app/core/app_events'; export class SideMenuCtrl { - isSignedIn: boolean; - showSignout: boolean; user: any; mainLinks: any; - orgMenu: any; - appSubUrl: string; + bottomNav: any; loginUrl: string; - orgFilter: string; - orgItems: any; - orgs: any; - maxShownOrgs: number; + isSignedIn: boolean; + isOpenMobile: boolean; /** @ngInject */ - constructor(private $scope, private $location, private contextSrv, private backendSrv) { + constructor(private $scope, private $rootScope, private $location, private contextSrv, private $timeout) { this.isSignedIn = contextSrv.isSignedIn; this.user = contextSrv.user; - this.appSubUrl = config.appSubUrl; - this.showSignout = this.contextSrv.isSignedIn && !config['disableSignoutMenu']; - this.maxShownOrgs = 10; - - this.mainLinks = config.bootData.mainNavLinks; - this.openUserDropdown(); + 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()); - this.$scope.$on('$routeChangeSuccess', () => { - if (!this.contextSrv.pinned) { - this.contextSrv.sidemenu = false; + if (contextSrv.user.orgCount > 1) { + let profileNode = _.find(this.bottomNav, { id: 'profile' }); + if (profileNode) { + profileNode.showOrgSwitcher = true; } + } + + this.$scope.$on('$routeChangeSuccess', () => { this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path()); }); - - this.orgFilter = ''; } - getUrl(url) { - return config.appSubUrl + url; - } + toggleSideMenu() { + this.contextSrv.toggleSideMenu(); + appEvents.emit('toggle-sidemenu'); - openUserDropdown() { - this.orgMenu = [ - {section: 'You', cssClass: 'dropdown-menu-title'}, - {text: 'Profile', url: this.getUrl('/profile')}, - ]; + this.$timeout(() => { + this.$rootScope.$broadcast('render'); + }); + } - if (this.showSignout) { - this.orgMenu.push({text: "Sign out", url: this.getUrl("/logout"), target: "_self"}); - } + toggleSideMenuSmallBreakpoint() { + appEvents.emit('toggle-sidemenu-mobile'); + } - if (this.contextSrv.hasRole('Admin')) { - this.orgMenu.push({section: this.user.orgName, cssClass: 'dropdown-menu-title'}); - this.orgMenu.push({ - text: "Preferences", - url: this.getUrl("/org") - }); - this.orgMenu.push({ - text: "Users", - url: this.getUrl("/org/users") - }); - this.orgMenu.push({ - text: "API Keys", - url: this.getUrl("/org/apikeys") - }); - } + switchOrg() { + this.$rootScope.appEvent('show-modal', { + templateHtml: '', + }); + } - this.orgMenu.push({cssClass: "divider"}); - this.backendSrv.get('/api/user/orgs').then(orgs => { - this.orgs = orgs; - this.loadOrgsItems(); - }); - } - - loadOrgsItems() { - this.orgItems = []; - this.orgs.forEach(org => { - if (org.orgId === this.contextSrv.user.orgId) { - return; - } - - if (this.orgItems.length === this.maxShownOrgs) { - return; - } - - if (this.orgFilter === '' || (org.name.toLowerCase().indexOf(this.orgFilter.toLowerCase()) !== -1)) { - this.orgItems.push({ - text: "Switch to " + org.name, - icon: "fa fa-fw fa-random", - url: this.getUrl('/profile/switch-org/' + org.orgId), - target: '_self' - }); - } - }); - if (config.allowOrgCreate) { - this.orgItems.push({text: "New organization", icon: "fa fa-fw fa-plus", url: this.getUrl('/org/new')}); - } - } + itemClicked(item, evt) { + if (item.url === '/shortcuts') { + appEvents.emit('show-modal', { + templateHtml: '', + }); + evt.preventDefault(); + } + } } export function sideMenuDirective() { @@ -121,11 +80,7 @@ export function sideMenuDirective() { parent.append(menu); }, 100); }); - - scope.$on("$destory", function() { - elem.off('click.dropdown'); - }); - } + }, }; } diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts index f7a2658a0ff..3203a42cd70 100644 --- a/public/app/core/components/switch.ts +++ b/public/app/core/components/switch.ts @@ -33,7 +33,6 @@ export class SwitchCtrl { return this.onChange(); }); } - } export function switchDirective() { @@ -43,12 +42,12 @@ export function switchDirective() { controllerAs: 'ctrl', bindToController: true, scope: { - checked: "=", - label: "@", - labelClass: "@", - tooltip: "@", - switchClass: "@", - onChange: "&", + checked: '=', + label: '@', + labelClass: '@', + tooltip: '@', + switchClass: '@', + onChange: '&', }, template: template, }; diff --git a/public/app/core/components/team_picker.ts b/public/app/core/components/team_picker.ts new file mode 100644 index 00000000000..228767a76c4 --- /dev/null +++ b/public/app/core/components/team_picker.ts @@ -0,0 +1,64 @@ +import coreModule from 'app/core/core_module'; +import _ from 'lodash'; + +const template = ` + +`; +export class TeamPickerCtrl { + group: any; + teamPicked: any; + debouncedSearchGroups: any; + + /** @ngInject */ + constructor(private backendSrv) { + this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, { + leading: true, + trailing: false, + }); + this.reset(); + } + + reset() { + this.group = { text: 'Choose', value: null }; + } + + 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 }; + }); + }) + ); + } + + onChange(option) { + this.teamPicked({ $group: option.value }); + } +} + +export function teamPicker() { + return { + restrict: 'E', + template: template, + controller: TeamPickerCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + teamPicked: '&', + }, + link: function(scope, elem, attrs, ctrl) { + scope.$on('team-picker-reset', () => { + ctrl.reset(); + }); + }, + }; +} + +coreModule.directive('teamPicker', teamPicker); diff --git a/public/app/core/components/user_picker.ts b/public/app/core/components/user_picker.ts new file mode 100644 index 00000000000..606ded09885 --- /dev/null +++ b/public/app/core/components/user_picker.ts @@ -0,0 +1,71 @@ +import coreModule from 'app/core/core_module'; +import _ from 'lodash'; + +const template = ` + +`; +export class UserPickerCtrl { + user: any; + debouncedSearchUsers: any; + userPicked: any; + + /** @ngInject */ + constructor(private backendSrv) { + this.reset(); + this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, { + leading: true, + trailing: false, + }); + } + + 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 }; + }); + }) + ); + } + + onChange(option) { + this.userPicked({ $user: option.value }); + } + + reset() { + this.user = { text: 'Choose', value: null }; + } +} + +export interface User { + id: number; + name: string; + login: string; + email: string; +} + +export function userPicker() { + return { + restrict: 'E', + template: template, + controller: UserPickerCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + userPicked: '&', + }, + link: function(scope, elem, attrs, ctrl) { + scope.$on('user-picker-reset', () => { + ctrl.reset(); + }); + }, + }; +} + +coreModule.directive('userPicker', userPicker); diff --git a/public/app/core/config.ts b/public/app/core/config.ts index e54d62d7c0e..91b1cfef3a4 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -1,39 +1,39 @@ import _ from 'lodash'; class Settings { - datasources: any; - panels: any; - appSubUrl: string; - window_title_prefix: string; - buildInfo: any; - new_panel_title: string; - bootData: any; - externalUserMngLinkUrl: string; - externalUserMngLinkName: string; - externalUserMngInfo: string; - allowOrgCreate: boolean; - disableLoginForm: boolean; - defaultDatasource: string; - alertingEnabled: boolean; - authProxyEnabled: boolean; - ldapEnabled: boolean; - oauth: any; - disableUserSignUp: boolean; - loginHint: any; - loginError: any; + datasources: any; + panels: any; + appSubUrl: string; + window_title_prefix: string; + buildInfo: any; + new_panel_title: string; + bootData: any; + externalUserMngLinkUrl: string; + externalUserMngLinkName: string; + externalUserMngInfo: string; + allowOrgCreate: boolean; + disableLoginForm: boolean; + defaultDatasource: string; + alertingEnabled: boolean; + authProxyEnabled: boolean; + ldapEnabled: boolean; + oauth: any; + disableUserSignUp: boolean; + loginHint: any; + loginError: any; - constructor(options) { - var defaults = { - datasources: {}, - window_title_prefix: 'Grafana - ', - panels: {}, - new_panel_title: 'Panel Title', - playlist_timespan: "1m", - unsaved_changes_warning: true, - appSubUrl: "" - }; - _.extend(this, defaults, options); - } + constructor(options) { + var defaults = { + datasources: {}, + window_title_prefix: 'Grafana - ', + panels: {}, + new_panel_title: 'Panel Title', + playlist_timespan: '1m', + unsaved_changes_warning: true, + appSubUrl: '', + }; + _.extend(this, defaults, options); + } } var bootData = (window).grafanaBootData || { settings: {} }; diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts new file mode 100644 index 00000000000..2642c5e400a --- /dev/null +++ b/public/app/core/constants.ts @@ -0,0 +1,10 @@ +export const GRID_CELL_HEIGHT = 30; +export const GRID_CELL_VMARGIN = 10; +export const GRID_COLUMN_COUNT = 24; +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/error_ctrl.ts b/public/app/core/controllers/error_ctrl.ts index fe894a69806..a47cae9fea3 100644 --- a/public/app/core/controllers/error_ctrl.ts +++ b/public/app/core/controllers/error_ctrl.ts @@ -1,18 +1,21 @@ import config from 'app/core/config'; import coreModule from '../core_module'; +import appEvents from 'app/core/app_events'; export class ErrorCtrl { - /** @ngInject */ constructor($scope, contextSrv, navModelSrv) { $scope.navModel = navModelSrv.getNotFoundNav(); $scope.appSubUrl = config.appSubUrl; - var showSideMenu = contextSrv.sidemenu; - contextSrv.sidemenu = false; + if (!contextSrv.isSignedIn) { + appEvents.emit('toggle-sidemenu-hidden'); + } - $scope.$on('$destroy', function() { - contextSrv.sidemenu = showSideMenu; + $scope.$on('destroy', () => { + if (!contextSrv.isSignedIn) { + appEvents.emit('toggle-sidemenu-hidden'); + } }); } } diff --git a/public/app/core/controllers/inspect_ctrl.ts b/public/app/core/controllers/inspect_ctrl.ts index 70516986099..5dd4cb3d06f 100644 --- a/public/app/core/controllers/inspect_ctrl.ts +++ b/public/app/core/controllers/inspect_ctrl.ts @@ -4,20 +4,19 @@ import $ from 'jquery'; import coreModule from '../core_module'; export class InspectCtrl { - /** @ngInject */ constructor($scope, $sanitize) { var model = $scope.inspector; - $scope.init = function () { + $scope.init = function() { $scope.editor = { index: 0 }; - if (!model.error) { + if (!model.error) { return; } if (_.isString(model.error.data)) { - $scope.response = $("
    " + model.error.data + "
    ").text(); + $scope.response = $('
    ' + model.error.data + '
    ').text(); } else if (model.error.data) { if (model.error.data.response) { $scope.response = $sanitize(model.error.data.response); @@ -30,7 +29,7 @@ export class InspectCtrl { if (model.error.config && model.error.config.params) { $scope.request_parameters = _.map(model.error.config.params, function(value, key) { - return { key: key, value: value}; + return { key: key, value: value }; }); } @@ -45,9 +44,9 @@ export class InspectCtrl { if (_.isString(model.error.config.data)) { $scope.request_parameters = this.getParametersFromQueryString(model.error.config.data); - } else { + } else { $scope.request_parameters = _.map(model.error.config.data, function(value, key) { - return {key: key, value: angular.toJson(value, true)}; + return { key: key, value: angular.toJson(value, true) }; }); } } @@ -55,11 +54,14 @@ export class InspectCtrl { } getParametersFromQueryString(queryString) { var result = []; - var parameters = queryString.split("&"); + var parameters = queryString.split('&'); for (var i = 0; i < parameters.length; i++) { - var keyValue = parameters[i].split("="); + var keyValue = parameters[i].split('='); if (keyValue[1].length > 0) { - result.push({ key: keyValue[0], value: (window).unescape(keyValue[1]) }); + result.push({ + key: keyValue[0], + value: (window).unescape(keyValue[1]), + }); } } return result; diff --git a/public/app/core/controllers/invited_ctrl.ts b/public/app/core/controllers/invited_ctrl.ts index ed4bd1793b8..e88c810b557 100644 --- a/public/app/core/controllers/invited_ctrl.ts +++ b/public/app/core/controllers/invited_ctrl.ts @@ -2,18 +2,25 @@ import coreModule from '../core_module'; import config from 'app/core/config'; export class InvitedCtrl { - /** @ngInject */ constructor($scope, $routeParams, contextSrv, backendSrv) { contextSrv.sidemenu = false; $scope.formModel = {}; + $scope.navModel = { + main: { + icon: 'gicon gicon-branding', + subTitle: 'Register your Grafana account', + breadcrumbs: [{ title: 'Login', url: '/login' }, { title: 'Invite' }], + }, + }; + $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; + $scope.formModel.inviteCode = $routeParams.code; $scope.greeting = invite.name || invite.email || invite.username; $scope.invitedBy = invite.invitedBy; diff --git a/public/app/core/controllers/json_editor_ctrl.ts b/public/app/core/controllers/json_editor_ctrl.ts index ba6d9abfd74..d369fe8b3c0 100644 --- a/public/app/core/controllers/json_editor_ctrl.ts +++ b/public/app/core/controllers/json_editor_ctrl.ts @@ -2,16 +2,18 @@ import angular from 'angular'; import coreModule from '../core_module'; export class JsonEditorCtrl { - /** @ngInject */ constructor($scope) { $scope.json = angular.toJson($scope.object, true); $scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor; + $scope.canCopy = $scope.enableCopy; - $scope.update = function () { + $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 11bebbce8e6..313fc2efa1a 100644 --- a/public/app/core/controllers/login_ctrl.ts +++ b/public/app/core/controllers/login_ctrl.ts @@ -3,7 +3,6 @@ import coreModule from '../core_module'; import config from 'app/core/config'; export class LoginCtrl { - /** @ngInject */ constructor($scope, backendSrv, contextSrv, $location) { $scope.formModel = { @@ -19,13 +18,13 @@ export class LoginCtrl { $scope.disableLoginForm = config.disableLoginForm; $scope.disableUserSignUp = config.disableUserSignUp; - $scope.loginHint = config.loginHint; + $scope.loginHint = config.loginHint; $scope.loginMode = true; $scope.submitBtnText = 'Log in'; $scope.init = function() { - $scope.$watch("loginMode", $scope.loginModeChanged); + $scope.$watch('loginMode', $scope.loginModeChanged); if (config.loginError) { $scope.appEvent('alert-warning', ['Login Failed', config.loginError]); @@ -51,7 +50,7 @@ export class LoginCtrl { backendSrv.post('/api/user/signup', $scope.formModel).then(function(result) { if (result.status === 'SignUpCreated') { - $location.path('/signup').search({email: $scope.formModel.email}); + $location.path('/signup').search({ email: $scope.formModel.email }); } else { window.location.href = config.appSubUrl + '/'; } diff --git a/public/app/core/controllers/reset_password_ctrl.ts b/public/app/core/controllers/reset_password_ctrl.ts index 524cfb7af64..38bd51d2933 100644 --- a/public/app/core/controllers/reset_password_ctrl.ts +++ b/public/app/core/controllers/reset_password_ctrl.ts @@ -1,7 +1,6 @@ import coreModule from '../core_module'; export class ResetPasswordCtrl { - /** @ngInject */ constructor($scope, contextSrv, backendSrv, $location) { contextSrv.sidemenu = false; @@ -14,6 +13,14 @@ export class ResetPasswordCtrl { $scope.formModel.code = params.code; } + $scope.navModel = { + main: { + icon: 'gicon gicon-branding', + subTitle: 'Reset your Grafana password', + breadcrumbs: [{ title: 'Login', url: '/login' }, { title: 'Reset Password' }], + }, + }; + $scope.sendResetEmail = function() { if (!$scope.sendResetForm.$valid) { return; @@ -24,7 +31,9 @@ export class ResetPasswordCtrl { }; $scope.submitReset = function() { - if (!$scope.resetForm.$valid) { return; } + if (!$scope.resetForm.$valid) { + return; + } if ($scope.formModel.newPassword !== $scope.formModel.confirmPassword) { $scope.appEvent('alert-warning', ['New passwords do not match', '']); diff --git a/public/app/core/controllers/signup_ctrl.ts b/public/app/core/controllers/signup_ctrl.ts index eb54110eb6d..3b4fd6a4a9e 100644 --- a/public/app/core/controllers/signup_ctrl.ts +++ b/public/app/core/controllers/signup_ctrl.ts @@ -4,14 +4,8 @@ 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; @@ -26,13 +20,21 @@ export class SignUpCtrl { $scope.verifyEmailEnabled = false; $scope.autoAssignOrg = false; + $scope.navModel = { + main: { + icon: 'gicon gicon-branding', + subTitle: 'Register your Grafana account', + breadcrumbs: [{ title: 'Login', url: '/login' }, { title: 'Sign Up' }], + }, + }; + backendSrv.get('/api/user/signup/options').then(options => { $scope.verifyEmailEnabled = options.verifyEmailEnabled; $scope.autoAssignOrg = options.autoAssignOrg; }); } - submit () { + submit() { if (!this.$scope.signUpForm.$valid) { return; } @@ -48,4 +50,3 @@ export class SignUpCtrl { } coreModule.controller('SignUpCtrl', SignUpCtrl); - diff --git a/public/app/core/core.ts b/public/app/core/core.ts index fb92fd81d19..bd9e0f7460d 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -1,14 +1,14 @@ -import "./directives/dash_class"; -import "./directives/dash_edit_link"; -import "./directives/dropdown_typeahead"; -import "./directives/metric_segment"; -import "./directives/misc"; -import "./directives/ng_model_on_blur"; -import "./directives/tags"; -import "./directives/value_select_dropdown"; -import "./directives/rebuild_on_change"; -import "./directives/give_focus"; -import "./directives/diff-view"; +import './directives/dash_class'; +import './directives/dash_edit_link'; +import './directives/dropdown_typeahead'; +import './directives/metric_segment'; +import './directives/misc'; +import './directives/ng_model_on_blur'; +import './directives/tags'; +import './directives/value_select_dropdown'; +import './directives/rebuild_on_change'; +import './directives/give_focus'; +import './directives/diff-view'; import './jquery_extended'; import './partials'; import './components/jsontree/jsontree'; @@ -17,20 +17,22 @@ import './utils/outline'; import './components/colorpicker/ColorPicker'; import './components/colorpicker/SeriesColorPicker'; import './components/colorpicker/spectrum_picker'; +import './services/search_srv'; +import './services/ng_react'; -import {grafanaAppDirective} from './components/grafana_app'; -import {sideMenuDirective} from './components/sidemenu/sidemenu'; -import {searchDirective} from './components/search/search'; -import {infoPopover} from './components/info_popover'; -import {navbarDirective} from './components/navbar/navbar'; -import {arrayJoin} from './directives/array_join'; -import {liveSrv} from './live/live_srv'; -import {Emitter} from './utils/emitter'; -import {layoutSelector} from './components/layout_selector/layout_selector'; -import {switchDirective} from './components/switch'; -import {dashboardSelector} from './components/dashboard_selector'; -import {queryPartEditorDirective} from './components/query_part/query_part_editor'; -import {formDropdownDirective} from './components/form_dropdown/form_dropdown'; +import { grafanaAppDirective } from './components/grafana_app'; +import { sideMenuDirective } from './components/sidemenu/sidemenu'; +import { searchDirective } from './components/search/search'; +import { infoPopover } from './components/info_popover'; +import { navbarDirective } from './components/navbar/navbar'; +import { arrayJoin } from './directives/array_join'; +import { liveSrv } from './live/live_srv'; +import { Emitter } from './utils/emitter'; +import { layoutSelector } from './components/layout_selector/layout_selector'; +import { switchDirective } from './components/switch'; +import { dashboardSelector } from './components/dashboard_selector'; +import { queryPartEditorDirective } from './components/query_part/query_part_editor'; +import { formDropdownDirective } from './components/form_dropdown/form_dropdown'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -38,15 +40,26 @@ import './filters/filters'; import coreModule from './core_module'; import appEvents from './app_events'; import colors from './utils/colors'; -import {assignModelProperties} from './utils/model_utils'; -import {contextSrv} from './services/context_srv'; -import {KeybindingSrv} from './services/keybindingSrv'; -import {helpModal} from './components/help/help'; -import {JsonExplorer} from './components/json_explorer/json_explorer'; -import {NavModelSrv, NavModel} from './nav_model_srv'; -import {registerAngularDirectives} from './angular_wrappers'; +import { assignModelProperties } from './utils/model_utils'; +import { contextSrv } from './services/context_srv'; +import { KeybindingSrv } from './services/keybindingSrv'; +import { helpModal } from './components/help/help'; +import { JsonExplorer } from './components/json_explorer/json_explorer'; +import { NavModelSrv, NavModel } from './nav_model_srv'; +import { userPicker } from './components/user_picker'; +import { teamPicker } from './components/team_picker'; +import { geminiScrollbar } from './components/scroll/scroll'; +import { gfPageDirective } from './components/gf_page'; +import { orgSwitcher } from './components/org_switcher'; +import { profiler } from './profiler'; +import { registerAngularDirectives } from './angular_wrappers'; +import { updateLegendValues } from './time_series2'; +import TimeSeries from './time_series2'; +import { searchResultsDirective } from './components/search/search_results'; +import { manageDashboardsDirective } from './components/manage_dashboards/manage_dashboards'; export { + profiler, registerAngularDirectives, arrayJoin, coreModule, @@ -71,4 +84,13 @@ export { JsonExplorer, NavModelSrv, NavModel, + userPicker, + teamPicker, + geminiScrollbar, + gfPageDirective, + orgSwitcher, + manageDashboardsDirective, + TimeSeries, + updateLegendValues, + searchResultsDirective, }; diff --git a/public/app/core/directives/array_join.ts b/public/app/core/directives/array_join.ts index 6eb1cc0dd65..1bf1c6a9f53 100644 --- a/public/app/core/directives/array_join.ts +++ b/public/app/core/directives/array_join.ts @@ -10,7 +10,6 @@ export function arrayJoin() { restrict: 'A', require: 'ngModel', link: function(scope, element, attr, ngModel) { - function split_array(text) { return (text || '').split(','); } @@ -25,9 +24,8 @@ export function arrayJoin() { ngModel.$parsers.push(split_array); ngModel.$formatters.push(join_array); - } + }, }; } coreModule.directive('arrayJoin', arrayJoin); - diff --git a/public/app/core/directives/dash_class.js b/public/app/core/directives/dash_class.js index 08f4d3c7326..9df53bdbd48 100644 --- a/public/app/core/directives/dash_class.js +++ b/public/app/core/directives/dash_class.js @@ -18,21 +18,20 @@ function (_, $, coreModule) { elem.toggleClass('panel-in-fullscreen', false); }); - var lastHideControlsVal; - $scope.$watch('dashboard.hideControls', function() { - if (!$scope.dashboard) { - return; - } - - var hideControls = $scope.dashboard.hideControls; - if (lastHideControlsVal !== hideControls) { - elem.toggleClass('hide-controls', hideControls); - lastHideControlsVal = hideControls; - } + $scope.$watch('ctrl.playlistSrv.isPlaying', function(newValue) { + elem.toggleClass('playlist-active', newValue === true); }); - $scope.$watch('playlistSrv.isPlaying', function(newValue) { - elem.toggleClass('playlist-active', newValue === true); + $scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) { + if (newValue) { + elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue)); + setTimeout(function() { + elem.toggleClass('dashboard-page--settings-open', _.isString(newValue)); + }, 10); + } else { + elem.removeClass('dashboard-page--settings-opening'); + elem.removeClass('dashboard-page--settings-open'); + } }); } }; diff --git a/public/app/core/directives/dash_edit_link.js b/public/app/core/directives/dash_edit_link.js index d9a439b95c2..f0dc6f59a6a 100644 --- a/public/app/core/directives/dash_edit_link.js +++ b/public/app/core/directives/dash_edit_link.js @@ -2,8 +2,9 @@ define([ 'jquery', 'angular', '../core_module', + 'lodash', ], -function ($, angular, coreModule) { +function ($, angular, coreModule, _) { 'use strict'; var editViewMap = { @@ -12,7 +13,13 @@ function ($, angular, coreModule) { 'templating': { src: 'public/app/features/templating/partials/editor.html'}, 'history': { html: ''}, 'timepicker': { src: 'public/app/features/dashboard/timepicker/dropdown.html' }, - 'import': { html: '' } + 'import': { html: '', isModal: true }, + 'permissions': { html: '', isModal: true }, + 'new-folder': { + isModal: true, + html: '', + modalClass: 'modal--narrow' + } }; coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) { @@ -20,6 +27,7 @@ function ($, angular, coreModule) { restrict: 'A', link: function(scope, elem) { var editorScope; + var modalScope; var lastEditView; function hideEditorPane(hideToShowOtherView) { @@ -30,8 +38,7 @@ function ($, angular, coreModule) { function showEditorPane(evt, options) { if (options.editview) { - options.src = editViewMap[options.editview].src; - options.html = editViewMap[options.editview].html; + _.defaults(options, editViewMap[options.editview]); } if (lastEditView && lastEditView === options.editview) { @@ -45,6 +52,11 @@ function ($, angular, coreModule) { editorScope = options.scope ? options.scope.$new() : scope.$new(); editorScope.dismiss = function(hideToShowOtherView) { + if (modalScope) { + modalScope.dismiss(); + modalScope = null; + } + editorScope.$destroy(); lastEditView = null; editorScope = null; @@ -73,16 +85,17 @@ function ($, angular, coreModule) { } }; - if (options.editview === 'import') { - var modalScope = $rootScope.$new(); + if (options.isModal) { + modalScope = $rootScope.$new(); modalScope.$on("$destroy", function() { editorScope.dismiss(); }); $rootScope.appEvent('show-modal', { - templateHtml: '', + templateHtml: options.html, scope: modalScope, - backdrop: 'static' + backdrop: 'static', + modalClass: options.modalClass, }); return; @@ -109,7 +122,7 @@ function ($, angular, coreModule) { }, 10); } - scope.$watch("dashboardViewState.state.editview", function(newValue, oldValue) { + scope.$watch("ctrl.dashboardViewState.state.editview", function(newValue, oldValue) { if (newValue) { showEditorPane(null, {editview: newValue}); } else if (oldValue) { diff --git a/public/app/core/directives/diff-view.ts b/public/app/core/directives/diff-view.ts index db76681498c..2770a09a00b 100644 --- a/public/app/core/directives/diff-view.ts +++ b/public/app/core/directives/diff-view.ts @@ -8,8 +8,7 @@ export class DeltaCtrl { /** @ngInject */ constructor(private $rootScope) { - - const waitForCompile = (mutations) => { + const waitForCompile = mutations => { if (mutations.length === 1) { this.$rootScope.appEvent('json-diff-ready'); } @@ -72,7 +71,7 @@ export function linkJson() { link: '@lineLink', switchView: '&', }, - template: `Line {{ line }}` + template: `Line {{ line }}`, }; } coreModule.directive('diffLinkJson', linkJson); diff --git a/public/app/core/directives/give_focus.ts b/public/app/core/directives/give_focus.ts index 722cc4e5c10..41767d55c0f 100644 --- a/public/app/core/directives/give_focus.ts +++ b/public/app/core/directives/give_focus.ts @@ -8,19 +8,23 @@ coreModule.directive('giveFocus', function() { e.stopPropagation(); }); - scope.$watch(attrs.giveFocus, function (newValue) { - if (!newValue) { - return; - } - setTimeout(function() { - element.focus(); - var domEl = element[0]; - if (domEl.setSelectionRange) { - var pos = element.val().length * 2; - domEl.setSelectionRange(pos, pos); + scope.$watch( + attrs.giveFocus, + function(newValue) { + if (!newValue) { + return; } - }, 200); - }, true); + setTimeout(function() { + element.focus(); + var domEl = element[0]; + if (domEl.setSelectionRange) { + var pos = element.val().length * 2; + domEl.setSelectionRange(pos, pos); + } + }, 200); + }, + true + ); }; }); diff --git a/public/app/core/directives/misc.ts b/public/app/core/directives/misc.ts index 3ec5add0c35..299de05f112 100644 --- a/public/app/core/directives/misc.ts +++ b/public/app/core/directives/misc.ts @@ -1,50 +1,55 @@ -import angular from "angular"; -import Clipboard from "clipboard"; -import coreModule from "../core_module"; -import kbn from "app/core/utils/kbn"; +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) { return { - restrict: "E", + restrict: 'E', link: function(scope, elem, attrs) { var _t = '"; - _t = _t.replace(/{/g, "\\{").replace(/}/g, "\\}"); + '\'">
    '; + _t = _t.replace(/{/g, '\\{').replace(/}/g, '\\}'); elem.replaceWith($compile(angular.element(_t))(scope)); - } + }, }; } function clipboardButton() { return { scope: { - getText: "&clipboardButton" + getText: '&clipboardButton', }, link: function(scope, elem) { scope.clipboard = new Clipboard(elem[0], { text: function() { return scope.getText(); - } + }, }); - scope.$on("$destroy", function() { + scope.clipboard.on('success', () => { + appEvents.emit('alert-success', ['Content copied to clipboard']); + }); + + scope.$on('$destroy', function() { if (scope.clipboard) { scope.clipboard.destroy(); } }); - } + }, }; } /** @ngInject */ function compile($compile) { return { - restrict: "A", + restrict: 'A', link: function(scope, element, attrs) { scope.$watch( function(scope) { @@ -55,42 +60,42 @@ function compile($compile) { $compile(element.contents())(scope); } ); - } + }, }; } function watchChange() { return { - scope: { onchange: "&watchChange" }, + scope: { onchange: '&watchChange' }, link: function(scope, element) { - element.on("input", function() { + element.on('input', function() { scope.$apply(function() { scope.onchange({ inputValue: element.val() }); }); }); - } + }, }; } /** @ngInject */ function editorOptBool($compile) { return { - restrict: "E", + restrict: 'E', link: function(scope, elem, attrs) { - var ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : ""; - var tip = attrs.tip ? " " + attrs.tip + "" : ""; - var showIf = attrs.showIf ? ' ng-show="' + attrs.showIf + '" ' : ""; + var ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : ''; + var tip = attrs.tip ? ' ' + attrs.tip + '' : ''; + var showIf = attrs.showIf ? ' ng-show="' + attrs.showIf + '" ' : ''; var template = '
    " + + '>' + ' " + + '' + ''; elem.replaceWith($compile(angular.element(template))(scope)); - } + }, }; } /** @ngInject */ function editorCheckbox($compile, $interpolate) { return { - restrict: "E", + restrict: 'E', link: function(scope, elem, attrs) { var text = $interpolate(attrs.text)(scope); var model = $interpolate(attrs.model)(scope); - var ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : ""; - var tip = attrs.tip ? " " + attrs.tip + "" : ""; - var label = - '"; + var ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : ''; + var tip = attrs.tip ? ' ' + attrs.tip + '' : ''; + var label = ''; var template = ''; template = template + label; - elem.addClass("gf-form-checkbox"); + elem.addClass('gf-form-checkbox'); elem.html($compile(angular.element(template))(scope)); - } + }, }; } /** @ngInject */ function gfDropdown($parse, $compile, $timeout) { function buildTemplate(items, placement?) { - var upclass = placement === "top" ? "dropup" : ""; - var ul = [ - '" - ]; + var upclass = placement === 'top' ? 'dropup' : ''; + var ul = ['']; for (let index = 0; index < items.length; index++) { let item = items[index]; @@ -171,26 +164,24 @@ function gfDropdown($parse, $compile, $timeout) { } var li = - "" + + '' + '" + - (item.text || "") + - ""; + (item.click ? ' ng-click="' + item.click + '"' : '') + + (item.target ? ' target="' + item.target + '"' : '') + + (item.method ? ' data-method="' + item.method + '"' : '') + + '>' + + (item.text || '') + + ''; if (item.submenu && item.submenu.length) { - li += buildTemplate(item.submenu).join("\n"); + li += buildTemplate(item.submenu).join('\n'); } - li += ""; + li += ''; ul.splice(index + 1, 0, li); } @@ -198,29 +189,27 @@ function gfDropdown($parse, $compile, $timeout) { } return { - restrict: "EA", + restrict: 'EA', scope: true, link: function postLink(scope, iElement, iAttrs) { var getter = $parse(iAttrs.gfDropdown), items = getter(scope); $timeout(function() { - var placement = iElement.data("placement"); - var dropdown = angular.element( - buildTemplate(items, placement).join("") - ); + var placement = iElement.data('placement'); + var dropdown = angular.element(buildTemplate(items, placement).join('')); dropdown.insertAfter(iElement); - $compile(iElement.next("ul.dropdown-menu"))(scope); + $compile(iElement.next('ul.dropdown-menu'))(scope); }); - iElement.addClass("dropdown-toggle").attr("data-toggle", "dropdown"); - } + iElement.addClass('dropdown-toggle').attr('data-toggle', 'dropdown'); + }, }; } -coreModule.directive("tip", tip); -coreModule.directive("clipboardButton", clipboardButton); -coreModule.directive("compile", compile); -coreModule.directive("watchChange", watchChange); -coreModule.directive("editorOptBool", editorOptBool); -coreModule.directive("editorCheckbox", editorCheckbox); -coreModule.directive("gfDropdown", gfDropdown); +coreModule.directive('tip', tip); +coreModule.directive('clipboardButton', clipboardButton); +coreModule.directive('compile', compile); +coreModule.directive('watchChange', watchChange); +coreModule.directive('editorOptBool', editorOptBool); +coreModule.directive('editorCheckbox', editorCheckbox); +coreModule.directive('gfDropdown', gfDropdown); diff --git a/public/app/core/directives/ng_model_on_blur.ts b/public/app/core/directives/ng_model_on_blur.ts index 2f7de3bf20c..4385c72f1b0 100644 --- a/public/app/core/directives/ng_model_on_blur.ts +++ b/public/app/core/directives/ng_model_on_blur.ts @@ -17,7 +17,7 @@ function ngModelOnBlur() { ngModelCtrl.$setViewValue(elm.val()); }); }); - } + }, }; } @@ -25,12 +25,14 @@ function emptyToNull() { return { restrict: 'A', require: 'ngModel', - link: function (scope, elm, attrs, ctrl) { - ctrl.$parsers.push(function (viewValue) { - if (viewValue === "") { return null; } + link: function(scope, elm, attrs, ctrl) { + ctrl.$parsers.push(function(viewValue) { + if (viewValue === '') { + return null; + } return viewValue; }); - } + }, }; } @@ -48,7 +50,7 @@ function validTimeSpan() { var info = rangeUtil.describeTextRange(viewValue); return info.invalid !== true; }; - } + }, }; } diff --git a/public/app/core/directives/rebuild_on_change.ts b/public/app/core/directives/rebuild_on_change.ts index db2634869c0..15907dc6c19 100644 --- a/public/app/core/directives/rebuild_on_change.ts +++ b/public/app/core/directives/rebuild_on_change.ts @@ -20,7 +20,6 @@ function getBlockNodes(nodes) { /** @ngInject **/ function rebuildOnChange($animate) { - return { multiElement: true, terminal: true, @@ -57,14 +56,14 @@ function rebuildOnChange($animate) { transclude(function(clone, newScope) { childScope = newScope; clone[clone.length++] = document.createComment(' end rebuild on change '); - block = {clone: clone}; + block = { clone: clone }; $animate.enter(clone, elem.parent(), elem); }); } else { cleanUp(); } }); - } + }, }; } diff --git a/public/app/core/directives/tags.ts b/public/app/core/directives/tags.ts index 0f4d0990d47..b5020b71fc7 100644 --- a/public/app/core/directives/tags.ts +++ b/public/app/core/directives/tags.ts @@ -6,7 +6,7 @@ import 'vendor/tagsinput/bootstrap-tagsinput.js'; function djb2(str) { var hash = 5381; for (var i = 0; i < str.length; i++) { - hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */ + hash = (hash << 5) + hash + str.charCodeAt(i); /* hash * 33 + c */ } return hash; } @@ -14,33 +14,79 @@ function djb2(str) { function setColor(name, element) { var hash = djb2(name.toLowerCase()); var colors = [ - "#E24D42","#1F78C1","#BA43A9","#705DA0","#466803", - "#508642","#447EBC","#C15C17","#890F02","#757575", - "#0A437C","#6D1F62","#584477","#629E51","#2F4F4F", - "#BF1B00","#806EB7","#8a2eb8", "#699e00","#000000", - "#3F6833","#2F575E","#99440A","#E0752D","#0E4AB4", - "#58140C","#052B51","#511749","#3F2B5B", + '#E24D42', + '#1F78C1', + '#BA43A9', + '#705DA0', + '#466803', + '#508642', + '#447EBC', + '#C15C17', + '#890F02', + '#757575', + '#0A437C', + '#6D1F62', + '#584477', + '#629E51', + '#2F4F4F', + '#BF1B00', + '#806EB7', + '#8a2eb8', + '#699e00', + '#000000', + '#3F6833', + '#2F575E', + '#99440A', + '#E0752D', + '#0E4AB4', + '#58140C', + '#052B51', + '#511749', + '#3F2B5B', ]; var borderColors = [ - "#FF7368","#459EE7","#E069CF","#9683C6","#6C8E29", - "#76AC68","#6AA4E2","#E7823D","#AF3528","#9B9B9B", - "#3069A2","#934588","#7E6A9D","#88C477","#557575", - "#E54126","#A694DD","#B054DE", "#8FC426","#262626", - "#658E59","#557D84","#BF6A30","#FF9B53","#3470DA", - "#7E3A32","#2B5177","#773D6F","#655181", + '#FF7368', + '#459EE7', + '#E069CF', + '#9683C6', + '#6C8E29', + '#76AC68', + '#6AA4E2', + '#E7823D', + '#AF3528', + '#9B9B9B', + '#3069A2', + '#934588', + '#7E6A9D', + '#88C477', + '#557575', + '#E54126', + '#A694DD', + '#B054DE', + '#8FC426', + '#262626', + '#658E59', + '#557D84', + '#BF6A30', + '#FF9B53', + '#3470DA', + '#7E3A32', + '#2B5177', + '#773D6F', + '#655181', ]; var color = colors[Math.abs(hash % colors.length)]; var borderColor = borderColors[Math.abs(hash % borderColors.length)]; - element.css("background-color", color); - element.css("border-color", borderColor); + element.css('background-color', color); + element.css('border-color', borderColor); } function tagColorFromName() { return { - scope: { tagColorFromName: "=" }, - link: function (scope, element) { + scope: { tagColorFromName: '=' }, + link: function(scope, element) { setColor(scope.tagColorFromName, element); - } + }, }; } @@ -63,12 +109,11 @@ function bootstrapTagsinput() { restrict: 'EA', scope: { model: '=ngModel', - onTagsUpdated: "&", + onTagsUpdated: '&', }, template: '', replace: false, link: function(scope, element, attrs) { - if (!angular.isArray(scope.model)) { scope.model = []; } @@ -81,13 +126,18 @@ function bootstrapTagsinput() { select.tagsinput({ typeahead: { - source: angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null + source: angular.isFunction(scope.$parent[attrs.typeaheadSource]) + ? scope.$parent[attrs.typeaheadSource] + : null, }, widthClass: attrs.widthClass, itemValue: getItemProperty(scope, attrs.itemvalue), - itemText : getItemProperty(scope, attrs.itemtext), - tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ? - scope.$parent[attrs.tagclass] : function() { return attrs.tagclass; } + itemText: getItemProperty(scope, attrs.itemtext), + tagClass: angular.isFunction(scope.$parent[attrs.tagclass]) + ? scope.$parent[attrs.tagclass] + : function() { + return attrs.tagclass; + }, }); select.on('itemAdded', function(event) { @@ -97,7 +147,12 @@ function bootstrapTagsinput() { scope.onTagsUpdated(); } } - var tagElement = select.next().children("span").filter(function() { return $(this).text() === event.item; }); + var tagElement = select + .next() + .children('span') + .filter(function() { + return $(this).text() === event.item; + }); setColor(event.item, tagElement); }); @@ -111,19 +166,22 @@ function bootstrapTagsinput() { } }); - scope.$watch("model", function() { - if (!angular.isArray(scope.model)) { - scope.model = []; - } + scope.$watch( + 'model', + function() { + if (!angular.isArray(scope.model)) { + scope.model = []; + } - select.tagsinput('removeAll'); + select.tagsinput('removeAll'); - for (var i = 0; i < scope.model.length; i++) { - select.tagsinput('add', scope.model[i]); - } - - }, true); - } + for (var i = 0; i < scope.model.length; i++) { + select.tagsinput('add', scope.model[i]); + } + }, + true + ); + }, }; } diff --git a/public/app/core/filters/filters.ts b/public/app/core/filters/filters.ts index 0f53df8b4fb..1556e0a94f5 100644 --- a/public/app/core/filters/filters.ts +++ b/public/app/core/filters/filters.ts @@ -41,19 +41,17 @@ coreModule.filter('moment', function() { coreModule.filter('noXml', function() { var noXml = function(text) { - return _.isString(text) - ? text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/'/g, ''') - .replace(/"/g, '"') - : text; + return _.isString(text) + ? text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + : text; }; return function(text) { - return _.isArray(text) - ? _.map(text, noXml) - : noXml(text); + return _.isArray(text) ? _.map(text, noXml) : noXml(text); }; }); diff --git a/public/app/core/live/live_srv.ts b/public/app/core/live/live_srv.ts index 8e808e60e1a..e03e8296794 100644 --- a/public/app/core/live/live_srv.ts +++ b/public/app/core/live/live_srv.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import config from 'app/core/config'; -import {Observable} from 'rxjs/Observable'; +import { Observable } from 'rxjs/Observable'; export class LiveSrv { conn: any; @@ -14,7 +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() { @@ -30,25 +30,25 @@ export class LiveSrv { console.log('Live: connecting...'); this.conn = new WebSocket(this.getWebSocketUrl()); - this.conn.onclose = (evt) => { - console.log("Live: websocket onclose", evt); - reject({message: 'Connection closed'}); + this.conn.onclose = evt => { + console.log('Live: websocket onclose', evt); + reject({ message: 'Connection closed' }); this.initPromise = null; setTimeout(this.reconnect.bind(this), 2000); }; - this.conn.onmessage = (evt) => { + this.conn.onmessage = evt => { this.handleMessage(evt.data); }; - this.conn.onerror = (evt) => { + this.conn.onerror = evt => { this.initPromise = null; - reject({message: 'Connection error'}); - console.log("Live: websocket error", evt); + reject({ message: 'Connection error' }); + console.log('Live: websocket error', evt); }; - this.conn.onopen = (evt) => { + this.conn.onopen = evt => { console.log('opened'); this.initPromise = null; resolve(this.conn); @@ -62,7 +62,7 @@ export class LiveSrv { message = JSON.parse(message); if (!message.stream) { - console.log("Error: stream message without stream!", message); + console.log('Error: stream message without stream!', message); return; } @@ -85,7 +85,7 @@ export class LiveSrv { this.getConnection().then(conn => { _.each(this.observers, (value, key) => { - this.send({action: 'subscribe', stream: key}); + this.send({ action: 'subscribe', stream: key }); }); }); } @@ -98,7 +98,7 @@ export class LiveSrv { this.observers[stream] = observer; this.getConnection().then(conn => { - this.send({action: 'subscribe', stream: stream}); + this.send({ action: 'subscribe', stream: stream }); }); } @@ -107,7 +107,7 @@ export class LiveSrv { delete this.observers[stream]; this.getConnection().then(conn => { - this.send({action: 'unsubscribe', stream: stream}); + this.send({ action: 'unsubscribe', stream: stream }); }); } @@ -126,8 +126,7 @@ export class LiveSrv { // this.send({action: 'subscribe', stream: name}); // }); } - } var instance = new LiveSrv(); -export {instance as liveSrv}; +export { instance as liveSrv }; diff --git a/public/app/core/mod_defs.d.ts b/public/app/core/mod_defs.d.ts index e42bac80310..d5b2e6c2a8a 100644 --- a/public/app/core/mod_defs.d.ts +++ b/public/app/core/mod_defs.d.ts @@ -1,18 +1,14 @@ -declare module "app/core/controllers/all" { +declare module 'app/core/controllers/all' { let json: any; - export {json}; + export { json }; } -declare module "app/core/routes/all" { +declare module 'app/core/routes/all' { let json: any; - export {json}; + export { json }; } -declare module "app/core/services/all" { +declare module 'app/core/services/all' { let json: any; export default json; } - - - - diff --git a/public/app/core/nav_model_srv.ts b/public/app/core/nav_model_srv.ts index dd61db5d346..a9ebd4e79ed 100644 --- a/public/app/core/nav_model_srv.ts +++ b/public/app/core/nav_model_srv.ts @@ -1,224 +1,84 @@ -/// - import coreModule from 'app/core/core_module'; +import config from 'app/core/config'; +import _ from 'lodash'; export interface NavModelItem { - title: string; + text: string; url: string; icon?: string; - iconUrl?: string; + img?: string; + id: string; + active?: boolean; + hideFromTabs?: boolean; + divider?: boolean; + children: NavModelItem[]; + target?: string; } -export interface NavModel { - section: NavModelItem; - menu: NavModelItem[]; +export class NavModel { + breadcrumbs: NavModelItem[]; + main: NavModelItem; + node: NavModelItem; + + constructor() { + this.breadcrumbs = []; + } } export class NavModelSrv { - + navItems: any; /** @ngInject */ - constructor(private contextSrv) { + constructor() { + this.navItems = config.bootData.navTree; } - getAlertingNav(subPage) { - return { - section: { - title: 'Alerting', - url: 'plugins', - icon: 'icon-gf icon-gf-alert' - }, - menu: [ - {title: 'Alert List', active: subPage === 0, url: 'alerting/list', icon: 'fa fa-list-ul'}, - {title: 'Notification channels', active: subPage === 1, url: 'alerting/notifications', icon: 'fa fa-bell-o'}, - ] - }; + getCfgNode() { + return _.find(this.navItems, { id: 'cfg' }); } - getDatasourceNav(subPage) { - return { - section: { - title: 'Data Sources', - url: 'datasources', - icon: 'icon-gf icon-gf-datasources' - }, - menu: [ - {title: 'List view', active: subPage === 0, url: 'datasources', icon: 'fa fa-list-ul'}, - {title: 'Add data source', active: subPage === 1, url: 'datasources/new', icon: 'fa fa-plus'}, - ] - }; - } + getNav(...args) { + var children = this.navItems; + var nav = new NavModel(); - getPlaylistsNav(subPage) { - return { - section: { - title: 'Playlists', - url: 'playlists', - icon: 'fa fa-fw fa-film' - }, - menu: [ - {title: 'List view', active: subPage === 0, url: 'playlists', icon: 'fa fa-list-ul'}, - {title: 'Add Playlist', active: subPage === 1, url: 'playlists/create', icon: 'fa fa-plus'}, - ] - }; - } + for (let id of args) { + // if its a number then it's the index to use for main + if (_.isNumber(id)) { + nav.main = nav.breadcrumbs[id]; + break; + } - getProfileNav() { - return { - section: { - title: 'User Profile', - url: 'profile', - icon: 'fa fa-fw fa-user' - }, - menu: [] - }; + let node = _.find(children, { id: id }); + nav.breadcrumbs.push(node); + nav.node = node; + nav.main = node; + children = node.children; + } + + if (nav.main.children) { + for (let item of nav.main.children) { + item.active = false; + + if (item.url === nav.node.url) { + item.active = true; + } + } + } + + return nav; } getNotFoundNav() { - return { - section: { - title: 'Page', - url: '', - icon: 'fa fa-fw fa-warning' - }, - menu: [] + var node = { + text: 'Page not found', + icon: 'fa fa-fw fa-warning', + subTitle: '404 Error', }; - } - - getOrgNav(subPage) { - return { - section: { - title: 'Organization', - url: 'org', - icon: 'icon-gf icon-gf-users' - }, - menu: [ - {title: 'Preferences', active: subPage === 0, url: 'org', icon: 'fa fa-fw fa-cog'}, - {title: 'Org Users', active: subPage === 1, url: 'org/users', icon: 'fa fa-fw fa-users'}, - {title: 'API Keys', active: subPage === 2, url: 'org/apikeys', icon: 'fa fa-fw fa-key'}, - ] - }; - } - - getAdminNav(subPage) { - return { - section: { - title: 'Admin', - url: 'admin', - icon: 'fa fa-fw fa-cogs' - }, - menu: [ - {title: 'Users', active: subPage === 0, url: 'admin/users', icon: 'fa fa-fw fa-user'}, - {title: 'Orgs', active: subPage === 1, url: 'admin/orgs', icon: 'fa fa-fw fa-users'}, - {title: 'Server Settings', active: subPage === 2, url: 'admin/settings', icon: 'fa fa-fw fa-cogs'}, - {title: 'Server Stats', active: subPage === 2, url: 'admin/stats', icon: 'fa fa-fw fa-line-chart'}, - {title: 'Style Guide', active: subPage === 2, url: 'styleguide', icon: 'fa fa-fw fa-key'}, - ] - }; - } - - getPluginsNav() { - return { - section: { - title: 'Plugins', - url: 'plugins', - icon: 'icon-gf icon-gf-apps' - }, - menu: [] - }; - } - - getDashboardNav(dashboard, dashNavCtrl) { - // special handling for snapshots - if (dashboard.meta.isSnapshot) { - return { - section: { - title: dashboard.title, - icon: 'icon-gf icon-gf-snapshot' - }, - menu: [ - { - title: 'Go to original dashboard', - icon: 'fa fa-fw fa-external-link', - url: dashboard.snapshot.originalUrl, - } - ] - }; - } - - var menu = []; - - if (dashboard.meta.canEdit) { - menu.push({ - title: 'Settings', - icon: 'fa fa-fw fa-cog', - clickHandler: () => dashNavCtrl.openEditView('settings') - }); - - menu.push({ - title: 'Templating', - icon: 'fa fa-fw fa-code', - clickHandler: () => dashNavCtrl.openEditView('templating') - }); - - menu.push({ - title: 'Annotations', - icon: 'fa fa-fw fa-comment', - clickHandler: () => dashNavCtrl.openEditView('annotations') - }); - - if (!dashboard.meta.isHome) { - menu.push({ - title: 'Version history', - icon: 'fa fa-fw fa-history', - clickHandler: () => dashNavCtrl.openEditView('history') - }); - } - - menu.push({ - title: 'View JSON', - icon: 'fa fa-fw fa-eye', - clickHandler: () => dashNavCtrl.viewJson() - }); - } - - if (this.contextSrv.isEditor && !dashboard.editable) { - menu.push({ - title: 'Make Editable', - icon: 'fa fa-fw fa-edit', - clickHandler: () => dashNavCtrl.makeEditable() - }); - } - - menu.push({ - title: 'Shortcuts', - icon: 'fa fa-fw fa-keyboard-o', - clickHandler: () => dashNavCtrl.showHelpModal() - }); - - if (this.contextSrv.isEditor) { - menu.push({ - title: 'Save As ...', - icon: 'fa fa-fw fa-save', - clickHandler: () => dashNavCtrl.saveDashboardAs() - }); - } - - if (dashboard.meta.canSave) { - menu.push({ - title: 'Delete', - icon: 'fa fa-fw fa-trash', - clickHandler: () => dashNavCtrl.deleteDashboard() - }); - - } return { - section: { - title: dashboard.title, - icon: 'icon-gf icon-gf-dashboard' - }, - menu: menu + breadcrumbs: [node], + node: node, + main: node, }; } } diff --git a/public/app/core/profiler.ts b/public/app/core/profiler.ts index e762b3fc4b3..f459c5d4557 100644 --- a/public/app/core/profiler.ts +++ b/public/app/core/profiler.ts @@ -20,10 +20,13 @@ export class Profiler { return; } - $rootScope.$watch(() => { - this.digestCounter++; - return false; - }, () => {}); + $rootScope.$watch( + () => { + this.digestCounter++; + return false; + }, + () => {} + ); $rootScope.onAppEvent('refresh', this.refresh.bind(this), $rootScope); $rootScope.onAppEvent('dashboard-fetch-end', this.dashboardFetched.bind(this), $rootScope); @@ -55,12 +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"); + console.log('Dashboard::Performance All panels initialized in ' + timeTaken + ' ms'); // measure digest performance var rootDigestStart = window.performance.now(); @@ -68,7 +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); } @@ -77,15 +80,15 @@ export class Profiler { var scopes = 0; var root = $(document.getElementsByTagName('body')); - var f = function (element) { + var f = function(element) { if (element.data().hasOwnProperty('$scope')) { scopes++; - angular.forEach(element.data().$scope.$$watchers, function () { + angular.forEach(element.data().$scope.$$watchers, function() { count++; }); } - angular.forEach(element.children(), function (childElement) { + angular.forEach(element.children(), function(childElement) { f($(childElement)); }); }; @@ -116,8 +119,7 @@ export class Profiler { this.panelsInitCount++; this.timings.lastPanelInitializedAt = new Date().getTime(); } - } var profiler = new Profiler(); -export {profiler}; +export { profiler }; diff --git a/public/app/core/routes/bundle_loader.ts b/public/app/core/routes/bundle_loader.ts index b86f9fb1495..e4e345b49fc 100644 --- a/public/app/core/routes/bundle_loader.ts +++ b/public/app/core/routes/bundle_loader.ts @@ -4,19 +4,23 @@ export class BundleLoader { constructor(bundleName) { var defer = null; - this.lazy = ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => { - if (defer) { + 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; - } - - defer = $q.defer(); - - System.import(bundleName).then(() => { - defer.resolve(); - }); - - return defer.promise; - }]; - + }, + ]; } } diff --git a/public/app/core/routes/dashboard_loaders.ts b/public/app/core/routes/dashboard_loaders.ts index 14e093d5169..8aad65bd02f 100644 --- a/public/app/core/routes/dashboard_loaders.ts +++ b/public/app/core/routes/dashboard_loaders.ts @@ -1,10 +1,9 @@ import coreModule from '../core_module'; export class LoadDashboardCtrl { - /** @ngInject */ constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) { - $scope.appEvent("dashboard-fetch-start"); + $scope.appEvent('dashboard-fetch-start'); if (!$routeParams.slug) { backendSrv.get('/api/dashboards/home').then(function(homeDash) { @@ -20,29 +19,38 @@ export class LoadDashboardCtrl { } dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { + if ($routeParams.keepRows) { + result.meta.keepRows = true; + } $scope.initDashboard(result, $scope); }); } } export class NewDashboardCtrl { - /** @ngInject */ - constructor($scope) { - $scope.initDashboard({ - meta: { canStar: false, canShare: false, isNew: true }, - dashboard: { - title: "New dashboard", - rows: [ - { - title: 'Dashboard Row', - height: '250px', - panels: [], - isNew: true, - } - ] + constructor($scope, $routeParams) { + $scope.initDashboard( + { + meta: { + canStar: false, + canShare: false, + isNew: true, + folderId: Number($routeParams.folderId), + }, + dashboard: { + title: 'New dashboard', + panels: [ + { + type: 'add-panel', + gridPos: { x: 0, y: 0, w: 12, h: 9 }, + title: 'Panel Title', + }, + ], + }, }, - }, $scope); + $scope + ); } } diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 1f9dbcae57c..b4d3290bc45 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -6,228 +6,298 @@ function setupAngularRoutes($routeProvider, $locationProvider) { $locationProvider.html5Mode(true); var loadOrgBundle = { - lazy: ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => { - return System.import('app/features/org/all'); - }] + lazy: [ + '$q', + '$route', + '$rootScope', + ($q, $route, $rootScope) => { + return System.import('app/features/org/all'); + }, + ], }; var loadAdminBundle = { - lazy: ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => { - return System.import('app/features/admin/admin'); - }] + lazy: [ + '$q', + '$route', + '$rootScope', + ($q, $route, $rootScope) => { + return System.import('app/features/admin/admin'); + }, + ], }; var loadAlertingBundle = { - lazy: ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => { - return System.import('app/features/alerting/all'); - }] + lazy: [ + '$q', + '$route', + '$rootScope', + ($q, $route, $rootScope) => { + return System.import('app/features/alerting/all'); + }, + ], }; $routeProvider - .when('/', { - templateUrl: 'public/app/partials/dashboard.html', - controller : 'LoadDashboardCtrl', - reloadOnSearch: false, - pageClass: 'page-dashboard', - }) - .when('/dashboard/:type/:slug', { - templateUrl: 'public/app/partials/dashboard.html', - controller : 'LoadDashboardCtrl', - reloadOnSearch: false, - pageClass: 'page-dashboard', - }) - .when('/dashboard-solo/:type/:slug', { - templateUrl: 'public/app/features/panel/partials/soloPanel.html', - controller : 'SoloPanelCtrl', - reloadOnSearch: false, - pageClass: 'page-dashboard', - }) - .when('/dashboard/new', { - templateUrl: 'public/app/partials/dashboard.html', - controller : 'NewDashboardCtrl', - reloadOnSearch: false, - pageClass: 'page-dashboard', - }) - .when('/dashboards/list', { - templateUrl: 'public/app/features/dashboard/partials/dash_list.html', - controller : 'DashListCtrl', - }) - .when('/datasources', { - templateUrl: 'public/app/features/plugins/partials/ds_list.html', - controller : 'DataSourcesCtrl', - controllerAs: 'ctrl', - }) - .when('/datasources/edit/:id', { - templateUrl: 'public/app/features/plugins/partials/ds_edit.html', - controller : 'DataSourceEditCtrl', - controllerAs: 'ctrl', - }) - .when('/datasources/new', { - templateUrl: 'public/app/features/plugins/partials/ds_edit.html', - controller : 'DataSourceEditCtrl', - controllerAs: 'ctrl', - }) - .when('/org', { - templateUrl: 'public/app/features/org/partials/orgDetails.html', - controller : 'OrgDetailsCtrl', - resolve: loadOrgBundle, - }) - .when('/org/new', { - templateUrl: 'public/app/features/org/partials/newOrg.html', - controller : 'NewOrgCtrl', - resolve: loadOrgBundle, - }) - .when('/org/users', { - templateUrl: 'public/app/features/org/partials/orgUsers.html', - controller : 'OrgUsersCtrl', - controllerAs: 'ctrl', - resolve: loadOrgBundle, - }) - .when('/org/apikeys', { - templateUrl: 'public/app/features/org/partials/orgApiKeys.html', - controller : 'OrgApiKeysCtrl', - resolve: loadOrgBundle, - }) - .when('/profile', { - templateUrl: 'public/app/features/org/partials/profile.html', - controller : 'ProfileCtrl', - controllerAs: 'ctrl', - resolve: loadOrgBundle, - }) - .when('/profile/password', { - templateUrl: 'public/app/features/org/partials/change_password.html', - controller : 'ChangePasswordCtrl', - resolve: loadOrgBundle, - }) - .when('/profile/select-org', { - templateUrl: 'public/app/features/org/partials/select_org.html', - controller : 'SelectOrgCtrl', - resolve: loadOrgBundle, - }) - // ADMIN - .when('/admin', { - templateUrl: 'public/app/features/admin/partials/admin_home.html', - controller : 'AdminHomeCtrl', - controllerAs: 'ctrl', - resolve: loadAdminBundle, - }) - .when('/admin/settings', { - templateUrl: 'public/app/features/admin/partials/settings.html', - controller : 'AdminSettingsCtrl', - controllerAs: 'ctrl', - resolve: loadAdminBundle, - }) - .when('/admin/users', { - templateUrl: 'public/app/features/admin/partials/users.html', - controller : 'AdminListUsersCtrl', - controllerAs: 'ctrl', - resolve: loadAdminBundle, - }) - .when('/admin/users/create', { - templateUrl: 'public/app/features/admin/partials/new_user.html', - controller : 'AdminEditUserCtrl', - resolve: loadAdminBundle, - }) - .when('/admin/users/edit/:id', { - templateUrl: 'public/app/features/admin/partials/edit_user.html', - controller : 'AdminEditUserCtrl', - resolve: loadAdminBundle, - }) - .when('/admin/orgs', { - templateUrl: 'public/app/features/admin/partials/orgs.html', - controller : 'AdminListOrgsCtrl', - controllerAs: 'ctrl', - resolve: loadAdminBundle, - }) - .when('/admin/orgs/edit/:id', { - templateUrl: 'public/app/features/admin/partials/edit_org.html', - controller : 'AdminEditOrgCtrl', - controllerAs: 'ctrl', - resolve: loadAdminBundle, - }) - .when('/admin/stats', { - templateUrl: 'public/app/features/admin/partials/stats.html', - controller : 'AdminStatsCtrl', - controllerAs: 'ctrl', - resolve: loadAdminBundle, - }) - // LOGIN / SIGNUP - .when('/login', { - templateUrl: 'public/app/partials/login.html', - controller : 'LoginCtrl', - }) - .when('/invite/:code', { - templateUrl: 'public/app/partials/signup_invited.html', - controller : 'InvitedCtrl', - }) - .when('/signup', { - templateUrl: 'public/app/partials/signup_step2.html', - controller : 'SignUpCtrl', - }) - .when('/user/password/send-reset-email', { - templateUrl: 'public/app/partials/reset_password.html', - controller : 'ResetPasswordCtrl', - }) - .when('/user/password/reset', { - templateUrl: 'public/app/partials/reset_password.html', - controller : 'ResetPasswordCtrl', - }) - .when('/dashboard/snapshots', { - templateUrl: 'public/app/features/snapshot/partials/snapshots.html', - controller : 'SnapshotsCtrl', - controllerAs: 'ctrl', - }) - .when('/plugins', { - templateUrl: 'public/app/features/plugins/partials/plugin_list.html', - controller: 'PluginListCtrl', - controllerAs: 'ctrl', - }) - .when('/plugins/:pluginId/edit', { - templateUrl: 'public/app/features/plugins/partials/plugin_edit.html', - controller: 'PluginEditCtrl', - controllerAs: 'ctrl', - }) - .when('/plugins/:pluginId/page/:slug', { - templateUrl: 'public/app/features/plugins/partials/plugin_page.html', - controller: 'AppPageCtrl', - controllerAs: 'ctrl', - }) - .when('/styleguide/:page?', { - controller: 'StyleGuideCtrl', - controllerAs: 'ctrl', - templateUrl: 'public/app/features/styleguide/styleguide.html', - }) - .when('/alerting', { - redirectTo: '/alerting/list' - }) - .when('/alerting/list', { - templateUrl: 'public/app/features/alerting/partials/alert_list.html', - controller: 'AlertListCtrl', - controllerAs: 'ctrl', - resolve: loadAlertingBundle, - }) - .when('/alerting/notifications', { - templateUrl: 'public/app/features/alerting/partials/notifications_list.html', - controller: 'AlertNotificationsListCtrl', - controllerAs: 'ctrl', - resolve: loadAlertingBundle, - }) - .when('/alerting/notification/new', { - templateUrl: 'public/app/features/alerting/partials/notification_edit.html', - controller: 'AlertNotificationEditCtrl', - controllerAs: 'ctrl', - resolve: loadAlertingBundle, - }) - .when('/alerting/notification/:id/edit', { - templateUrl: 'public/app/features/alerting/partials/notification_edit.html', - controller: 'AlertNotificationEditCtrl', - controllerAs: 'ctrl', - resolve: loadAlertingBundle, - }) - .otherwise({ - templateUrl: 'public/app/partials/error.html', - controller: 'ErrorCtrl' - }); + .when('/', { + templateUrl: 'public/app/partials/dashboard.html', + controller: 'LoadDashboardCtrl', + reloadOnSearch: false, + pageClass: 'page-dashboard', + }) + .when('/dashboard/:type/:slug', { + templateUrl: 'public/app/partials/dashboard.html', + controller: 'LoadDashboardCtrl', + reloadOnSearch: false, + pageClass: 'page-dashboard', + }) + .when('/dashboard-solo/:type/:slug', { + templateUrl: 'public/app/features/panel/partials/soloPanel.html', + controller: 'SoloPanelCtrl', + reloadOnSearch: false, + pageClass: 'page-dashboard', + }) + .when('/dashboard/new', { + templateUrl: 'public/app/partials/dashboard.html', + controller: 'NewDashboardCtrl', + reloadOnSearch: false, + pageClass: 'page-dashboard', + }) + .when('/dashboard/import', { + templateUrl: 'public/app/features/dashboard/partials/dashboard_import.html', + controller: 'DashboardImportCtrl', + controllerAs: 'ctrl', + }) + .when('/datasources', { + templateUrl: 'public/app/features/plugins/partials/ds_list.html', + controller: 'DataSourcesCtrl', + controllerAs: 'ctrl', + }) + .when('/datasources/edit/:id', { + templateUrl: 'public/app/features/plugins/partials/ds_edit.html', + controller: 'DataSourceEditCtrl', + controllerAs: 'ctrl', + }) + .when('/datasources/new', { + templateUrl: 'public/app/features/plugins/partials/ds_edit.html', + controller: 'DataSourceEditCtrl', + controllerAs: 'ctrl', + }) + .when('/dashboards', { + templateUrl: 'public/app/features/dashboard/partials/dashboard_list.html', + controller: 'DashboardListCtrl', + controllerAs: 'ctrl', + }) + .when('/dashboards/folder/new', { + templateUrl: 'public/app/features/dashboard/partials/create_folder.html', + controller: 'CreateFolderCtrl', + controllerAs: 'ctrl', + }) + .when('/dashboards/folder/:folderId/:slug/permissions', { + templateUrl: 'public/app/features/dashboard/partials/folder_permissions.html', + controller: 'FolderPermissionsCtrl', + controllerAs: 'ctrl', + }) + .when('/dashboards/folder/:folderId/:slug/settings', { + templateUrl: 'public/app/features/dashboard/partials/folder_settings.html', + controller: 'FolderSettingsCtrl', + controllerAs: 'ctrl', + }) + .when('/dashboards/folder/:folderId/:slug', { + templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html', + controller: 'FolderDashboardsCtrl', + controllerAs: 'ctrl', + }) + .when('/org', { + templateUrl: 'public/app/features/org/partials/orgDetails.html', + controller: 'OrgDetailsCtrl', + resolve: loadOrgBundle, + }) + .when('/org/new', { + templateUrl: 'public/app/features/org/partials/newOrg.html', + controller: 'NewOrgCtrl', + resolve: loadOrgBundle, + }) + .when('/org/users', { + templateUrl: 'public/app/features/org/partials/orgUsers.html', + controller: 'OrgUsersCtrl', + controllerAs: 'ctrl', + resolve: loadOrgBundle, + }) + .when('/org/users/invite', { + templateUrl: 'public/app/features/org/partials/invite.html', + controller: 'UserInviteCtrl', + controllerAs: 'ctrl', + resolve: loadOrgBundle, + }) + .when('/org/apikeys', { + templateUrl: 'public/app/features/org/partials/orgApiKeys.html', + controller: 'OrgApiKeysCtrl', + resolve: loadOrgBundle, + }) + .when('/org/teams', { + templateUrl: 'public/app/features/org/partials/teams.html', + controller: 'TeamsCtrl', + controllerAs: 'ctrl', + resolve: loadOrgBundle, + }) + .when('/org/teams/new', { + templateUrl: 'public/app/features/org/partials/create_team.html', + controller: 'CreateTeamCtrl', + controllerAs: 'ctrl', + resolve: loadOrgBundle, + }) + .when('/org/teams/edit/:id', { + templateUrl: 'public/app/features/org/partials/team_details.html', + controller: 'TeamDetailsCtrl', + controllerAs: 'ctrl', + resolve: loadOrgBundle, + }) + .when('/profile', { + templateUrl: 'public/app/features/org/partials/profile.html', + controller: 'ProfileCtrl', + controllerAs: 'ctrl', + resolve: loadOrgBundle, + }) + .when('/profile/password', { + templateUrl: 'public/app/features/org/partials/change_password.html', + controller: 'ChangePasswordCtrl', + resolve: loadOrgBundle, + }) + .when('/profile/select-org', { + templateUrl: 'public/app/features/org/partials/select_org.html', + controller: 'SelectOrgCtrl', + resolve: loadOrgBundle, + }) + // ADMIN + .when('/admin', { + templateUrl: 'public/app/features/admin/partials/admin_home.html', + controller: 'AdminHomeCtrl', + controllerAs: 'ctrl', + resolve: loadAdminBundle, + }) + .when('/admin/settings', { + templateUrl: 'public/app/features/admin/partials/settings.html', + controller: 'AdminSettingsCtrl', + controllerAs: 'ctrl', + resolve: loadAdminBundle, + }) + .when('/admin/users', { + templateUrl: 'public/app/features/admin/partials/users.html', + controller: 'AdminListUsersCtrl', + controllerAs: 'ctrl', + resolve: loadAdminBundle, + }) + .when('/admin/users/create', { + templateUrl: 'public/app/features/admin/partials/new_user.html', + controller: 'AdminEditUserCtrl', + resolve: loadAdminBundle, + }) + .when('/admin/users/edit/:id', { + templateUrl: 'public/app/features/admin/partials/edit_user.html', + controller: 'AdminEditUserCtrl', + resolve: loadAdminBundle, + }) + .when('/admin/orgs', { + templateUrl: 'public/app/features/admin/partials/orgs.html', + controller: 'AdminListOrgsCtrl', + controllerAs: 'ctrl', + resolve: loadAdminBundle, + }) + .when('/admin/orgs/edit/:id', { + templateUrl: 'public/app/features/admin/partials/edit_org.html', + controller: 'AdminEditOrgCtrl', + controllerAs: 'ctrl', + resolve: loadAdminBundle, + }) + .when('/admin/stats', { + templateUrl: 'public/app/features/admin/partials/stats.html', + controller: 'AdminStatsCtrl', + controllerAs: 'ctrl', + resolve: loadAdminBundle, + }) + // LOGIN / SIGNUP + .when('/login', { + templateUrl: 'public/app/partials/login.html', + controller: 'LoginCtrl', + pageClass: 'login-page sidemenu-hidden', + }) + .when('/invite/:code', { + templateUrl: 'public/app/partials/signup_invited.html', + controller: 'InvitedCtrl', + pageClass: 'sidemenu-hidden', + }) + .when('/signup', { + templateUrl: 'public/app/partials/signup_step2.html', + controller: 'SignUpCtrl', + pageClass: 'sidemenu-hidden', + }) + .when('/user/password/send-reset-email', { + templateUrl: 'public/app/partials/reset_password.html', + controller: 'ResetPasswordCtrl', + pageClass: 'sidemenu-hidden', + }) + .when('/user/password/reset', { + templateUrl: 'public/app/partials/reset_password.html', + controller: 'ResetPasswordCtrl', + pageClass: 'sidemenu-hidden', + }) + .when('/dashboard/snapshots', { + templateUrl: 'public/app/features/snapshot/partials/snapshots.html', + controller: 'SnapshotsCtrl', + controllerAs: 'ctrl', + }) + .when('/plugins', { + templateUrl: 'public/app/features/plugins/partials/plugin_list.html', + controller: 'PluginListCtrl', + controllerAs: 'ctrl', + }) + .when('/plugins/:pluginId/edit', { + templateUrl: 'public/app/features/plugins/partials/plugin_edit.html', + controller: 'PluginEditCtrl', + controllerAs: 'ctrl', + }) + .when('/plugins/:pluginId/page/:slug', { + templateUrl: 'public/app/features/plugins/partials/plugin_page.html', + controller: 'AppPageCtrl', + controllerAs: 'ctrl', + }) + .when('/styleguide/:page?', { + controller: 'StyleGuideCtrl', + controllerAs: 'ctrl', + templateUrl: 'public/app/features/styleguide/styleguide.html', + }) + .when('/alerting', { + redirectTo: '/alerting/list', + }) + .when('/alerting/list', { + templateUrl: 'public/app/features/alerting/partials/alert_list.html', + controller: 'AlertListCtrl', + controllerAs: 'ctrl', + resolve: loadAlertingBundle, + }) + .when('/alerting/notifications', { + templateUrl: 'public/app/features/alerting/partials/notifications_list.html', + controller: 'AlertNotificationsListCtrl', + controllerAs: 'ctrl', + resolve: loadAlertingBundle, + }) + .when('/alerting/notification/new', { + templateUrl: 'public/app/features/alerting/partials/notification_edit.html', + controller: 'AlertNotificationEditCtrl', + controllerAs: 'ctrl', + resolve: loadAlertingBundle, + }) + .when('/alerting/notification/:id/edit', { + templateUrl: 'public/app/features/alerting/partials/notification_edit.html', + controller: 'AlertNotificationEditCtrl', + controllerAs: 'ctrl', + resolve: loadAlertingBundle, + }) + .otherwise({ + templateUrl: 'public/app/partials/error.html', + controller: 'ErrorCtrl', + }); } coreModule.config(setupAngularRoutes); diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 1a87ed6096c..ea15077b960 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -14,17 +14,29 @@ export class AlertSrv { } init() { - this.$rootScope.onAppEvent('alert-error', (e, alert) => { - this.set(alert[0], alert[1], 'error', 12000); - }, this.$rootScope); + this.$rootScope.onAppEvent( + 'alert-error', + (e, alert) => { + this.set(alert[0], alert[1], 'error', 12000); + }, + this.$rootScope + ); - this.$rootScope.onAppEvent('alert-warning', (e, alert) => { - this.set(alert[0], alert[1], 'warning', 5000); - }, this.$rootScope); + this.$rootScope.onAppEvent( + 'alert-warning', + (e, alert) => { + this.set(alert[0], alert[1], 'warning', 5000); + }, + this.$rootScope + ); - this.$rootScope.onAppEvent('alert-success', (e, alert) => { - this.set(alert[0], alert[1], 'success', 3000); - }, this.$rootScope); + this.$rootScope.onAppEvent( + 'alert-success', + (e, alert) => { + this.set(alert[0], alert[1], 'success', 3000); + }, + 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)); @@ -34,9 +46,12 @@ export class AlertSrv { getIconForSeverity(severity) { switch (severity) { - case 'success': return 'fa fa-check'; - case 'error': return 'fa fa-exclamation-triangle'; - default: return 'fa fa-exclamation'; + case 'success': + return 'fa fa-check'; + case 'error': + return 'fa fa-exclamation-triangle'; + default: + return 'fa fa-exclamation'; } } @@ -52,7 +67,7 @@ export class AlertSrv { title: title || '', text: text || '', severity: severity || 'info', - icon: this.getIconForSeverity(severity) + icon: this.getIconForSeverity(severity), }; var newAlertJson = angular.toJson(newAlert); @@ -73,7 +88,7 @@ export class AlertSrv { this.$rootScope.$digest(); } - return(newAlert); + return newAlert; } clear(alert) { @@ -104,9 +119,9 @@ export class AlertSrv { scope.onConfirm = payload.onConfirm; scope.onAltAction = payload.onAltAction; scope.altActionText = payload.altActionText; - scope.icon = payload.icon || "fa-check"; - scope.yesText = payload.yesText || "Yes"; - scope.noText = payload.noText || "Cancel"; + scope.icon = payload.icon || 'fa-check'; + scope.yesText = payload.yesText || 'Yes'; + scope.noText = payload.noText || 'Cancel'; scope.confirmTextValid = scope.confirmText ? false : true; var confirmModal = this.$modal({ @@ -115,7 +130,7 @@ export class AlertSrv { modalClass: 'confirm-modal', show: false, scope: scope, - keyboard: false + keyboard: false, }); confirmModal.then(function(modalEl) { diff --git a/public/app/core/services/all.js b/public/app/core/services/all.js index a308febb219..0053d789cbe 100644 --- a/public/app/core/services/all.js +++ b/public/app/core/services/all.js @@ -8,5 +8,6 @@ define([ './segment_srv', './backend_srv', './dynamic_directive_srv', + './global_event_srv' ], function () {}); diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts index 87e84efa706..370773154e5 100644 --- a/public/app/core/services/analytics.ts +++ b/public/app/core/services/analytics.ts @@ -3,14 +3,17 @@ import coreModule from 'app/core/core_module'; import config from 'app/core/config'; export class Analytics { - /** @ngInject */ - constructor(private $rootScope, private $location) { - } + constructor(private $rootScope, private $location) {} gaInit() { $.getScript('https://www.google-analytics.com/analytics.js'); // jQuery shortcut - var ga = (window).ga = (window).ga || function () { (ga.q = ga.q || []).push(arguments); }; ga.l = +new Date; + var ga = ((window).ga = + (window).ga || + function() { + (ga.q = ga.q || []).push(arguments); + }); + ga.l = +new Date(); ga('create', (config).googleAnalyticsId, 'auto'); return ga; } @@ -33,4 +36,3 @@ function startAnalytics(googleAnalyticsSrv) { } coreModule.service('googleAnalyticsSrv', Analytics).run(startAnalytics); - diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 544e6c60de8..5d582116d8e 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -3,6 +3,7 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; +import { DashboardModel } from 'app/features/dashboard/dashboard_model'; export class BackendSrv { private inFlightRequests = {}; @@ -10,8 +11,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 }); @@ -51,22 +51,22 @@ export class BackendSrv { } if (err.status === 422) { - this.alertSrv.set("Validation failed", data.message, "warning", 4000); + this.alertSrv.set('Validation failed', data.message, 'warning', 4000); throw data; } data.severity = 'error'; if (err.status < 500) { - data.severity = "warning"; + data.severity = 'warning'; } if (data.message) { - let description = ""; + let description = ''; let message = data.message; if (message.length > 80) { description = message; - message = "Error"; + message = 'Error'; } this.alertSrv.set(message, description, data.severity, 10000); } @@ -85,32 +85,35 @@ export class BackendSrv { options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId; } - if (options.url.indexOf("/") === 0) { + if (options.url.indexOf('/') === 0) { options.url = options.url.substring(1); } } - return this.$http(options).then(results => { - if (options.method !== 'GET') { - if (results && results.data.message) { - if (options.showSuccessAlert !== false) { - this.alertSrv.set(results.data.message, '', 'success', 3000); + return this.$http(options).then( + results => { + if (options.method !== 'GET') { + if (results && results.data.message) { + if (options.showSuccessAlert !== false) { + this.alertSrv.set(results.data.message, '', 'success', 3000); + } } } - } - return results.data; - }, err => { - // handle unauthorized - if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) { - return this.loginPing().then(() => { - options.retry = 1; - return this.request(options); - }); - } + return results.data; + }, + err => { + // handle unauthorized + if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) { + return this.loginPing().then(() => { + options.retry = 1; + return this.request(options); + }); + } - this.$timeout(this.requestErrorHandler.bind(this, err), 50); - throw err; - }); + this.$timeout(this.requestErrorHandler.bind(this, err), 50); + throw err; + } + ); } addCanceler(requestId, canceler) { @@ -153,7 +156,7 @@ export class BackendSrv { options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId; } - if (options.url.indexOf("/") === 0) { + if (options.url.indexOf('/') === 0) { options.url = options.url.substring(1); } @@ -167,51 +170,53 @@ export class BackendSrv { } } - return this.$http(options).then(response => { - appEvents.emit('ds-request-response', response); - return response; - }).catch(err => { - if (err.status === this.HTTP_REQUEST_CANCELLED) { - throw {err, cancelled: true}; - } + return this.$http(options) + .then(response => { + appEvents.emit('ds-request-response', response); + return response; + }) + .catch(err => { + if (err.status === this.HTTP_REQUEST_CANCELLED) { + throw { err, cancelled: true }; + } - // handle unauthorized for backend requests - if (requestIsLocal && firstAttempt && err.status === 401) { - return this.loginPing().then(() => { - options.retry = 1; - if (canceler) { - canceler.resolve(); - } - return this.datasourceRequest(options); - }); - } + // handle unauthorized for backend requests + if (requestIsLocal && firstAttempt && err.status === 401) { + return this.loginPing().then(() => { + options.retry = 1; + if (canceler) { + canceler.resolve(); + } + return this.datasourceRequest(options); + }); + } - // populate error obj on Internal Error - if (_.isString(err.data) && err.status === 500) { - err.data = { - error: err.statusText, - response: err.data, - }; - } + // populate error obj on Internal Error + if (_.isString(err.data) && err.status === 500) { + err.data = { + error: err.statusText, + response: err.data, + }; + } - // for Prometheus - if (err.data && !err.data.message && _.isString(err.data.error)) { - err.data.message = err.data.error; - } + // for Prometheus + if (err.data && !err.data.message && _.isString(err.data.error)) { + err.data.message = err.data.error; + } - appEvents.emit('ds-request-error', err); - throw err; - - }).finally(() => { - // clean up - if (options.requestId) { - this.inFlightRequests[options.requestId].shift(); - } - }); + appEvents.emit('ds-request-error', err); + throw err; + }) + .finally(() => { + // clean up + if (options.requestId) { + this.inFlightRequests[options.requestId].shift(); + } + }); } loginPing() { - return this.request({url: '/api/login/ping', method: 'GET', retry: 1 }); + return this.request({ url: '/api/login/ping', method: 'GET', retry: 1 }); } search(query) { @@ -223,14 +228,137 @@ export class BackendSrv { } saveDashboard(dash, options) { - options = (options || {}); + options = options || {}; return this.post('/api/dashboards/db/', { dashboard: dash, + folderId: options.folderId, overwrite: options.overwrite === true, message: options.message || '', }); } + + createDashboardFolder(name) { + const dash = { + schemaVersion: 16, + title: name.trim(), + editable: true, + panels: [], + }; + + return this.post('/api/dashboards/db/', { + dashboard: dash, + isFolder: true, + overwrite: false, + }).then(res => { + return this.getDashboard('db', res.slug); + }); + } + + deleteDashboard(slug) { + let deferred = this.$q.defer(); + + this.getDashboard('db', slug).then(fullDash => { + this.delete(`/api/dashboards/db/${slug}`) + .then(() => { + deferred.resolve(fullDash); + }) + .catch(err => { + deferred.reject(err); + }); + }); + + return deferred.promise; + } + + deleteDashboards(dashboardSlugs) { + const tasks = []; + + for (let slug of dashboardSlugs) { + tasks.push(this.createTask(this.deleteDashboard.bind(this), true, slug)); + } + + return this.executeInOrder(tasks, []); + } + + moveDashboards(dashboardSlugs, toFolder) { + const tasks = []; + + for (let slug of dashboardSlugs) { + 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, + }; + }); + } + + private moveDashboard(slug, toFolder) { + let deferred = this.$q.defer(); + + 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) { + deferred.resolve({ alreadyInFolder: true }); + return; + } + + const clone = model.getSaveModelClone(); + let options = { + folderId: toFolder.id, + overwrite: false, + }; + + 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, options) + .then(() => { + deferred.resolve({ succeeded: true }); + }) + .catch(err => { + deferred.resolve({ succeeded: false }); + }); + } else { + deferred.resolve({ succeeded: false }); + } + }); + }); + + return deferred.promise; + } + + private createTask(fn, ignoreRejections, ...args: any[]) { + return result => { + return fn + .apply(null, args) + .then(res => { + return Array.prototype.concat(result, [res]); + }) + .catch(err => { + if (ignoreRejections) { + return result; + } + + throw err; + }); + }; + } + + private executeInOrder(tasks, initialValue) { + return tasks.reduce(this.$q.when, initialValue); + } } coreModule.service('backendSrv', BackendSrv); diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index ddff1093720..5a879895267 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -1,5 +1,3 @@ -/// - import config from 'app/core/config'; import _ from 'lodash'; import coreModule from 'app/core/core_module'; @@ -9,6 +7,7 @@ export class User { isGrafanaAdmin: any; isSignedIn: any; orgRole: any; + orgId: number; timezone: string; helpFlags1: number; lightTheme: boolean; @@ -28,18 +27,16 @@ export class ContextSrv { isGrafanaAdmin: any; isEditor: any; sidemenu: any; + sidemenuSmallBreakpoint = false; constructor() { - this.pinned = store.getBool('grafana.sidemenu.pinned', false); - if (this.pinned) { - this.sidemenu = true; - } + this.sidemenu = store.getBool('grafana.sidemenu', true); if (!config.buildInfo) { config.buildInfo = {}; } if (!config.bootData) { - config.bootData = {user: {}, settings: {}}; + config.bootData = { user: {}, settings: {} }; } this.version = config.buildInfo.version; @@ -53,25 +50,18 @@ export class ContextSrv { return this.user.orgRole === role; } - setPinnedState(val) { - this.pinned = val; - store.set('grafana.sidemenu.pinned', val); - } - isGrafanaVisible() { return !!(document.visibilityState === undefined || document.visibilityState === 'visible'); } toggleSideMenu() { this.sidemenu = !this.sidemenu; - if (!this.sidemenu) { - this.setPinnedState(false); - } + store.set('grafana.sidemenu', this.sidemenu); } } var contextSrv = new ContextSrv(); -export {contextSrv}; +export { contextSrv }; coreModule.factory('contextSrv', function() { return contextSrv; diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index 5565e19a3f6..cfcc70516b6 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -4,7 +4,6 @@ import angular from 'angular'; import coreModule from '../core_module'; class DynamicDirectiveSrv { - /** @ngInject */ constructor(private $compile, private $rootScope) {} @@ -17,22 +16,25 @@ class DynamicDirectiveSrv { } link(scope, elem, attrs, options) { - options.directive(scope).then(directiveInfo => { - if (!directiveInfo || !directiveInfo.fn) { - elem.empty(); - return; - } + options + .directive(scope) + .then(directiveInfo => { + if (!directiveInfo || !directiveInfo.fn) { + elem.empty(); + return; + } - if (!directiveInfo.fn.registered) { - coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); - directiveInfo.fn.registered = true; - } + if (!directiveInfo.fn.registered) { + coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); + directiveInfo.fn.registered = true; + } - this.addDirective(elem, directiveInfo.name, scope); - }).catch(err => { - console.log('Plugin load:', err); - this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]); - }); + this.addDirective(elem, directiveInfo.name, scope); + }) + .catch(err => { + console.log('Plugin load:', err); + this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]); + }); } create(options) { @@ -52,7 +54,7 @@ class DynamicDirectiveSrv { } else { this.link(scope, elem, attrs, options); } - } + }, }; return directiveDef; @@ -60,5 +62,3 @@ class DynamicDirectiveSrv { } coreModule.service('dynamicDirectiveSrv', DynamicDirectiveSrv); - - diff --git a/public/app/core/services/global_event_srv.ts b/public/app/core/services/global_event_srv.ts new file mode 100644 index 00000000000..0569b9933d8 --- /dev/null +++ b/public/app/core/services/global_event_srv.ts @@ -0,0 +1,43 @@ +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/features/dashboard/impression_store.ts b/public/app/core/services/impression_srv.ts similarity index 75% rename from public/app/features/dashboard/impression_store.ts rename to public/app/core/services/impression_srv.ts index 68478aef09a..3945c048876 100644 --- a/public/app/features/dashboard/impression_store.ts +++ b/public/app/core/services/impression_srv.ts @@ -2,7 +2,7 @@ import store from 'app/core/store'; import _ from 'lodash'; import config from 'app/core/config'; -export class ImpressionsStore { +export class ImpressionSrv { constructor() {} addDashboardImpression(dashboardId) { @@ -15,7 +15,7 @@ export class ImpressionsStore { } } - impressions = impressions.filter((imp) => { + impressions = impressions.filter(imp => { return dashboardId !== imp; }); @@ -28,7 +28,7 @@ export class ImpressionsStore { } getDashboardOpened() { - var impressions = store.get(this.impressionKey(config)) || "[]"; + var impressions = store.get(this.impressionKey(config)) || '[]'; impressions = JSON.parse(impressions); @@ -40,12 +40,9 @@ export class ImpressionsStore { } impressionKey(config) { - return "dashboard_impressions-" + config.bootData.user.orgId; + return 'dashboard_impressions-' + config.bootData.user.orgId; } } -var impressions = new ImpressionsStore(); - -export { - impressions -}; +const impressionSrv = new ImpressionSrv(); +export default impressionSrv; diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index a8f36afc468..36fe73b62ce 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -1,5 +1,3 @@ -/// - import $ from 'jquery'; import _ from 'lodash'; @@ -12,10 +10,7 @@ export class KeybindingSrv { helpModal: boolean; /** @ngInject */ - constructor( - private $rootScope, - private $location) { - + constructor(private $rootScope, private $location) { // clear out all shortcuts on route change $rootScope.$on('$routeChangeSuccess', () => { Mousetrap.reset(); @@ -28,54 +23,58 @@ export class KeybindingSrv { setupGlobal() { this.bind(['?', 'h'], this.showHelpModal); - this.bind("g h", this.goToHome); - this.bind("g a", this.openAlerting); - this.bind("g p", this.goToProfile); - this.bind("s s", this.openSearchStarred); + this.bind('g h', this.goToHome); + this.bind('g a', this.openAlerting); + this.bind('g p', this.goToProfile); + this.bind('s s', this.openSearchStarred); this.bind('s o', this.openSearch); this.bind('s t', this.openSearchTags); this.bind('f', this.openSearch); } openSearchStarred() { - this.$rootScope.appEvent('show-dash-search', {starred: true}); + appEvents.emit('show-dash-search', { starred: true }); } openSearchTags() { - this.$rootScope.appEvent('show-dash-search', {tagsMode: true}); + appEvents.emit('show-dash-search', { tagsMode: true }); } openSearch() { - this.$rootScope.appEvent('show-dash-search'); + appEvents.emit('show-dash-search'); } openAlerting() { - this.$location.url("/alerting"); + this.$location.url('/alerting'); } goToHome() { - this.$location.url("/"); + this.$location.url('/'); } goToProfile() { - this.$location.url("/profile"); + this.$location.url('/profile'); } showHelpModal() { - appEvents.emit('show-modal', {templateHtml: ''}); + appEvents.emit('show-modal', { templateHtml: '' }); } bind(keyArg, fn) { - Mousetrap.bind(keyArg, evt => { - evt.preventDefault(); - evt.stopPropagation(); - evt.returnValue = false; - return this.$rootScope.$apply(fn.bind(this)); - }, 'keydown'); + Mousetrap.bind( + keyArg, + evt => { + evt.preventDefault(); + evt.stopPropagation(); + evt.returnValue = false; + return this.$rootScope.$apply(fn.bind(this)); + }, + 'keydown' + ); } - showDashEditView(view) { - var search = _.extend(this.$location.search(), {editview: view}); + showDashEditView() { + var search = _.extend(this.$location.search(), { editview: 'settings' }); this.$location.search(search); } @@ -83,11 +82,7 @@ export class KeybindingSrv { this.bind('mod+o', () => { dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3; appEvents.emit('graph-hover-clear'); - scope.broadcastRefresh(); - }); - - this.bind('mod+h', () => { - dashboard.hideControls = !dashboard.hideControls; + this.$rootScope.$broadcast('refresh'); }); this.bind('mod+s', e => { @@ -117,7 +112,7 @@ export class KeybindingSrv { fullscreen: true, edit: true, panelId: dashboard.meta.focusPanelId, - toggle: true + toggle: true, }); } }); @@ -146,14 +141,14 @@ export class KeybindingSrv { // share panel this.bind('p s', () => { if (dashboard.meta.focusPanelId) { - var shareScope = scope.$new(); + var shareScope = scope.$new(); var panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId); shareScope.panel = panelInfo.panel; shareScope.dashboard = dashboard; appEvents.emit('show-modal', { src: 'public/app/features/dashboard/partials/shareModal.html', - scope: shareScope + scope: shareScope, }); } }); @@ -191,15 +186,15 @@ export class KeybindingSrv { }); this.bind('d n', e => { - this.$location.url("/dashboard/new"); + this.$location.url('/dashboard/new'); }); this.bind('d r', () => { - scope.broadcastRefresh(); + this.$rootScope.$broadcast('refresh'); }); this.bind('d s', () => { - this.showDashEditView('settings'); + this.showDashEditView(); }); this.bind('d k', () => { @@ -217,8 +212,14 @@ export class KeybindingSrv { } scope.appEvent('hide-modal'); - scope.appEvent('hide-dash-editor'); - scope.appEvent('panel-change-view', {fullscreen: false, edit: false}); + scope.appEvent('panel-change-view', { fullscreen: false, edit: false }); + + // close settings view + var search = this.$location.search(); + if (search.editview) { + delete search.editview; + this.$location.search(search); + } }); } } diff --git a/public/app/core/services/ng_react.ts b/public/app/core/services/ng_react.ts new file mode 100644 index 00000000000..3c61412669e --- /dev/null +++ b/public/app/core/services/ng_react.ts @@ -0,0 +1,300 @@ +// +// This is using ng-react with this PR applied https://github.com/ngReact/ngReact/pull/199 +// + +// # ngReact +// ### Use React Components inside of your Angular applications +// +// Composed of +// - reactComponent (generic directive for delegating off to React Components) +// - reactDirective (factory for creating specific directives that correspond to reactComponent directives) + +import React from 'react'; +import ReactDOM from 'react-dom'; +import angular from 'angular'; + +// get a react component from name (components can be an angular injectable e.g. value, factory or +// available on window +function getReactComponent(name, $injector) { + // if name is a function assume it is component and return it + if (angular.isFunction(name)) { + return name; + } + + // a React component name must be specified + if (!name) { + throw new Error('ReactComponent name attribute must be specified'); + } + + // ensure the specified React component is accessible, and fail fast if it's not + var reactComponent; + try { + reactComponent = $injector.get(name); + } catch (e) {} + + if (!reactComponent) { + try { + reactComponent = name.split('.').reduce(function(current, namePart) { + return current[namePart]; + }, window); + } catch (e) {} + } + + if (!reactComponent) { + throw Error('Cannot find react component ' + name); + } + + return reactComponent; +} + +// wraps a function with scope.$apply, if already applied just return +function applied(fn, scope) { + if (fn.wrappedInApply) { + return fn; + } + var wrapped: any = function() { + var args = arguments; + var phase = scope.$root.$$phase; + if (phase === '$apply' || phase === '$digest') { + return fn.apply(null, args); + } else { + return scope.$apply(function() { + return fn.apply(null, args); + }); + } + }; + wrapped.wrappedInApply = true; + return wrapped; +} + +/** + * wraps functions on obj in scope.$apply + * + * keeps backwards compatibility, as if propsConfig is not passed, it will + * work as before, wrapping all functions and won't wrap only when specified. + * + * @version 0.4.1 + * @param obj react component props + * @param scope current scope + * @param propsConfig configuration object for all properties + * @returns {Object} props with the functions wrapped in scope.$apply + */ +function applyFunctions(obj, scope, propsConfig?) { + return Object.keys(obj || {}).reduce(function(prev, key) { + var value = obj[key]; + var config = (propsConfig || {})[key] || {}; + /** + * wrap functions in a function that ensures they are scope.$applied + * 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; + + return prev; + }, {}); +} + +/** + * + * @param watchDepth (value of HTML watch-depth attribute) + * @param scope (angular scope) + * + * Uses the watchDepth attribute to determine how to watch props on scope. + * If watchDepth attribute is NOT reference or collection, watchDepth defaults to deep watching by value + */ +function watchProps(watchDepth, scope, watchExpressions, listener) { + var supportsWatchCollection = angular.isFunction(scope.$watchCollection); + var supportsWatchGroup = angular.isFunction(scope.$watchGroup); + + var watchGroupExpressions = []; + + watchExpressions.forEach(function(expr) { + var actualExpr = getPropExpression(expr); + var exprWatchDepth = getPropWatchDepth(watchDepth, expr); + + if (exprWatchDepth === 'collection' && supportsWatchCollection) { + scope.$watchCollection(actualExpr, listener); + } else if (exprWatchDepth === 'reference' && supportsWatchGroup) { + watchGroupExpressions.push(actualExpr); + } else if (exprWatchDepth === 'one-time') { + //do nothing because we handle our one time bindings after this + } else { + scope.$watch(actualExpr, listener, exprWatchDepth !== 'reference'); + } + }); + + if (watchDepth === 'one-time') { + listener(); + } + + if (watchGroupExpressions.length) { + scope.$watchGroup(watchGroupExpressions, listener); + } +} + +// render React component, with scope[attrs.props] being passed in as the component props +function renderComponent(component, props, scope, elem) { + scope.$evalAsync(function() { + ReactDOM.render(React.createElement(component, props), elem[0]); + }); +} + +// get prop name from prop (string or array) +function getPropName(prop) { + return Array.isArray(prop) ? prop[0] : prop; +} + +// get prop name from prop (string or array) +function getPropConfig(prop) { + return Array.isArray(prop) ? prop[1] : {}; +} + +// get prop expression from prop (string or array) +function getPropExpression(prop) { + return Array.isArray(prop) ? prop[0] : prop; +} + +// find the normalized attribute knowing that React props accept any type of capitalization +function findAttribute(attrs, propName) { + var index = Object.keys(attrs).filter(function(attr) { + return attr.toLowerCase() === propName.toLowerCase(); + })[0]; + return attrs[index]; +} + +// get watch depth of prop (string or array) +function getPropWatchDepth(defaultWatch, prop) { + var customWatchDepth = Array.isArray(prop) && angular.isObject(prop[1]) && prop[1].watchDepth; + return customWatchDepth || defaultWatch; +} + +// # reactComponent +// Directive that allows React components to be used in Angular templates. +// +// Usage: +// +// +// This requires that there exists an injectable or globally available 'Hello' React component. +// The 'props' attribute is optional and is passed to the component. +// +// The following would would create and register the component: +// +// var module = angular.module('ace.react.components'); +// module.value('Hello', React.createClass({ +// render: function() { +// return
    Hello {this.props.name}
    ; +// } +// })); +// +var reactComponent = function($injector) { + return { + restrict: 'E', + replace: true, + link: function(scope, elem, attrs) { + var reactComponent = getReactComponent(attrs.name, $injector); + + var renderMyComponent = function() { + var scopeProps = scope.$eval(attrs.props); + var props = applyFunctions(scopeProps, scope); + + renderComponent(reactComponent, props, scope, elem); + }; + + // If there are props, re-render when they change + attrs.props ? watchProps(attrs.watchDepth, scope, [attrs.props], renderMyComponent) : renderMyComponent(); + + // cleanup when scope is destroyed + scope.$on('$destroy', function() { + if (!attrs.onScopeDestroy) { + ReactDOM.unmountComponentAtNode(elem[0]); + } else { + scope.$eval(attrs.onScopeDestroy, { + unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]), + }); + } + }); + }, + }; +}; + +// # reactDirective +// Factory function to create directives for React components. +// +// With a component like this: +// +// var module = angular.module('ace.react.components'); +// module.value('Hello', React.createClass({ +// render: function() { +// return
    Hello {this.props.name}
    ; +// } +// })); +// +// A directive can be created and registered with: +// +// module.directive('hello', function(reactDirective) { +// return reactDirective('Hello', ['name']); +// }); +// +// Where the first argument is the injectable or globally accessible name of the React component +// and the second argument is an array of property names to be watched and passed to the React component +// as props. +// +// This directive can then be used like this: +// +// +// +var reactDirective = function($injector) { + return function(reactComponentName, props, conf, injectableProps) { + var directive = { + restrict: 'E', + replace: true, + link: function(scope, elem, attrs) { + var reactComponent = getReactComponent(reactComponentName, $injector); + + // if props is not defined, fall back to use the React component's propTypes if present + props = props || Object.keys(reactComponent.propTypes || {}); + + // for each of the properties, get their scope value and set it to scope.props + var renderMyComponent = function() { + var scopeProps = {}, + config = {}; + + props.forEach(function(prop) { + var propName = getPropName(prop); + scopeProps[propName] = scope.$eval(findAttribute(attrs, propName)); + config[propName] = getPropConfig(prop); + }); + + scopeProps = applyFunctions(scopeProps, scope, config); + scopeProps = angular.extend({}, scopeProps, injectableProps); + renderComponent(reactComponent, scopeProps, scope, elem); + }; + + // 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]; + }); + + // If we don't have any props, then our watch statement won't fire. + props.length ? watchProps(attrs.watchDepth, scope, propExpressions, renderMyComponent) : renderMyComponent(); + + // cleanup when scope is destroyed + scope.$on('$destroy', function() { + if (!attrs.onScopeDestroy) { + ReactDOM.unmountComponentAtNode(elem[0]); + } else { + scope.$eval(attrs.onScopeDestroy, { + unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]), + }); + } + }); + }, + }; + return angular.extend(directive, conf); + }; +}; + +let ngModule = angular.module('react', []); +ngModule.directive('reactComponent', ['$injector', reactComponent]); +ngModule.factory('reactDirective', ['$injector', reactDirective]); diff --git a/public/app/core/services/popover_srv.ts b/public/app/core/services/popover_srv.ts index bfbd9e9950e..7c1708b15a7 100644 --- a/public/app/core/services/popover_srv.ts +++ b/public/app/core/services/popover_srv.ts @@ -57,8 +57,8 @@ function popoverSrv($compile, $rootScope, $timeout) { openOn: options.openOn, hoverCloseDelay: 200, tetherOptions: { - constraints: [{to: 'scrollParent', attachment: "none both"}] - } + constraints: [{ to: 'scrollParent', attachment: 'together' }], + }, }); drop.on('close', () => { @@ -68,6 +68,13 @@ function popoverSrv($compile, $rootScope, $timeout) { openDrop = drop; openDrop.open(); }, 100); + + // return close function + return function() { + if (drop) { + drop.close(); + } + }; }; } diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts new file mode 100644 index 00000000000..a909b4af09f --- /dev/null +++ b/public/app/core/services/search_srv.ts @@ -0,0 +1,211 @@ +import _ from 'lodash'; +import coreModule from 'app/core/core_module'; +import impressionSrv from 'app/core/services/impression_srv'; +import store from 'app/core/store'; +import { contextSrv } from 'app/core/services/context_srv'; + +export class SearchSrv { + recentIsOpen: boolean; + starredIsOpen: boolean; + + /** @ngInject */ + constructor(private backendSrv, private $q) { + this.recentIsOpen = store.getBool('search.sections.recent', true); + this.starredIsOpen = store.getBool('search.sections.starred', true); + } + + private getRecentDashboards(sections) { + return this.queryForRecentDashboards().then(result => { + if (result.length > 0) { + sections['recent'] = { + title: 'Recent', + icon: 'fa fa-clock-o', + score: -1, + removable: true, + expanded: this.recentIsOpen, + toggle: this.toggleRecent.bind(this), + items: result, + }; + } + }); + } + + private queryForRecentDashboards() { + var dashIds = _.take(impressionSrv.getDashboardOpened(), 5); + if (dashIds.length === 0) { + return Promise.resolve([]); + } + + return this.backendSrv.search({ dashboardIds: dashIds }).then(result => { + return dashIds + .map(orderId => { + return _.find(result, { id: orderId }); + }) + .filter(hit => hit && !hit.isStarred) + .map(hit => { + return this.transformToViewModel(hit); + }); + }); + } + + private toggleRecent(section) { + this.recentIsOpen = section.expanded = !section.expanded; + store.set('search.sections.recent', this.recentIsOpen); + + if (!section.expanded || section.items.length) { + return Promise.resolve(section); + } + + return this.queryForRecentDashboards().then(result => { + section.items = result; + return Promise.resolve(section); + }); + } + + private toggleStarred(section) { + this.starredIsOpen = section.expanded = !section.expanded; + store.set('search.sections.starred', this.starredIsOpen); + return Promise.resolve(section); + } + + private getStarred(sections) { + if (!contextSrv.isSignedIn) { + return Promise.resolve(); + } + + return this.backendSrv.search({ starred: true, limit: 5 }).then(result => { + if (result.length > 0) { + sections['starred'] = { + title: 'Starred', + icon: 'fa fa-star-o', + score: -2, + expanded: this.starredIsOpen, + toggle: this.toggleStarred.bind(this), + items: result.map(this.transformToViewModel), + }; + } + }); + } + + private transformToViewModel(hit) { + hit.url = 'dashboard/db/' + hit.slug; + return hit; + } + + search(options) { + let sections: any = {}; + let promises = []; + let query = _.clone(options); + let hasFilters = + options.query || + (options.tag && options.tag.length > 0) || + options.starred || + (options.folderIds && options.folderIds.length > 0); + + if (!options.skipRecent && !hasFilters) { + promises.push(this.getRecentDashboards(sections)); + } + + if (!options.skipStarred && !hasFilters) { + promises.push(this.getStarred(sections)); + } + + query.folderIds = query.folderIds || []; + if (!hasFilters) { + query.folderIds = [0]; + } + + promises.push( + this.backendSrv.search(query).then(results => { + return this.handleSearchResult(sections, results); + }) + ); + + return this.$q.all(promises).then(() => { + return _.sortBy(_.values(sections), 'score'); + }); + } + + private handleSearchResult(sections, results) { + if (results.length === 0) { + return sections; + } + + // create folder index + for (let hit of results) { + if (hit.type === 'dash-folder') { + sections[hit.id] = { + id: hit.id, + title: hit.title, + expanded: false, + items: [], + toggle: this.toggleFolder.bind(this), + url: `dashboards/folder/${hit.id}/${hit.slug}`, + slug: hit.slug, + icon: 'fa fa-folder', + score: _.keys(sections).length, + }; + } + } + + for (let hit of results) { + if (hit.type === 'dash-folder') { + continue; + } + + let section = sections[hit.folderId || 0]; + if (!section) { + if (hit.folderId) { + section = { + id: hit.folderId, + title: hit.folderTitle, + url: `dashboards/folder/${hit.folderId}/${hit.folderSlug}`, + slug: hit.slug, + items: [], + icon: 'fa fa-folder-open', + toggle: this.toggleFolder.bind(this), + score: _.keys(sections).length, + }; + } else { + section = { + id: 0, + title: 'Root', + items: [], + icon: 'fa fa-folder-open', + toggle: this.toggleFolder.bind(this), + score: _.keys(sections).length, + }; + } + // add section + sections[hit.folderId || 0] = section; + } + + section.expanded = true; + section.items.push(this.transformToViewModel(hit)); + } + } + + private toggleFolder(section) { + section.expanded = !section.expanded; + section.icon = section.expanded ? 'fa fa-folder-open' : 'fa fa-folder'; + + if (section.items.length) { + return Promise.resolve(section); + } + + let query = { + folderIds: [section.id], + }; + + return this.backendSrv.search(query).then(results => { + section.items = _.map(results, this.transformToViewModel); + return Promise.resolve(section); + }); + } + + getDashboardTags() { + return this.backendSrv.get('/api/dashboards/tags'); + } +} + +coreModule.service('searchSrv', SearchSrv); diff --git a/public/app/core/services/timer.ts b/public/app/core/services/timer.ts index 6355105ee0e..8052b3f2e2c 100644 --- a/public/app/core/services/timer.ts +++ b/public/app/core/services/timer.ts @@ -7,8 +7,7 @@ export class Timer { timers = []; /** @ngInject */ - constructor(private $timeout) { - } + constructor(private $timeout) {} register(promise) { this.timers.push(promise); diff --git a/public/app/core/services/util_srv.ts b/public/app/core/services/util_srv.ts index dca016e1391..2a7dbe3a684 100644 --- a/public/app/core/services/util_srv.ts +++ b/public/app/core/services/util_srv.ts @@ -7,8 +7,7 @@ export class UtilSrv { modalScope: any; /** @ngInject */ - constructor(private $rootScope, private $modal) { - } + constructor(private $rootScope, private $modal) {} init() { appEvents.on('show-modal', this.showModal.bind(this), this.$rootScope); @@ -43,7 +42,7 @@ export class UtilSrv { show: false, scope: this.modalScope, keyboard: false, - backdrop: options.backdrop + backdrop: options.backdrop, }); Promise.resolve(modal).then(function(modalEl) { diff --git a/public/app/core/specs/backend_srv_specs.ts b/public/app/core/specs/backend_srv_specs.ts index 0e78007f210..74b058b98c8 100644 --- a/public/app/core/specs/backend_srv_specs.ts +++ b/public/app/core/specs/backend_srv_specs.ts @@ -1,28 +1,30 @@ -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() { var _backendSrv; - var _http; var _httpBackend; beforeEach(angularMocks.module('grafana.core')); beforeEach(angularMocks.module('grafana.services')); - beforeEach(angularMocks.inject(function ($httpBackend, $http, backendSrv) { - _httpBackend = $httpBackend; - _http = $http; - _backendSrv = backendSrv; - })); + beforeEach( + angularMocks.inject(function($httpBackend, $http, backendSrv) { + _httpBackend = $httpBackend; + _backendSrv = backendSrv; + }) + ); describe('when handling errors', function() { it('should return the http status code', function(done) { _httpBackend.whenGET('gateway-error').respond(502); - _backendSrv.datasourceRequest({ - url: 'gateway-error' - }).catch(function(err) { - expect(err.status).to.be(502); - done(); - }); + _backendSrv + .datasourceRequest({ + url: 'gateway-error', + }) + .catch(function(err) { + expect(err.status).to.be(502); + done(); + }); _httpBackend.flush(); }); }); diff --git a/public/app/core/specs/datemath.jest.ts b/public/app/core/specs/datemath.jest.ts index e2bdebcebce..820c53486db 100644 --- a/public/app/core/specs/datemath.jest.ts +++ b/public/app/core/specs/datemath.jest.ts @@ -4,9 +4,9 @@ import * as dateMath from 'app/core/utils/datemath'; import moment from 'moment'; import _ from 'lodash'; -describe("DateMath", () => { +describe('DateMath', () => { var spans = ['s', 'm', 'h', 'd', 'w', 'M', 'y']; - var anchor = '2014-01-01T06:06:06.666Z'; + var anchor = '2014-01-01T06:06:06.666Z'; var unix = moment(anchor).valueOf(); var format = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; var clock; @@ -35,7 +35,7 @@ describe("DateMath", () => { }); }); - it("now/d should set to start of current day", () => { + it('now/d should set to start of current day', () => { var expected = new Date(); expected.setHours(0); expected.setMinutes(0); @@ -46,7 +46,7 @@ describe("DateMath", () => { expect(startOfDay).toBe(expected.getTime()); }); - it("now/d on a utc dashboard should be start of the current day in UTC time", () => { + 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)); @@ -64,9 +64,9 @@ describe("DateMath", () => { anchored = moment(anchor); }); - _.each(spans, (span) => { + _.each(spans, span => { var nowEx = 'now-5' + span; - var thenEx = anchor + '||-5' + span; + var thenEx = anchor + '||-5' + span; it('should return 5' + span + ' ago', () => { expect(dateMath.parse(nowEx).format(format)).toEqual(now.subtract(5, span).format(format)); @@ -84,20 +84,18 @@ describe("DateMath", () => { describe('rounding', () => { var now; - var anchored; beforeEach(() => { clock = sinon.useFakeTimers(unix); now = moment(); - anchored = moment(anchor); }); - _.each(spans, (span) => { - it('should round now to the beginning of the ' + span, function () { + _.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)); }); - it('should round now to the end of the ' + span, function () { + 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)); }); }); @@ -132,7 +130,4 @@ describe("DateMath", () => { expect(date).toEqual(undefined); }); }); - }); - - diff --git a/public/app/core/specs/emitter.jest.ts b/public/app/core/specs/emitter.jest.ts index 1de59852fcc..a819cf0ede2 100644 --- a/public/app/core/specs/emitter.jest.ts +++ b/public/app/core/specs/emitter.jest.ts @@ -1,9 +1,7 @@ -import {Emitter} from '../utils/emitter'; - -describe("Emitter", () => { +import { Emitter } from '../utils/emitter'; +describe('Emitter', () => { describe('given 2 subscribers', () => { - it('should notfiy subscribers', () => { var events = new Emitter(); var sub1Called = false; @@ -45,20 +43,22 @@ describe("Emitter", () => { events.on('test', () => { sub1Called++; - throw {message: "hello"}; + throw { message: 'hello' }; }); events.on('test', () => { sub2Called++; }); - try { events.emit('test', null); } catch (_) { } - try { events.emit('test', null); } catch (_) {} + try { + events.emit('test', null); + } catch (_) {} + try { + events.emit('test', null); + } catch (_) {} expect(sub1Called).toBe(2); expect(sub2Called).toBe(0); }); }); }); - - diff --git a/public/app/core/specs/flatten.jest.ts b/public/app/core/specs/flatten.jest.ts index 7fe07128d1e..7c7f4816d94 100644 --- a/public/app/core/specs/flatten.jest.ts +++ b/public/app/core/specs/flatten.jest.ts @@ -1,22 +1,22 @@ import flatten from 'app/core/utils/flatten'; -describe("flatten", () => { - +describe('flatten', () => { it('should return flatten object', () => { - var flattened = flatten({ - level1: 'level1-value', - deeper: { - level2: 'level2-value', + var flattened = flatten( + { + level1: 'level1-value', deeper: { - level3: 'level3-value' - } - } - }, null); + level2: 'level2-value', + deeper: { + level3: 'level3-value', + }, + }, + }, + null + ); expect(flattened['level1']).toBe('level1-value'); expect(flattened['deeper.level2']).toBe('level2-value'); expect(flattened['deeper.deeper.level3']).toBe('level3-value'); }); - }); - diff --git a/public/app/core/specs/global_event_srv.jest.ts b/public/app/core/specs/global_event_srv.jest.ts new file mode 100644 index 00000000000..ba318b81cc7 --- /dev/null +++ b/public/app/core/specs/global_event_srv.jest.ts @@ -0,0 +1,23 @@ +import { GlobalEventSrv } from 'app/core/services/global_event_srv'; +import { beforeEach } from 'test/lib/common'; + +jest.mock('app/core/config', () => { + return { + appSubUrl: '/subUrl', + }; +}); + +describe('GlobalEventSrv', () => { + let searchSrv; + + beforeEach(() => { + searchSrv = new GlobalEventSrv(null, null, null); + }); + + describe('With /subUrl as appSubUrl', () => { + it('/subUrl should be stripped', () => { + const urlWithoutMaster = searchSrv.stripBaseFromUrl('/subUrl/grafana/'); + expect(urlWithoutMaster).toBe('/grafana/'); + }); + }); +}); diff --git a/public/app/core/specs/kbn.jest.ts b/public/app/core/specs/kbn.jest.ts index 9f50a05d5ad..4ba0e623784 100644 --- a/public/app/core/specs/kbn.jest.ts +++ b/public/app/core/specs/kbn.jest.ts @@ -5,9 +5,7 @@ import moment from 'moment'; describe('unit format menu', function() { var menu = kbn.getUnitFormats(); menu.map(function(submenu) { - describe('submenu ' + submenu.text, function() { - it('should have a title', function() { expect(typeof submenu.text).toBe('string'); }); @@ -18,8 +16,12 @@ describe('unit format menu', function() { submenu.submenu.map(function(entry) { describe('entry ' + entry.text, function() { - it('should have a title', function() { expect(typeof entry.text).toBe('string'); }); - it('should have a format', function() { expect(typeof entry.value).toBe('string'); }); + it('should have a title', function() { + expect(typeof entry.text).toBe('string'); + }); + it('should have a format', function() { + expect(typeof entry.value).toBe('string'); + }); it('should have a valid format', function() { expect(typeof kbn.valueFormats[entry.value]).toBe('function'); }); @@ -30,7 +32,6 @@ 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); @@ -38,7 +39,6 @@ function describeValueFormat(desc, value, tickSize, tickDecimals, result) { expect(str).toBe(result); }); }); - } describeValueFormat('ms', 0.0024, 0.0005, 4, '0.0024 ms'); @@ -53,7 +53,7 @@ describeValueFormat('none', 2.75e-10, 0, 10, '3e-10'); describeValueFormat('none', 0, 0, 2, '0'); describeValueFormat('dB', 10, 1000, 2, '10.00 dB'); -describeValueFormat('percent', 0, 0, 0, '0%'); +describeValueFormat('percent', 0, 0, 0, '0%'); describeValueFormat('percent', 53, 0, 1, '53.0%'); describeValueFormat('percentunit', 0.0, 0, 0, '0%'); describeValueFormat('percentunit', 0.278, 0, 1, '27.8%'); @@ -63,7 +63,7 @@ describeValueFormat('currencyUSD', 7.42, 10000, 2, '$7.42'); describeValueFormat('currencyUSD', 1532.82, 1000, 1, '$1.53K'); describeValueFormat('currencyUSD', 18520408.7, 10000000, 0, '$19M'); -describeValueFormat('bytes', -1.57e+308, -1.57e+308, 2, 'NA'); +describeValueFormat('bytes', -1.57e308, -1.57e308, 2, 'NA'); describeValueFormat('ns', 25, 1, 0, '25 ns'); describeValueFormat('ns', 2558, 50, 0, '2.56 µs'); @@ -109,7 +109,7 @@ describe('date time formats', function() { it('should format as iso date and skip date when today', function() { var now = moment(); var str = kbn.valueFormats.dateTimeAsIso(now.valueOf(), 1); - expect(str).toBe(now.format("HH:mm:ss")); + expect(str).toBe(now.format('HH:mm:ss')); }); it('should format as US date', function() { @@ -120,7 +120,7 @@ describe('date time formats', function() { it('should format as US date and skip date when today', function() { var now = moment(); var str = kbn.valueFormats.dateTimeAsUS(now.valueOf(), 1); - expect(str).toBe(now.format("h:mm:ss a")); + expect(str).toBe(now.format('h:mm:ss a')); }); it('should format as from now with days', function() { @@ -190,7 +190,7 @@ describe('calculateInterval', function() { }); it('fixed user min interval', function() { - var range = {from: dateMath.parse('now-10m'), to: dateMath.parse('now')}; + var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; var res = kbn.calculateInterval(range, 1600, '10s'); expect(res.interval).toBe('10s'); expect(res.intervalMs).toBe(10000); @@ -203,7 +203,7 @@ describe('calculateInterval', function() { }); it('large time range and user low limit', function() { - var range = {from: dateMath.parse('now-14d'), to: dateMath.parse('now')}; + var range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') }; var res = kbn.calculateInterval(range, 1000, '>10s'); expect(res.interval).toBe('20m'); }); @@ -222,7 +222,10 @@ describe('calculateInterval', function() { }); it('86399s 1 resolution', function() { - var range = { from: dateMath.parse('now-86390s'), to: dateMath.parse('now') }; + var range = { + from: dateMath.parse('now-86390s'), + to: dateMath.parse('now'), + }; var res = kbn.calculateInterval(range, 1, null); expect(res.interval).toBe('12h'); expect(res.intervalMs).toBe(43200000); @@ -254,11 +257,11 @@ describe('hex', function() { describe('hex 0x', function() { it('positive integeter', function() { - var str = kbn.valueFormats.hex0x(7999,0); + var str = kbn.valueFormats.hex0x(7999, 0); expect(str).toBe('0x1F3F'); }); it('negative integer', function() { - var str = kbn.valueFormats.hex0x(-584,0); + var str = kbn.valueFormats.hex0x(-584, 0); expect(str).toBe('-0x248'); }); it('null', function() { @@ -277,71 +280,71 @@ describe('hex 0x', function() { describe('duration', function() { it('null', function() { - var str = kbn.toDuration(null, 0, "millisecond"); + var str = kbn.toDuration(null, 0, 'millisecond'); expect(str).toBe(''); }); it('0 milliseconds', function() { - var str = kbn.toDuration(0, 0, "millisecond"); + var str = kbn.toDuration(0, 0, 'millisecond'); expect(str).toBe('0 milliseconds'); }); it('1 millisecond', function() { - var str = kbn.toDuration(1, 0, "millisecond"); + var str = kbn.toDuration(1, 0, 'millisecond'); expect(str).toBe('1 millisecond'); }); it('-1 millisecond', function() { - var str = kbn.toDuration(-1, 0, "millisecond"); + var str = kbn.toDuration(-1, 0, 'millisecond'); expect(str).toBe('1 millisecond ago'); }); it('seconds', function() { - var str = kbn.toDuration(1, 0, "second"); + var str = kbn.toDuration(1, 0, 'second'); expect(str).toBe('1 second'); }); it('minutes', function() { - var str = kbn.toDuration(1, 0, "minute"); + var str = kbn.toDuration(1, 0, 'minute'); expect(str).toBe('1 minute'); }); it('hours', function() { - var str = kbn.toDuration(1, 0, "hour"); + var str = kbn.toDuration(1, 0, 'hour'); expect(str).toBe('1 hour'); }); it('days', function() { - var str = kbn.toDuration(1, 0, "day"); + var str = kbn.toDuration(1, 0, 'day'); expect(str).toBe('1 day'); }); it('weeks', function() { - var str = kbn.toDuration(1, 0, "week"); + var str = kbn.toDuration(1, 0, 'week'); expect(str).toBe('1 week'); }); it('months', function() { - var str = kbn.toDuration(1, 0, "month"); + var str = kbn.toDuration(1, 0, 'month'); expect(str).toBe('1 month'); }); it('years', function() { - var str = kbn.toDuration(1, 0, "year"); + var str = kbn.toDuration(1, 0, 'year'); expect(str).toBe('1 year'); }); it('decimal days', function() { - var str = kbn.toDuration(1.5, 2, "day"); + var str = kbn.toDuration(1.5, 2, 'day'); expect(str).toBe('1 day, 12 hours, 0 minutes'); }); it('decimal months', function() { - var str = kbn.toDuration(1.5, 3, "month"); + var str = kbn.toDuration(1.5, 3, 'month'); expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours'); }); it('no decimals', function() { - var str = kbn.toDuration(38898367008, 0, "millisecond"); + var str = kbn.toDuration(38898367008, 0, 'millisecond'); expect(str).toBe('1 year'); }); it('1 decimal', function() { - var str = kbn.toDuration(38898367008, 1, "millisecond"); + var str = kbn.toDuration(38898367008, 1, 'millisecond'); expect(str).toBe('1 year, 2 months'); }); it('too many decimals', function() { - var str = kbn.toDuration(38898367008, 20, "millisecond"); + 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'); }); it('floating point error', function() { - var str = kbn.toDuration(36993906007, 8, "millisecond"); + 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'); }); }); diff --git a/public/app/core/specs/manage_dashboards.jest.ts b/public/app/core/specs/manage_dashboards.jest.ts new file mode 100644 index 00000000000..bd257c32f9b --- /dev/null +++ b/public/app/core/specs/manage_dashboards.jest.ts @@ -0,0 +1,571 @@ +import { ManageDashboardsCtrl } from 'app/core/components/manage_dashboards/manage_dashboards'; +import { SearchSrv } from 'app/core/services/search_srv'; +import q from 'q'; + +describe('ManageDashboards', () => { + let ctrl; + + describe('when browsing dashboards', () => { + beforeEach(() => { + const response = [ + { + id: 410, + title: 'afolder', + type: 'dash-folder', + items: [ + { + id: 399, + title: 'Dashboard Test', + url: 'dashboard/db/dashboard-test', + icon: 'fa fa-folder', + tags: [], + isStarred: false, + folderId: 410, + folderTitle: 'afolder', + folderSlug: 'afolder', + }, + ], + tags: [], + isStarred: false, + }, + { + id: 0, + title: 'Root', + icon: 'fa fa-folder-open', + uri: 'db/something-else', + type: 'dash-db', + items: [ + { + id: 500, + title: 'Dashboard Test', + url: 'dashboard/db/dashboard-test', + icon: 'fa fa-folder', + tags: [], + isStarred: false, + }, + ], + tags: [], + isStarred: false, + }, + ]; + ctrl = createCtrlWithStubs(response); + return ctrl.getDashboards(); + }); + + it('should set checked to false on all sections and children', () => { + expect(ctrl.sections.length).toEqual(2); + expect(ctrl.sections[0].checked).toEqual(false); + expect(ctrl.sections[0].items[0].checked).toEqual(false); + expect(ctrl.sections[1].checked).toEqual(false); + expect(ctrl.sections[1].items[0].checked).toEqual(false); + expect(ctrl.sections[0].hideHeader).toBeFalsy(); + }); + }); + + describe('when browsing dashboards for a folder', () => { + beforeEach(() => { + const response = [ + { + id: 410, + title: 'afolder', + type: 'dash-folder', + items: [ + { + id: 399, + title: 'Dashboard Test', + url: 'dashboard/db/dashboard-test', + icon: 'fa fa-folder', + tags: [], + isStarred: false, + folderId: 410, + folderTitle: 'afolder', + folderSlug: 'afolder', + }, + ], + tags: [], + isStarred: false, + }, + ]; + ctrl = createCtrlWithStubs(response); + ctrl.folderId = 410; + return ctrl.getDashboards(); + }); + + it('should set hide header to true on section', () => { + expect(ctrl.sections[0].hideHeader).toBeTruthy(); + }); + }); + + describe('when searching dashboards', () => { + beforeEach(() => { + const response = [ + { + checked: false, + expanded: true, + hideHeader: true, + items: [ + { + id: 399, + title: 'Dashboard Test', + url: 'dashboard/db/dashboard-test', + icon: 'fa fa-folder', + tags: [], + isStarred: false, + folderId: 410, + folderTitle: 'afolder', + folderSlug: 'afolder', + }, + { + id: 500, + title: 'Dashboard Test', + url: 'dashboard/db/dashboard-test', + icon: 'fa fa-folder', + tags: [], + folderId: 499, + isStarred: false, + }, + ], + }, + ]; + + ctrl = createCtrlWithStubs(response); + }); + + describe('with query filter', () => { + beforeEach(() => { + ctrl.query.query = 'd'; + ctrl.canMove = true; + ctrl.canDelete = true; + ctrl.selectAllChecked = true; + return ctrl.getDashboards(); + }); + + it('should set checked to false on all sections and children', () => { + expect(ctrl.sections.length).toEqual(1); + expect(ctrl.sections[0].checked).toEqual(false); + expect(ctrl.sections[0].items[0].checked).toEqual(false); + expect(ctrl.sections[0].items[1].checked).toEqual(false); + }); + + it('should uncheck select all', () => { + expect(ctrl.selectAllChecked).toBeFalsy(); + }); + + it('should disable Move To button', () => { + expect(ctrl.canMove).toBeFalsy(); + }); + + it('should disable delete button', () => { + expect(ctrl.canDelete).toBeFalsy(); + }); + + it('should have active filters', () => { + expect(ctrl.hasFilters).toBeTruthy(); + }); + + describe('when select all is checked', () => { + beforeEach(() => { + ctrl.selectAllChecked = true; + ctrl.onSelectAllChanged(); + }); + + it('should select all dashboards', () => { + expect(ctrl.sections[0].checked).toBeFalsy(); + expect(ctrl.sections[0].items[0].checked).toBeTruthy(); + expect(ctrl.sections[0].items[1].checked).toBeTruthy(); + }); + + it('should enable Move To button', () => { + expect(ctrl.canMove).toBeTruthy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + + describe('when clearing filters', () => { + beforeEach(() => { + return ctrl.clearFilters(); + }); + + it('should reset query filter', () => { + expect(ctrl.query.query).toEqual(''); + }); + }); + }); + }); + + describe('with tag filter', () => { + beforeEach(() => { + return ctrl.filterByTag('test'); + }); + + it('should set tag filter', () => { + expect(ctrl.sections.length).toEqual(1); + expect(ctrl.query.tag[0]).toEqual('test'); + }); + + it('should have active filters', () => { + expect(ctrl.hasFilters).toBeTruthy(); + }); + + describe('when clearing filters', () => { + beforeEach(() => { + return ctrl.clearFilters(); + }); + + it('should reset tag filter', () => { + expect(ctrl.query.tag.length).toEqual(0); + }); + }); + }); + + describe('with starred filter', () => { + beforeEach(() => { + const yesOption: any = ctrl.starredFilterOptions[1]; + + ctrl.selectedStarredFilter = yesOption; + return ctrl.onStarredFilterChange(); + }); + + it('should set starred filter', () => { + expect(ctrl.sections.length).toEqual(1); + expect(ctrl.query.starred).toEqual(true); + }); + + it('should have active filters', () => { + expect(ctrl.hasFilters).toBeTruthy(); + }); + + describe('when clearing filters', () => { + beforeEach(() => { + return ctrl.clearFilters(); + }); + + it('should reset starred filter', () => { + expect(ctrl.query.starred).toEqual(false); + }); + }); + }); + }); + + describe('when selecting dashboards', () => { + let ctrl; + + beforeEach(() => { + ctrl = createCtrlWithStubs([]); + }); + + describe('and no dashboards are selected', () => { + beforeEach(() => { + ctrl.sections = [ + { + id: 1, + items: [{ id: 2, checked: false }], + checked: false, + }, + { + id: 0, + items: [{ id: 3, checked: false }], + checked: false, + }, + ]; + ctrl.selectionChanged(); + }); + + it('should disable Move To button', () => { + expect(ctrl.canMove).toBeFalsy(); + }); + + it('should disable delete button', () => { + expect(ctrl.canDelete).toBeFalsy(); + }); + + describe('when select all is checked', () => { + beforeEach(() => { + ctrl.selectAllChecked = true; + ctrl.onSelectAllChanged(); + }); + + it('should select all folders and dashboards', () => { + expect(ctrl.sections[0].checked).toBeTruthy(); + expect(ctrl.sections[0].items[0].checked).toBeTruthy(); + expect(ctrl.sections[1].checked).toBeTruthy(); + expect(ctrl.sections[1].items[0].checked).toBeTruthy(); + }); + + it('should enable Move To button', () => { + expect(ctrl.canMove).toBeTruthy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + }); + }); + + describe('and all folders and dashboards are selected', () => { + beforeEach(() => { + ctrl.sections = [ + { + id: 1, + items: [{ id: 2, checked: true }], + checked: true, + }, + { + id: 0, + items: [{ id: 3, checked: true }], + checked: true, + }, + ]; + ctrl.selectionChanged(); + }); + + it('should enable Move To button', () => { + expect(ctrl.canMove).toBeTruthy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + + describe('when select all is unchecked', () => { + beforeEach(() => { + ctrl.selectAllChecked = false; + ctrl.onSelectAllChanged(); + }); + + it('should uncheck all checked folders and dashboards', () => { + expect(ctrl.sections[0].checked).toBeFalsy(); + expect(ctrl.sections[0].items[0].checked).toBeFalsy(); + expect(ctrl.sections[1].checked).toBeFalsy(); + expect(ctrl.sections[1].items[0].checked).toBeFalsy(); + }); + + it('should disable Move To button', () => { + expect(ctrl.canMove).toBeFalsy(); + }); + + it('should disable delete button', () => { + expect(ctrl.canDelete).toBeFalsy(); + }); + }); + }); + + describe('and one dashboard in root is selected', () => { + beforeEach(() => { + ctrl.sections = [ + { + id: 1, + title: 'folder', + items: [{ id: 2, checked: false }], + checked: false, + }, + { + id: 0, + title: 'Root', + items: [{ id: 3, checked: true }], + checked: false, + }, + ]; + ctrl.selectionChanged(); + }); + + it('should enable Move To button', () => { + expect(ctrl.canMove).toBeTruthy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + }); + + describe('and one child dashboard is selected', () => { + beforeEach(() => { + ctrl.sections = [ + { + id: 1, + title: 'folder', + items: [{ id: 2, checked: true }], + checked: false, + }, + { + id: 0, + title: 'Root', + items: [{ id: 3, checked: false }], + checked: false, + }, + ]; + + ctrl.selectionChanged(); + }); + + it('should enable Move To button', () => { + expect(ctrl.canMove).toBeTruthy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + }); + + describe('and one child dashboard and one dashboard is selected', () => { + beforeEach(() => { + ctrl.sections = [ + { + id: 1, + title: 'folder', + items: [{ id: 2, checked: true }], + checked: false, + }, + { + id: 0, + title: 'Root', + items: [{ id: 3, checked: true }], + checked: false, + }, + ]; + + ctrl.selectionChanged(); + }); + + it('should enable Move To button', () => { + expect(ctrl.canMove).toBeTruthy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + }); + + describe('and one child dashboard and one folder is selected', () => { + beforeEach(() => { + ctrl.sections = [ + { + id: 1, + title: 'folder', + items: [{ id: 2, checked: false }], + checked: true, + }, + { + id: 3, + title: 'folder', + items: [{ id: 4, checked: true }], + checked: false, + }, + { + id: 0, + title: 'Root', + items: [{ id: 3, checked: false }], + checked: false, + }, + ]; + + ctrl.selectionChanged(); + }); + + it('should enable Move To button', () => { + expect(ctrl.canMove).toBeTruthy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + }); + }); + + describe('when deleting dashboards', () => { + let toBeDeleted: any; + + beforeEach(() => { + ctrl = createCtrlWithStubs([]); + + ctrl.sections = [ + { + id: 1, + title: 'folder', + items: [{ id: 2, checked: true, slug: 'folder-dash' }], + checked: true, + slug: 'folder', + }, + { + id: 3, + title: 'folder-2', + items: [{ id: 3, checked: true, slug: 'folder-2-dash' }], + checked: false, + slug: 'folder-2', + }, + { + id: 0, + title: 'Root', + items: [{ id: 3, checked: true, slug: 'root-dash' }], + checked: true, + }, + ]; + + toBeDeleted = ctrl.getFoldersAndDashboardsToDelete(); + }); + + it('should return 1 folder', () => { + expect(toBeDeleted.folders.length).toEqual(1); + }); + + it('should return 2 dashboards', () => { + expect(toBeDeleted.dashboards.length).toEqual(2); + }); + + it('should filter out children if parent is checked', () => { + expect(toBeDeleted.folders[0]).toEqual('folder'); + }); + + it('should not filter out children if parent not is checked', () => { + expect(toBeDeleted.dashboards[0]).toEqual('folder-2-dash'); + }); + + it('should not filter out children if parent is checked and root', () => { + expect(toBeDeleted.dashboards[1]).toEqual('root-dash'); + }); + }); + + describe('when moving dashboards', () => { + beforeEach(() => { + ctrl = createCtrlWithStubs([]); + + ctrl.sections = [ + { + id: 1, + title: 'folder', + items: [{ id: 2, checked: true, slug: 'dash' }], + checked: false, + slug: 'folder', + }, + { + id: 0, + title: 'Root', + items: [{ id: 3, checked: true, slug: 'dash-2' }], + checked: false, + }, + ]; + }); + + it('should get selected dashboards', () => { + const toBeMove = ctrl.getDashboardsToMove(); + expect(toBeMove.length).toEqual(2); + expect(toBeMove[0]).toEqual('dash'); + expect(toBeMove[1]).toEqual('dash-2'); + }); + }); +}); + +function createCtrlWithStubs(searchResponse: any, tags?: any) { + const searchSrvStub = { + search: (options: any) => { + return q.resolve(searchResponse); + }, + getDashboardTags: () => { + return q.resolve(tags || []); + }, + }; + + 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 new file mode 100644 index 00000000000..06172604069 --- /dev/null +++ b/public/app/core/specs/org_switcher.jest.ts @@ -0,0 +1,42 @@ +import { OrgSwitchCtrl } from '../components/org_switcher'; +import q from 'q'; + +jest.mock('app/core/services/context_srv', () => ({ + contextSrv: { + user: { orgId: 1 }, + }, +})); + +describe('OrgSwitcher', () => { + describe('when switching org', () => { + let expectedHref; + let expectedUsingUrl; + + beforeEach(() => { + const backendSrvStub: any = { + get: url => { + return q.resolve([]); + }, + post: url => { + expectedUsingUrl = url; + return q.resolve({}); + }, + }; + + const orgSwitcherCtrl = new OrgSwitchCtrl(backendSrvStub); + + orgSwitcherCtrl.getWindowLocationHref = () => 'http://localhost:3000?orgId=1&from=now-3h&to=now'; + orgSwitcherCtrl.setWindowLocationHref = href => (expectedHref = href); + + return orgSwitcherCtrl.setUsingOrg({ orgId: 2 }); + }); + + it('should switch orgId in call to backend', () => { + expect(expectedUsingUrl).toBe('/api/user/using/2'); + }); + + it('should switch orgId in url', () => { + 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 eb9dcfc3ace..6bfc0503900 100644 --- a/public/app/core/specs/rangeutil.jest.ts +++ b/public/app/core/specs/rangeutil.jest.ts @@ -2,17 +2,16 @@ import * as rangeUtil from 'app/core/utils/rangeutil'; import _ from 'lodash'; import moment from 'moment'; -describe("rangeUtil", () => { - - describe("Can get range grouped list of ranges", () => { +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); }); }); - describe("Can get range text described", () => { + describe('Can get range text described', () => { it('should handle simple old expression with only amount and unit', () => { var info = rangeUtil.describeTextRange('5m'); expect(info.display).toBe('Last 5 minutes'); @@ -57,52 +56,62 @@ describe("rangeUtil", () => { }); }); - describe("Can get date range described", () => { + describe('Can get date range described', () => { it('Date range with simple ranges', () => { - var text = rangeUtil.describeTimeRange({from: 'now-1h', to: 'now'}); + var text = rangeUtil.describeTimeRange({ from: 'now-1h', to: 'now' }); expect(text).toBe('Last 1 hour'); }); it('Date range with rounding ranges', () => { - var text = rangeUtil.describeTimeRange({from: 'now/d+6h', to: 'now'}); + var text = rangeUtil.describeTimeRange({ from: 'now/d+6h', to: 'now' }); expect(text).toBe('now/d+6h to now'); }); it('Date range with absolute to now', () => { - var text = rangeUtil.describeTimeRange({from: moment([2014,10,10,2,3,4]), to: 'now'}); + var text = rangeUtil.describeTimeRange({ + from: moment([2014, 10, 10, 2, 3, 4]), + to: 'now', + }); expect(text).toBe('Nov 10, 2014 02:03:04 to a few seconds ago'); }); it('Date range with absolute to relative', () => { - var text = rangeUtil.describeTimeRange({from: moment([2014,10,10,2,3,4]), to: 'now-1d'}); + var text = rangeUtil.describeTimeRange({ + from: moment([2014, 10, 10, 2, 3, 4]), + to: 'now-1d', + }); expect(text).toBe('Nov 10, 2014 02:03:04 to a day ago'); }); it('Date range with relative to absolute', () => { - var text = rangeUtil.describeTimeRange({from: 'now-7d', to: moment([2014,10,10,2,3,4])}); + var text = rangeUtil.describeTimeRange({ + from: 'now-7d', + to: moment([2014, 10, 10, 2, 3, 4]), + }); expect(text).toBe('7 days ago to Nov 10, 2014 02:03:04'); }); it('Date range with non matching default ranges', () => { - var text = rangeUtil.describeTimeRange({from: 'now-13h', to: 'now'}); + var text = rangeUtil.describeTimeRange({ from: 'now-13h', to: 'now' }); expect(text).toBe('Last 13 hours'); }); it('Date range with from and to both are in now-* format', () => { - var text = rangeUtil.describeTimeRange({from: 'now-6h', to: 'now-3h'}); + var text = rangeUtil.describeTimeRange({ from: 'now-6h', to: 'now-3h' }); expect(text).toBe('now-6h to now-3h'); }); it('Date range with from and to both are either in now-* or now/* format', () => { - var text = rangeUtil.describeTimeRange({from: 'now/d+6h', to: 'now-3h'}); + var text = rangeUtil.describeTimeRange({ + from: 'now/d+6h', + to: 'now-3h', + }); expect(text).toBe('now/d+6h to now-3h'); }); it('Date range with from and to both are either in now-* or now+* format', () => { - var text = rangeUtil.describeTimeRange({from: 'now-6h', to: 'now+1h'}); + var text = rangeUtil.describeTimeRange({ from: 'now-6h', to: 'now+1h' }); expect(text).toBe('now-6h to now+1h'); }); - }); - }); diff --git a/public/app/core/specs/search.jest.ts b/public/app/core/specs/search.jest.ts new file mode 100644 index 00000000000..2457d71a48d --- /dev/null +++ b/public/app/core/specs/search.jest.ts @@ -0,0 +1,323 @@ +import { SearchCtrl } from '../components/search/search'; +import { SearchSrv } from '../services/search_srv'; + +describe('SearchCtrl', () => { + const searchSrvStub = { + search: (options: any) => {}, + getDashboardTags: () => {}, + }; + let ctrl = new SearchCtrl({ $on: () => {} }, {}, {}, searchSrvStub); + + describe('Given an empty result', () => { + beforeEach(() => { + ctrl.results = []; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); + + it('should not navigate', () => { + expect(ctrl.selectedIndex).toBe(0); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should not navigate', () => { + expect(ctrl.selectedIndex).toBe(0); + }); + }); + }); + + describe('Given a result of one selected collapsed folder with no dashboards and a root folder with 2 dashboards', () => { + beforeEach(() => { + ctrl.results = [ + { + id: 1, + title: 'folder', + items: [], + selected: true, + expanded: false, + toggle: i => (i.expanded = !i.expanded), + }, + { + id: 0, + title: 'Root', + items: [{ id: 3, selected: false }, { id: 5, selected: false }], + selected: false, + expanded: true, + toggle: i => (i.expanded = !i.expanded), + }, + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first folder', () => { + expect(ctrl.results[0].selected).toBeTruthy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + }); + + describe('Given a result of one selected collapsed folder with 2 dashboards and a root folder with 2 dashboards', () => { + beforeEach(() => { + ctrl.results = [ + { + id: 1, + title: 'folder', + items: [{ id: 2, selected: false }, { id: 4, selected: false }], + selected: true, + expanded: false, + toggle: i => (i.expanded = !i.expanded), + }, + { + id: 0, + title: 'Root', + items: [{ id: 3, selected: false }, { id: 5, selected: false }], + selected: false, + expanded: true, + toggle: i => (i.expanded = !i.expanded), + }, + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first folder', () => { + expect(ctrl.results[0].selected).toBeTruthy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + }); + + describe('Given a result of a search with 2 dashboards where the first is selected', () => { + beforeEach(() => { + ctrl.results = [ + { + hideHeader: true, + items: [{ id: 3, selected: true }, { id: 5, selected: false }], + selected: false, + expanded: true, + toggle: i => (i.expanded = !i.expanded), + }, + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeTruthy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeTruthy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + }); + }); + }); +}); diff --git a/public/app/core/specs/search_results.jest.ts b/public/app/core/specs/search_results.jest.ts new file mode 100644 index 00000000000..830496be3a8 --- /dev/null +++ b/public/app/core/specs/search_results.jest.ts @@ -0,0 +1,144 @@ +import { SearchResultsCtrl } from '../components/search/search_results'; +import { beforeEach, afterEach } from 'test/lib/common'; +import appEvents from 'app/core/app_events'; + +jest.mock('app/core/app_events', () => { + return { + emit: jest.fn(), + }; +}); + +describe('SearchResultsCtrl', () => { + let ctrl; + + describe('when checking an item that is not checked', () => { + let item = { checked: false }; + let selectionChanged = false; + + beforeEach(() => { + ctrl = new SearchResultsCtrl({}); + ctrl.onSelectionChanged = () => (selectionChanged = true); + ctrl.toggleSelection(item); + }); + + it('should set checked to true', () => { + expect(item.checked).toBeTruthy(); + }); + + it('should trigger selection changed callback', () => { + expect(selectionChanged).toBeTruthy(); + }); + }); + + describe('when checking an item that is checked', () => { + let item = { checked: true }; + let selectionChanged = false; + + beforeEach(() => { + ctrl = new SearchResultsCtrl({}); + ctrl.onSelectionChanged = () => (selectionChanged = true); + ctrl.toggleSelection(item); + }); + + it('should set checked to false', () => { + expect(item.checked).toBeFalsy(); + }); + + it('should trigger selection changed callback', () => { + expect(selectionChanged).toBeTruthy(); + }); + }); + + describe('when selecting a tag', () => { + let selectedTag = null; + + beforeEach(() => { + ctrl = new SearchResultsCtrl({}); + ctrl.onTagSelected = tag => (selectedTag = tag); + ctrl.selectTag('tag-test'); + }); + + it('should trigger tag selected callback', () => { + expect(selectedTag['$tag']).toBe('tag-test'); + }); + }); + + describe('when toggle a collapsed folder', () => { + let folderExpanded = false; + + beforeEach(() => { + ctrl = new SearchResultsCtrl({}); + ctrl.onFolderExpanding = () => { + folderExpanded = true; + }; + + let folder = { + expanded: false, + toggle: () => Promise.resolve(folder), + }; + + ctrl.toggleFolderExpand(folder); + }); + + it('should trigger folder expanding callback', () => { + expect(folderExpanded).toBeTruthy(); + }); + }); + + describe('when toggle an expanded folder', () => { + let folderExpanded = false; + + beforeEach(() => { + ctrl = new SearchResultsCtrl({}); + ctrl.onFolderExpanding = () => { + folderExpanded = true; + }; + + let folder = { + expanded: true, + toggle: () => Promise.resolve(folder), + }; + + ctrl.toggleFolderExpand(folder); + }); + + it('should not trigger folder expanding callback', () => { + expect(folderExpanded).toBeFalsy(); + }); + }); + + describe('when clicking on a link in search result', () => { + const dashPath = 'dashboard/path'; + const $location = { path: () => dashPath }; + const appEventsMock = appEvents as any; + + describe('with the same url as current path', () => { + beforeEach(() => { + ctrl = new SearchResultsCtrl($location); + const item = { url: dashPath }; + ctrl.onItemClick(item); + }); + + it('should close the search', () => { + expect(appEventsMock.emit.mock.calls.length).toBe(1); + expect(appEventsMock.emit.mock.calls[0][0]).toBe('hide-dash-search'); + }); + }); + + describe('with a different url than current path', () => { + beforeEach(() => { + ctrl = new SearchResultsCtrl($location); + const item = { url: 'another/path' }; + ctrl.onItemClick(item); + }); + + it('should do nothing', () => { + expect(appEventsMock.emit.mock.calls.length).toBe(0); + }); + }); + + afterEach(() => { + appEventsMock.emit.mockClear(); + }); + }); +}); diff --git a/public/app/core/specs/search_srv.jest.ts b/public/app/core/specs/search_srv.jest.ts new file mode 100644 index 00000000000..444939d1f4a --- /dev/null +++ b/public/app/core/specs/search_srv.jest.ts @@ -0,0 +1,276 @@ +import { SearchSrv } from 'app/core/services/search_srv'; +import { BackendSrvMock } from 'test/mocks/backend_srv'; +import impressionSrv from 'app/core/services/impression_srv'; +import { contextSrv } from 'app/core/services/context_srv'; +import { beforeEach } from 'test/lib/common'; + +jest.mock('app/core/store', () => { + return { + getBool: jest.fn(), + set: jest.fn(), + }; +}); + +jest.mock('app/core/services/impression_srv', () => { + return { + getDashboardOpened: jest.fn, + }; +}); + +describe('SearchSrv', () => { + let searchSrv, backendSrvMock; + + beforeEach(() => { + backendSrvMock = new BackendSrvMock(); + searchSrv = new SearchSrv(backendSrvMock, Promise); + + contextSrv.isSignedIn = true; + impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]); + }); + + describe('With recent dashboards', () => { + let results; + + beforeEach(() => { + backendSrvMock.search = jest + .fn() + .mockReturnValueOnce( + Promise.resolve([{ id: 2, title: 'second but first' }, { id: 1, title: 'first but second' }]) + ) + .mockReturnValue(Promise.resolve([])); + + impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]); + + return searchSrv.search({ query: '' }).then(res => { + results = res; + }); + }); + + it('should include recent dashboards section', () => { + expect(results[0].title).toBe('Recent'); + }); + + it('should return order decided by impressions store not api', () => { + expect(results[0].items[0].title).toBe('first but second'); + expect(results[0].items[1].title).toBe('second but first'); + }); + + describe('and 3 recent dashboards removed in backend', () => { + let results; + + beforeEach(() => { + backendSrvMock.search = jest + .fn() + .mockReturnValueOnce(Promise.resolve([{ id: 2, title: 'two' }, { id: 1, title: 'one' }])) + .mockReturnValue(Promise.resolve([])); + + impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([4, 5, 1, 2, 3]); + + return searchSrv.search({ query: '' }).then(res => { + results = res; + }); + }); + + it('should return 2 dashboards', () => { + expect(results[0].items.length).toBe(2); + expect(results[0].items[0].id).toBe(1); + expect(results[0].items[1].id).toBe(2); + }); + }); + }); + + describe('With starred dashboards', () => { + let results; + + beforeEach(() => { + backendSrvMock.search = jest.fn().mockReturnValue(Promise.resolve([{ id: 1, title: 'starred' }])); + + return searchSrv.search({ query: '' }).then(res => { + results = res; + }); + }); + + it('should include starred dashboards section', () => { + expect(results[0].title).toBe('Starred'); + expect(results[0].items.length).toBe(1); + }); + }); + + describe('With starred dashboards and recent', () => { + let results; + + beforeEach(() => { + backendSrvMock.search = jest + .fn() + .mockReturnValueOnce( + Promise.resolve([{ id: 1, title: 'starred and recent', isStarred: true }, { id: 2, title: 'recent' }]) + ) + .mockReturnValue(Promise.resolve([{ id: 1, title: 'starred and recent' }])); + + impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]); + return searchSrv.search({ query: '' }).then(res => { + results = res; + }); + }); + + it('should not show starred in recent', () => { + expect(results[1].title).toBe('Recent'); + expect(results[1].items[0].title).toBe('recent'); + }); + + it('should show starred', () => { + expect(results[0].title).toBe('Starred'); + expect(results[0].items[0].title).toBe('starred and recent'); + }); + }); + + describe('with no query string and dashboards with folders returned', () => { + let results; + + beforeEach(() => { + backendSrvMock.search = jest + .fn() + .mockReturnValueOnce(Promise.resolve([])) + .mockReturnValue( + Promise.resolve([ + { + title: 'folder1', + type: 'dash-folder', + id: 1, + }, + { + title: 'dash with no folder', + type: 'dash-db', + id: 2, + }, + { + title: 'dash in folder1 1', + type: 'dash-db', + id: 3, + folderId: 1, + }, + { + title: 'dash in folder1 2', + type: 'dash-db', + id: 4, + folderId: 1, + }, + ]) + ); + + return searchSrv.search({ query: '' }).then(res => { + results = res; + }); + }); + + it('should create sections for each folder and root', () => { + expect(results).toHaveLength(2); + }); + + it('should place folders first', () => { + expect(results[0].title).toBe('folder1'); + }); + }); + + describe('with query string and dashboards with folders returned', () => { + let results; + + beforeEach(() => { + backendSrvMock.search = jest.fn(); + + backendSrvMock.search.mockReturnValue( + Promise.resolve([ + { + id: 2, + title: 'dash with no folder', + type: 'dash-db', + }, + { + id: 3, + title: 'dash in folder1 1', + type: 'dash-db', + folderId: 1, + folderTitle: 'folder1', + }, + ]) + ); + + return searchSrv.search({ query: 'search' }).then(res => { + results = res; + }); + }); + + it('should not specify folder ids', () => { + expect(backendSrvMock.search.mock.calls[0][0].folderIds).toHaveLength(0); + }); + + it('should group results by folder', () => { + expect(results).toHaveLength(2); + }); + }); + + describe('with tags', () => { + beforeEach(() => { + backendSrvMock.search = jest.fn(); + backendSrvMock.search.mockReturnValue(Promise.resolve([])); + + return searchSrv.search({ tag: ['atag'] }).then(() => {}); + }); + + it('should send tags query to backend search', () => { + expect(backendSrvMock.search.mock.calls[0][0].tag).toHaveLength(1); + }); + }); + + describe('with starred', () => { + beforeEach(() => { + backendSrvMock.search = jest.fn(); + backendSrvMock.search.mockReturnValue(Promise.resolve([])); + + return searchSrv.search({ starred: true }).then(() => {}); + }); + + it('should send starred query to backend search', () => { + expect(backendSrvMock.search.mock.calls[0][0].starred).toEqual(true); + }); + }); + + describe('when skipping recent dashboards', () => { + let getRecentDashboardsCalled = false; + + beforeEach(() => { + backendSrvMock.search = jest.fn(); + backendSrvMock.search.mockReturnValue(Promise.resolve([])); + + searchSrv.getRecentDashboards = () => { + getRecentDashboardsCalled = true; + }; + + return searchSrv.search({ skipRecent: true }).then(() => {}); + }); + + it('should not fetch recent dashboards', () => { + expect(getRecentDashboardsCalled).toBeFalsy(); + }); + }); + + describe('when skipping starred dashboards', () => { + let getStarredCalled = false; + + beforeEach(() => { + backendSrvMock.search = jest.fn(); + backendSrvMock.search.mockReturnValue(Promise.resolve([])); + impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]); + + searchSrv.getStarred = () => { + getStarredCalled = true; + }; + + return searchSrv.search({ skipStarred: true }).then(() => {}); + }); + + it('should not fetch starred dashboards', () => { + expect(getStarredCalled).toBeFalsy(); + }); + }); +}); diff --git a/public/app/core/specs/store.jest.ts b/public/app/core/specs/store.jest.ts index 44cb2a84959..0162960621d 100644 --- a/public/app/core/specs/store.jest.ts +++ b/public/app/core/specs/store.jest.ts @@ -4,41 +4,37 @@ Object.assign(window, { localStorage: { removeItem(key) { delete window.localStorage[key]; - } - } + }, + }, }); describe('store', () => { - - it("should store", ()=> { - store.set("key1", "123"); - expect(store.get("key1")).toBe("123"); + it('should store', () => { + store.set('key1', '123'); + expect(store.get('key1')).toBe('123'); }); - it("get key when undefined", ()=> { - expect(store.get("key2")).toBe(undefined); + it('get key when undefined', () => { + expect(store.get('key2')).toBe(undefined); }); - it("check if key exixts", ()=> { - store.set("key3", "123"); - expect(store.exists("key3")).toBe(true); + it('check if key exixts', () => { + store.set('key3', '123'); + expect(store.exists('key3')).toBe(true); }); - it("get boolean when no key", ()=> { - expect(store.getBool("key4", false)).toBe(false); + it('get boolean when no key', () => { + expect(store.getBool('key4', false)).toBe(false); }); - it("get boolean", ()=> { - store.set("key5", "true"); - expect(store.getBool("key5", false)).toBe(true); + it('get boolean', () => { + store.set('key5', 'true'); + expect(store.getBool('key5', false)).toBe(true); }); - it("key should be deleted", ()=> { - store.set("key6", "123"); - store.delete("key6"); - expect(store.exists("key6")).toBe(false); + it('key should be deleted', () => { + store.set('key6', '123'); + store.delete('key6'); + expect(store.exists('key6')).toBe(false); }); - }); - - diff --git a/public/app/core/specs/table_model.jest.ts b/public/app/core/specs/table_model.jest.ts index 05c9fbda039..a2c1eb5e1af 100644 --- a/public/app/core/specs/table_model.jest.ts +++ b/public/app/core/specs/table_model.jest.ts @@ -3,7 +3,7 @@ import TableModel from 'app/core/table_model'; describe('when sorting table desc', () => { var table; var panel = { - sort: {col: 0, desc: true}, + sort: { col: 0, desc: true }, }; beforeEach(() => { @@ -19,17 +19,16 @@ describe('when sorting table desc', () => { expect(table.rows[2][0]).toBe(100); }); - it ('should mark column being sorted', () => { + it('should mark column being sorted', () => { expect(table.columns[0].sort).toBe(true); expect(table.columns[0].desc).toBe(true); }); - }); describe('when sorting table asc', () => { var table; var panel = { - sort: {col: 1, desc: false}, + sort: { col: 1, desc: false }, }; beforeEach(() => { @@ -44,6 +43,4 @@ describe('when sorting table asc', () => { expect(table.rows[1][1]).toBe(11); expect(table.rows[2][1]).toBe(15); }); - }); - diff --git a/public/app/core/specs/time_series.jest.ts b/public/app/core/specs/time_series.jest.ts index 1a199daa59f..5043953071a 100644 --- a/public/app/core/specs/time_series.jest.ts +++ b/public/app/core/specs/time_series.jest.ts @@ -1,6 +1,6 @@ import TimeSeries from 'app/core/time_series2'; -describe("TimeSeries", function() { +describe('TimeSeries', function() { var points, series; var yAxisFormats = ['short', 'ms']; var testData; @@ -8,9 +8,7 @@ describe("TimeSeries", function() { beforeEach(function() { testData = { alias: 'test', - datapoints: [ - [1,2],[null,3],[10,4],[8,5] - ] + datapoints: [[1, 2], [null, 3], [10, 4], [8, 5]], }; }); @@ -30,7 +28,7 @@ describe("TimeSeries", function() { it('if last is null current should pick next to last', function() { series = new TimeSeries({ - datapoints: [[10,1], [null, 2]] + datapoints: [[10, 1], [null, 2]], }); series.getFlotPairs('null', yAxisFormats); expect(series.stats.current).toBe(10); @@ -38,7 +36,7 @@ describe("TimeSeries", function() { it('max value should work for negative values', function() { series = new TimeSeries({ - datapoints: [[-10,1], [-4, 2]] + datapoints: [[-10, 1], [-4, 2]], }); series.getFlotPairs('null', yAxisFormats); expect(series.stats.max).toBe(-4); @@ -52,7 +50,7 @@ describe("TimeSeries", function() { it('the delta value should account for nulls', function() { series = new TimeSeries({ - datapoints: [[1,2],[3,3],[null,4],[10,5],[15,6]] + datapoints: [[1, 2], [3, 3], [null, 4], [10, 5], [15, 6]], }); series.getFlotPairs('null', yAxisFormats); expect(series.stats.delta).toBe(14); @@ -60,7 +58,7 @@ describe("TimeSeries", function() { it('the delta value should account for nulls on first', function() { series = new TimeSeries({ - datapoints: [[null,2],[1,3],[10,4],[15,5]] + datapoints: [[null, 2], [1, 3], [10, 4], [15, 5]], }); series.getFlotPairs('null', yAxisFormats); expect(series.stats.delta).toBe(14); @@ -68,7 +66,7 @@ describe("TimeSeries", function() { it('the delta value should account for nulls on last', function() { series = new TimeSeries({ - datapoints: [[1,2],[5,3],[10,4],[null,5]] + datapoints: [[1, 2], [5, 3], [10, 4], [null, 5]], }); series.getFlotPairs('null', yAxisFormats); expect(series.stats.delta).toBe(9); @@ -76,7 +74,7 @@ describe("TimeSeries", function() { it('the delta value should account for resets', function() { series = new TimeSeries({ - datapoints: [[1,2],[5,3],[10,4],[0,5],[10,6]] + datapoints: [[1, 2], [5, 3], [10, 4], [0, 5], [10, 6]], }); series.getFlotPairs('null', yAxisFormats); expect(series.stats.delta).toBe(19); @@ -84,7 +82,7 @@ describe("TimeSeries", function() { it('the delta value should account for resets on last', function() { series = new TimeSeries({ - datapoints: [[1,2],[2,3],[10,4],[8,5]] + datapoints: [[1, 2], [2, 3], [10, 4], [8, 5]], }); series.getFlotPairs('null', yAxisFormats); expect(series.stats.delta).toBe(17); @@ -101,7 +99,7 @@ describe("TimeSeries", function() { series.getFlotPairs('null', yAxisFormats); expect(series.stats.first).toBe(1); series = new TimeSeries({ - datapoints: [[null,2],[1,3],[10,4],[8,5]] + datapoints: [[null, 2], [1, 3], [10, 4], [8, 5]], }); series.getFlotPairs('null', yAxisFormats); expect(series.stats.first).toBe(1); @@ -115,7 +113,7 @@ describe("TimeSeries", function() { it('average value should be null if all values is null', function() { series = new TimeSeries({ - datapoints: [[null,2],[null,3],[null,4],[null,5]] + datapoints: [[null, 2], [null, 3], [null, 4], [null, 5]], }); series.getFlotPairs('null'); expect(series.stats.avg).toBe(null); @@ -125,7 +123,9 @@ describe("TimeSeries", function() { describe('When checking if ms resolution is needed', function() { describe('msResolution with second resolution timestamps', function() { beforeEach(function() { - series = new TimeSeries({datapoints: [[45, 1234567890], [60, 1234567899]]}); + series = new TimeSeries({ + datapoints: [[45, 1234567890], [60, 1234567899]], + }); }); it('should set hasMsResolution to false', function() { @@ -135,7 +135,9 @@ describe("TimeSeries", function() { describe('msResolution with millisecond resolution timestamps', function() { beforeEach(function() { - series = new TimeSeries({datapoints: [[55, 1236547890001], [90, 1234456709000]]}); + series = new TimeSeries({ + datapoints: [[55, 1236547890001], [90, 1234456709000]], + }); }); it('should show millisecond resolution tooltip', function() { @@ -145,7 +147,9 @@ describe("TimeSeries", function() { describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() { beforeEach(function() { - series = new TimeSeries({datapoints: [[45, 1234567890000], [60, 1234567899000]]}); + series = new TimeSeries({ + datapoints: [[45, 1234567890000], [60, 1234567899000]], + }); }); it('should not show millisecond resolution tooltip', function() { @@ -277,7 +281,6 @@ describe("TimeSeries", function() { expect(series.zindex).toBe(2); }); }); - }); describe('value formatter', function() { @@ -287,12 +290,11 @@ describe("TimeSeries", function() { }); it('should format non-numeric values as empty string', function() { - expect(series.formatValue(null)).toBe(""); - expect(series.formatValue(undefined)).toBe(""); - expect(series.formatValue(NaN)).toBe(""); - expect(series.formatValue(Infinity)).toBe(""); - expect(series.formatValue(-Infinity)).toBe(""); + expect(series.formatValue(null)).toBe(''); + expect(series.formatValue(undefined)).toBe(''); + expect(series.formatValue(NaN)).toBe(''); + expect(series.formatValue(Infinity)).toBe(''); + expect(series.formatValue(-Infinity)).toBe(''); }); }); - }); diff --git a/public/app/core/specs/value_select_dropdown_specs.ts b/public/app/core/specs/value_select_dropdown_specs.ts index 644320e7e0d..8f6408fb389 100644 --- a/public/app/core/specs/value_select_dropdown_specs.ts +++ b/public/app/core/specs/value_select_dropdown_specs.ts @@ -1,7 +1,7 @@ -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() { +describe('SelectDropdownCtrl', function() { var scope; var ctrl; var tagValuesMap: any = {}; @@ -9,19 +9,21 @@ describe("SelectDropdownCtrl", function() { var q; beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.inject(function($controller, $rootScope, $q, $httpBackend) { - rootScope = $rootScope; - q = $q; - scope = $rootScope.$new(); - ctrl = $controller('ValueSelectDropdownCtrl', {$scope: scope}); - ctrl.onUpdated = sinon.spy(); - $httpBackend.when('GET', /\.html$/).respond(''); - })); + beforeEach( + angularMocks.inject(function($controller, $rootScope, $q, $httpBackend) { + rootScope = $rootScope; + q = $q; + scope = $rootScope.$new(); + ctrl = $controller('ValueSelectDropdownCtrl', { $scope: scope }); + ctrl.onUpdated = sinon.spy(); + $httpBackend.when('GET', /\.html$/).respond(''); + }) + ); - describe("Given simple variable", function() { + describe('Given simple variable', function() { beforeEach(function() { ctrl.variable = { - current: {text: 'hej', value: 'hej' }, + current: { text: 'hej', value: 'hej' }, getValuesForTag: function(key) { return q.when(tagValuesMap[key]); }, @@ -29,25 +31,25 @@ describe("SelectDropdownCtrl", function() { ctrl.init(); }); - it("Should init labelText and linkText", function() { - expect(ctrl.linkText).to.be("hej"); + it('Should init labelText and linkText', function() { + expect(ctrl.linkText).to.be('hej'); }); }); - describe("Given variable with tags and dropdown is opened", function() { + describe('Given variable with tags and dropdown is opened', function() { beforeEach(function() { ctrl.variable = { - current: {text: 'server-1', value: 'server-1'}, + current: { text: 'server-1', value: 'server-1' }, options: [ - {text: 'server-1', value: 'server-1', selected: true}, - {text: 'server-2', value: 'server-2'}, - {text: 'server-3', value: 'server-3'}, + { text: 'server-1', value: 'server-1', selected: true }, + { text: 'server-2', value: 'server-2' }, + { text: 'server-3', value: 'server-3' }, ], - tags: ["key1", "key2", "key3"], + tags: ['key1', 'key2', 'key3'], getValuesForTag: function(key) { return q.when(tagValuesMap[key]); }, - multi: true + multi: true, }; tagValuesMap.key1 = ['server-1', 'server-3']; tagValuesMap.key2 = ['server-2', 'server-3']; @@ -56,20 +58,20 @@ describe("SelectDropdownCtrl", function() { ctrl.show(); }); - it("should init tags model", function() { + it('should init tags model', function() { expect(ctrl.tags.length).to.be(3); - expect(ctrl.tags[0].text).to.be("key1"); + expect(ctrl.tags[0].text).to.be('key1'); }); - it("should init options model", function() { + it('should init options model', function() { expect(ctrl.options.length).to.be(3); }); - it("should init selected values array", function() { + it('should init selected values array', function() { expect(ctrl.selectedValues.length).to.be(1); }); - it("should set linkText", function() { + it('should set linkText', function() { expect(ctrl.linkText).to.be('server-1'); }); @@ -91,16 +93,16 @@ describe("SelectDropdownCtrl", function() { ctrl.commitChanges(); }); - it("should select tag", function() { + it('should select tag', function() { expect(ctrl.selectedTags.length).to.be(1); }); - it("should select values", function() { + it('should select values', function() { expect(ctrl.options[0].selected).to.be(true); expect(ctrl.options[2].selected).to.be(true); }); - it("link text should not include tag values", function() { + it('link text should not include tag values', function() { expect(ctrl.linkText).to.be(''); }); @@ -111,7 +113,7 @@ describe("SelectDropdownCtrl", function() { rootScope.$digest(); }); - it("should still have selected tag", function() { + it('should still have selected tag', function() { expect(ctrl.selectedTags.length).to.be(1); }); }); @@ -122,7 +124,7 @@ describe("SelectDropdownCtrl", function() { rootScope.$digest(); }); - it("should deselect tag", function() { + it('should deselect tag', function() { expect(ctrl.selectedTags.length).to.be(0); }); }); @@ -132,36 +134,38 @@ describe("SelectDropdownCtrl", function() { ctrl.selectValue(ctrl.options[0], {}); }); - it("should deselect tag", function() { + it('should deselect tag', function() { expect(ctrl.selectedTags.length).to.be(0); }); }); }); }); - describe("Given variable with selected tags", function() { + describe('Given variable with selected tags', function() { beforeEach(function() { ctrl.variable = { - current: {text: 'server-1', value: 'server-1', tags: [{text: 'key1', selected: true}] }, + current: { + text: 'server-1', + value: 'server-1', + tags: [{ text: 'key1', selected: true }], + }, options: [ - {text: 'server-1', value: 'server-1'}, - {text: 'server-2', value: 'server-2'}, - {text: 'server-3', value: 'server-3'}, + { text: 'server-1', value: 'server-1' }, + { text: 'server-2', value: 'server-2' }, + { text: 'server-3', value: 'server-3' }, ], - tags: ["key1", "key2", "key3"], + tags: ['key1', 'key2', 'key3'], getValuesForTag: function(key) { return q.when(tagValuesMap[key]); }, - multi: true + multi: true, }; ctrl.init(); ctrl.show(); }); - it("should set tag as selected", function() { + it('should set tag as selected', function() { expect(ctrl.tags[0].selected).to.be(true); }); - }); }); - diff --git a/public/app/core/store.ts b/public/app/core/store.ts index cbed16510ab..b0714f49256 100644 --- a/public/app/core/store.ts +++ b/public/app/core/store.ts @@ -1,5 +1,4 @@ export class Store { - get(key) { return window.localStorage[key]; } @@ -22,7 +21,6 @@ export class Store { delete(key) { window.localStorage.removeItem(key); } - } const store = new Store(); diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index 6b02e906583..57800b3e48d 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -1,4 +1,3 @@ - export default class TableModel { columns: any[]; rows: any[]; diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index 0f3dcbc1171..3e02b1ec939 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -1,8 +1,11 @@ import kbn from 'app/core/utils/kbn'; +import { getFlotTickDecimals } from 'app/core/utils/ticks'; import _ from 'lodash'; function matchSeriesOverride(aliasOrRegex, seriesAlias) { - if (!aliasOrRegex) { return false; } + if (!aliasOrRegex) { + return false; + } if (aliasOrRegex[0] === '/') { var regex = kbn.stringToJsRegex(aliasOrRegex); @@ -13,7 +16,50 @@ function matchSeriesOverride(aliasOrRegex, seriesAlias) { } function translateFillOption(fill) { - return fill === 0 ? 0.001 : fill/10; + return fill === 0 ? 0.001 : fill / 10; +} + +/** + * Calculate decimals for legend and update values for each series. + * @param data series data + * @param panel + */ +export function updateLegendValues(data: TimeSeries[], panel) { + for (let i = 0; i < data.length; i++) { + let series = data[i]; + let yaxes = panel.yaxes; + const seriesYAxis = series.yaxis || 1; + let axis = yaxes[seriesYAxis - 1]; + let { tickDecimals, scaledDecimals } = getFlotTickDecimals(data, axis); + let formater = kbn.valueFormats[panel.yaxes[seriesYAxis - 1].format]; + + // decimal override + if (_.isNumber(panel.decimals)) { + series.updateLegendValues(formater, panel.decimals, null); + } else { + // auto decimals + // legend and tooltip gets one more decimal precision + // than graph legend ticks + tickDecimals = (tickDecimals || -1) + 1; + series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2); + } + } +} + +export function getDataMinMax(data: TimeSeries[]) { + let datamin = null; + let datamax = null; + + for (let series of data) { + if (datamax === null || datamax < series.stats.max) { + datamax = series.stats.max; + } + if (datamin === null || datamin > series.stats.min) { + datamin = series.stats.min; + } + } + + return { datamin, datamax }; } export default class TimeSeries { @@ -63,7 +109,7 @@ export default class TimeSeries { applySeriesOverrides(overrides) { this.lines = {}; this.dashes = { - dashLength: [] + dashLength: [], }; this.points = {}; this.bars = {}; @@ -77,29 +123,59 @@ export default class TimeSeries { if (!matchSeriesOverride(override.alias, this.alias)) { continue; } - if (override.lines !== void 0) { this.lines.show = override.lines; } + if (override.lines !== void 0) { + this.lines.show = override.lines; + } if (override.dashes !== void 0) { - this.dashes.show = override.dashes; - this.lines.lineWidth = 0; + this.dashes.show = override.dashes; + this.lines.lineWidth = 0; + } + if (override.points !== void 0) { + this.points.show = override.points; + } + if (override.bars !== void 0) { + this.bars.show = override.bars; + } + if (override.fill !== void 0) { + this.lines.fill = translateFillOption(override.fill); + } + if (override.stack !== void 0) { + this.stack = override.stack; } - if (override.points !== void 0) { this.points.show = override.points; } - if (override.bars !== void 0) { this.bars.show = override.bars; } - if (override.fill !== void 0) { this.lines.fill = translateFillOption(override.fill); } - if (override.stack !== void 0) { this.stack = override.stack; } if (override.linewidth !== void 0) { - this.lines.lineWidth = this.dashes.show ? 0: override.linewidth; - this.dashes.lineWidth = override.linewidth; + this.lines.lineWidth = this.dashes.show ? 0 : override.linewidth; + this.dashes.lineWidth = override.linewidth; + } + if (override.dashLength !== void 0) { + this.dashes.dashLength[0] = override.dashLength; + } + if (override.spaceLength !== void 0) { + this.dashes.dashLength[1] = override.spaceLength; + } + if (override.nullPointMode !== void 0) { + this.nullPointMode = override.nullPointMode; + } + if (override.pointradius !== void 0) { + this.points.radius = override.pointradius; + } + if (override.steppedLine !== void 0) { + this.lines.steps = override.steppedLine; + } + if (override.zindex !== void 0) { + this.zindex = override.zindex; + } + if (override.fillBelowTo !== void 0) { + this.fillBelowTo = override.fillBelowTo; + } + if (override.color !== void 0) { + this.color = override.color; + } + if (override.transform !== void 0) { + this.transform = override.transform; + } + if (override.legend !== void 0) { + this.legend = override.legend; } - if (override.dashLength !== void 0) { this.dashes.dashLength[0] = override.dashLength; } - if (override.spaceLength !== void 0) { this.dashes.dashLength[1] = override.spaceLength; } - if (override.nullPointMode !== void 0) { this.nullPointMode = override.nullPointMode; } - if (override.pointradius !== void 0) { this.points.radius = override.pointradius; } - if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; } - if (override.zindex !== void 0) { this.zindex = override.zindex; } - if (override.fillBelowTo !== void 0) { this.fillBelowTo = override.fillBelowTo; } - if (override.color !== void 0) { this.color = override.color; } - if (override.transform !== void 0) { this.transform = override.transform; } - if (override.legend !== void 0) { this.legend = override.legend; } if (override.yaxis !== void 0) { this.yaxis = override.yaxis; @@ -148,7 +224,9 @@ export default class TimeSeries { previousTime = currentTime; if (currentValue === null) { - if (ignoreNulls) { continue; } + if (ignoreNulls) { + continue; + } if (nullAsZero) { currentValue = 0; } @@ -172,16 +250,18 @@ export default class TimeSeries { if (this.stats.first === null) { this.stats.first = currentValue; } else { - if (previousValue > currentValue) { // counter reset + if (previousValue > currentValue) { + // counter reset previousDeltaUp = false; - if (i === this.datapoints.length-1) { // reset on last - this.stats.delta += currentValue; + if (i === this.datapoints.length - 1) { + // reset on last + this.stats.delta += currentValue; } } else { if (previousDeltaUp) { - this.stats.delta += currentValue - previousValue; // normal increment + this.stats.delta += currentValue - previousValue; // normal increment } else { - this.stats.delta += currentValue; // account for counter reset + this.stats.delta += currentValue; // account for counter reset } previousDeltaUp = true; } @@ -200,14 +280,18 @@ export default class TimeSeries { result.push([currentTime, currentValue]); } - if (this.stats.max === -Number.MAX_VALUE) { this.stats.max = null; } - if (this.stats.min === Number.MAX_VALUE) { this.stats.min = null; } + if (this.stats.max === -Number.MAX_VALUE) { + this.stats.max = null; + } + if (this.stats.min === Number.MAX_VALUE) { + this.stats.min = null; + } if (result.length && !this.allIsNull) { - this.stats.avg = (this.stats.total / nonNulls); - this.stats.current = result[result.length-1][1]; + this.stats.avg = this.stats.total / nonNulls; + this.stats.current = result[result.length - 1][1]; if (this.stats.current === null && result.length > 1) { - this.stats.current = result[result.length-2][1]; + this.stats.current = result[result.length - 2][1]; } } if (this.stats.max !== null && this.stats.min !== null) { @@ -238,7 +322,7 @@ export default class TimeSeries { for (var i = 0; i < this.datapoints.length; i++) { if (this.datapoints[i][1] !== null) { var timestamp = this.datapoints[i][1].toString(); - if (timestamp.length === 13 && (timestamp % 1000) !== 0) { + if (timestamp.length === 13 && timestamp % 1000 !== 0) { return true; } } diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index a38c92a6476..8a70e093ea2 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -4,19 +4,68 @@ import tinycolor from 'tinycolor2'; export const PALETTE_ROWS = 4; export const PALETTE_COLUMNS = 14; export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; -export const OK_COLOR = "rgba(11, 237, 50, 1)"; -export const ALERTING_COLOR = "rgba(237, 46, 24, 1)"; -export const NO_DATA_COLOR = "rgba(150, 150, 150, 1)"; +export const OK_COLOR = 'rgba(11, 237, 50, 1)'; +export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; +export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; export const REGION_FILL_ALPHA = 0.09; let 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" + '#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', ]; export function sortColorsByHue(hexColors) { diff --git a/public/app/core/utils/css_loader.ts b/public/app/core/utils/css_loader.ts index e81e659032d..4ff03ec3c97 100644 --- a/public/app/core/utils/css_loader.ts +++ b/public/app/core/utils/css_loader.ts @@ -49,7 +49,9 @@ var loadCSS = function(url) { link.href = url; if (!isWebkit) { - link.onload = function() { _callback(undefined); }; + link.onload = function() { + _callback(undefined); + }; } else { webkitLoadCheck(link, _callback); } @@ -75,4 +77,3 @@ export function fetch(load): any { } return loadCSS(load.address); } - diff --git a/public/app/core/utils/datemath.ts b/public/app/core/utils/datemath.ts index f5608443f49..b892d49f2d8 100644 --- a/public/app/core/utils/datemath.ts +++ b/public/app/core/utils/datemath.ts @@ -6,9 +6,15 @@ import moment from 'moment'; var units = ['y', 'M', 'w', 'd', 'h', 'm', 's']; export function parse(text, roundUp?, timezone?) { - if (!text) { return undefined; } - if (moment.isMoment(text)) { return text; } - if (_.isDate(text)) { return moment(text); } + if (!text) { + return undefined; + } + if (moment.isMoment(text)) { + return text; + } + if (_.isDate(text)) { + return moment(text); + } var time; var mathString = ''; @@ -84,7 +90,9 @@ export function parseDateMath(mathString, time, roundUp?) { var numFrom = i; while (!isNaN(mathString.charAt(i))) { i++; - if (i > 10) { return undefined; } + if (i > 10) { + return undefined; + } } num = parseInt(mathString.substring(numFrom, i), 10); } @@ -115,4 +123,3 @@ export function parseDateMath(mathString, time, roundUp?) { } return dateTime; } - diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index 03fcd147a3e..1ebd5b90a86 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -1,72 +1,72 @@ import _ from 'lodash'; import moment from 'moment'; -import {saveAs} from 'file-saver'; +import { saveAs } from 'file-saver'; const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'; 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'; - }); + 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'; }); - saveSaveBlob(text, 'grafana_data_export.csv'); + }); + saveSaveBlob(text, 'grafana_data_export.csv'); } export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - var text = (excel ? 'sep=;\n' : '') + 'Time;'; - // add header - _.each(seriesList, function(series) { - text += series.alias + ';'; - }); - text = text.substring(0,text.length-1); - text += '\n'; + var text = (excel ? 'sep=;\n' : '') + 'Time;'; + // add header + _.each(seriesList, function(series) { + text += series.alias + ';'; + }); + text = text.substring(0, text.length - 1); + text += '\n'; - // process data - var dataArr = [[]]; - var sIndex = 1; - _.each(seriesList, function(series) { - var cIndex = 0; - dataArr.push([]); - _.each(series.datapoints, function(dp) { - dataArr[0][cIndex] = moment(dp[1]).format(dateTimeFormat); - dataArr[sIndex][cIndex] = dp[0]; - cIndex++; - }); - sIndex++; + // process data + var dataArr = [[]]; + var sIndex = 1; + _.each(seriesList, function(series) { + var cIndex = 0; + dataArr.push([]); + _.each(series.datapoints, function(dp) { + dataArr[0][cIndex] = moment(dp[1]).format(dateTimeFormat); + dataArr[sIndex][cIndex] = dp[0]; + cIndex++; }); + sIndex++; + }); - // make text - for (var i = 0; i < dataArr[0].length; i++) { - text += dataArr[0][i] + ';'; - for (var j = 1; j < dataArr.length; j++) { - text += dataArr[j][i] + ';'; - } - text = text.substring(0,text.length-1); - text += '\n'; + // make text + for (var i = 0; i < dataArr[0].length; i++) { + text += dataArr[0][i] + ';'; + for (var j = 1; j < dataArr.length; j++) { + text += dataArr[j][i] + ';'; } - saveSaveBlob(text, 'grafana_data_export.csv'); + text = text.substring(0, text.length - 1); + text += '\n'; + } + saveSaveBlob(text, 'grafana_data_export.csv'); } export function exportTableDataToCsv(table, excel = false) { var text = excel ? 'sep=;\n' : ''; - // add header - _.each(table.columns, function(column) { - text += (column.title || column.text) + ';'; + // add header + _.each(table.columns, function(column) { + text += (column.title || column.text) + ';'; + }); + text += '\n'; + // process data + _.each(table.rows, function(row) { + _.each(row, function(value) { + text += value + ';'; }); text += '\n'; - // process data - _.each(table.rows, function(row) { - _.each(row, function(value) { - text += value + ';'; - }); - text += '\n'; - }); - saveSaveBlob(text, 'grafana_data_export.csv'); + }); + saveSaveBlob(text, 'grafana_data_export.csv'); } export function saveSaveBlob(payload, fname) { - var blob = new Blob([payload], { type: "text/csv;charset=utf-8" }); - saveAs(blob, fname); + var blob = new Blob([payload], { type: 'text/csv;charset=utf-8' }); + saveAs(blob, fname); } diff --git a/public/app/core/utils/flatten.ts b/public/app/core/utils/flatten.ts index 927fbf34b78..150017e34f8 100644 --- a/public/app/core/utils/flatten.ts +++ b/public/app/core/utils/flatten.ts @@ -14,7 +14,7 @@ export default function flatten(target, opts): any { var value = object[key]; var isarray = opts.safe && Array.isArray(value); var type = Object.prototype.toString.call(value); - var isobject = type === "[object Object]"; + var isobject = type === '[object Object]'; var newKey = prev ? prev + delimiter + key : key; diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index b0715502114..ec4837fe31d 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -541,7 +541,7 @@ kbn.valueFormats.velocityknot = kbn.formatBuilders.fixedUnit('kn'); // Acceleration kbn.valueFormats.accMS2 = kbn.formatBuilders.fixedUnit('m/sec²'); kbn.valueFormats.accFS2 = kbn.formatBuilders.fixedUnit('f/sec²'); -kbn.valueFormats.accG = kbn.formatBuilders.fixedUnit('g'); +kbn.valueFormats.accG = kbn.formatBuilders.fixedUnit('g'); // Volume kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L'); @@ -557,9 +557,9 @@ kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs'); kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm'); // Angle -kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°'); -kbn.valueFormats.radian = kbn.formatBuilders.fixedUnit('rad'); -kbn.valueFormats.grad = kbn.formatBuilders.fixedUnit('grad'); +kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°'); +kbn.valueFormats.radian = kbn.formatBuilders.fixedUnit('rad'); +kbn.valueFormats.grad = kbn.formatBuilders.fixedUnit('grad'); // Time kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz'); @@ -902,10 +902,10 @@ kbn.getUnitFormats = function() { { text: 'area', submenu: [ - {text: 'Square Meters (m²)', value: 'areaM2' }, - {text: 'Square Feet (ft²)', value: 'areaF2' }, - {text: 'Square Miles (mi²)', value: 'areaMI2'}, - ] + { text: 'Square Meters (m²)', value: 'areaM2' }, + { text: 'Square Feet (ft²)', value: 'areaF2' }, + { text: 'Square Miles (mi²)', value: 'areaMI2' }, + ], }, { text: 'mass', @@ -957,7 +957,7 @@ kbn.getUnitFormats = function() { { text: 'Kilovolt (kV)', value: 'kvolt' }, { text: 'Millivolt (mV)', value: 'mvolt' }, { text: 'Decibel-milliwatt (dBm)', value: 'dBm' }, - { text: 'Ohm (Ω)', value: 'ohm' } + { text: 'Ohm (Ω)', value: 'ohm' }, ], }, { @@ -1002,17 +1002,17 @@ kbn.getUnitFormats = function() { submenu: [ { text: 'Degrees (°)', value: 'degree' }, { text: 'Radians', value: 'radian' }, - { text: 'Gradian', value: 'grad' } - ] + { text: 'Gradian', value: 'grad' }, + ], }, { text: 'acceleration', submenu: [ { text: 'Meters/sec²', value: 'accMS2' }, - { text: 'Feet/sec²', value: 'accFS2' }, - { text: 'G unit', value: 'accG' } - ] - } + { text: 'Feet/sec²', value: 'accFS2' }, + { text: 'G unit', value: 'accG' }, + ], + }, ]; }; diff --git a/public/app/core/utils/model_utils.ts b/public/app/core/utils/model_utils.ts index dd620aff53d..e595ada6e13 100644 --- a/public/app/core/utils/model_utils.ts +++ b/public/app/core/utils/model_utils.ts @@ -7,4 +7,3 @@ export function assignModelProperties(target, source, defaults, removeDefaults?) target[key] = source[key] === undefined ? defaults[key] : source[key]; } } - diff --git a/public/app/core/utils/outline.ts b/public/app/core/utils/outline.ts index 109d13d9fc9..94393e781e9 100644 --- a/public/app/core/utils/outline.ts +++ b/public/app/core/utils/outline.ts @@ -5,7 +5,7 @@ function outlineFixer() { var style_element = d.createElement('STYLE'); var dom_events = 'addEventListener' in d; - var add_event_listener = function (type, callback) { + var add_event_listener = function(type, callback) { // Basic cross-browser event handling if (dom_events) { d.addEventListener(type, callback); @@ -14,19 +14,19 @@ function outlineFixer() { } }; - var set_css = function (css_text) { + var set_css = function(css_text) { // Handle setting of + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icn-row.svg b/public/img/icn-row.svg new file mode 100644 index 00000000000..5218991033b --- /dev/null +++ b/public/img/icn-row.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_add_annotation.svg b/public/img/icons_dark_theme/icon_add_annotation.svg new file mode 100644 index 00000000000..330dfad85ad --- /dev/null +++ b/public/img/icons_dark_theme/icon_add_annotation.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_add_annotation_alt.svg b/public/img/icons_dark_theme/icon_add_annotation_alt.svg new file mode 100644 index 00000000000..f24a46ee75c --- /dev/null +++ b/public/img/icons_dark_theme/icon_add_annotation_alt.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_add_data_sources.svg b/public/img/icons_dark_theme/icon_add_data_sources.svg new file mode 100644 index 00000000000..6ea76c00735 --- /dev/null +++ b/public/img/icons_dark_theme/icon_add_data_sources.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_add_folder.svg b/public/img/icons_dark_theme/icon_add_folder.svg new file mode 100644 index 00000000000..0e3357d1086 --- /dev/null +++ b/public/img/icons_dark_theme/icon_add_folder.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_add_link.svg b/public/img/icons_dark_theme/icon_add_link.svg new file mode 100644 index 00000000000..fe8e95f1eda --- /dev/null +++ b/public/img/icons_dark_theme/icon_add_link.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_add_panel.svg b/public/img/icons_dark_theme/icon_add_panel.svg new file mode 100644 index 00000000000..a3005b3ee1c --- /dev/null +++ b/public/img/icons_dark_theme/icon_add_panel.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_add_team.svg b/public/img/icons_dark_theme/icon_add_team.svg new file mode 100644 index 00000000000..35fd526e053 --- /dev/null +++ b/public/img/icons_dark_theme/icon_add_team.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_add_user.svg b/public/img/icons_dark_theme/icon_add_user.svg new file mode 100644 index 00000000000..6a09f96ef1e --- /dev/null +++ b/public/img/icons_dark_theme/icon_add_user.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_add_variable.svg b/public/img/icons_dark_theme/icon_add_variable.svg new file mode 100644 index 00000000000..9b10092e78f --- /dev/null +++ b/public/img/icons_dark_theme/icon_add_variable.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_alert.svg b/public/img/icons_dark_theme/icon_alert.svg new file mode 100644 index 00000000000..2a3a764a925 --- /dev/null +++ b/public/img/icons_dark_theme/icon_alert.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_alert_alt.svg b/public/img/icons_dark_theme/icon_alert_alt.svg new file mode 100644 index 00000000000..a6c6e9bd051 --- /dev/null +++ b/public/img/icons_dark_theme/icon_alert_alt.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_alert_off.svg b/public/img/icons_dark_theme/icon_alert_off.svg new file mode 100644 index 00000000000..cebbe532ce4 --- /dev/null +++ b/public/img/icons_dark_theme/icon_alert_off.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_alert_rules.svg b/public/img/icons_dark_theme/icon_alert_rules.svg new file mode 100644 index 00000000000..71915eb6d1c --- /dev/null +++ b/public/img/icons_dark_theme/icon_alert_rules.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_annotation.svg b/public/img/icons_dark_theme/icon_annotation.svg new file mode 100644 index 00000000000..514b52fba40 --- /dev/null +++ b/public/img/icons_dark_theme/icon_annotation.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_annotation_alt.svg b/public/img/icons_dark_theme/icon_annotation_alt.svg new file mode 100644 index 00000000000..e798f91805c --- /dev/null +++ b/public/img/icons_dark_theme/icon_annotation_alt.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_apikeys.svg b/public/img/icons_dark_theme/icon_apikeys.svg new file mode 100644 index 00000000000..8cc30794456 --- /dev/null +++ b/public/img/icons_dark_theme/icon_apikeys.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/public/img/icons_dark_theme/icon_cog.svg b/public/img/icons_dark_theme/icon_cog.svg new file mode 100644 index 00000000000..126b529e0df --- /dev/null +++ b/public/img/icons_dark_theme/icon_cog.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_dashboard.svg b/public/img/icons_dark_theme/icon_dashboard.svg new file mode 100644 index 00000000000..a5d4e9b491d --- /dev/null +++ b/public/img/icons_dark_theme/icon_dashboard.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_dashboard_fav.svg b/public/img/icons_dark_theme/icon_dashboard_fav.svg new file mode 100644 index 00000000000..22d2462e922 --- /dev/null +++ b/public/img/icons_dark_theme/icon_dashboard_fav.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_dashboard_list.svg b/public/img/icons_dark_theme/icon_dashboard_list.svg new file mode 100644 index 00000000000..1979400de7b --- /dev/null +++ b/public/img/icons_dark_theme/icon_dashboard_list.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_data_sources.svg b/public/img/icons_dark_theme/icon_data_sources.svg new file mode 100644 index 00000000000..5cce28eb217 --- /dev/null +++ b/public/img/icons_dark_theme/icon_data_sources.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_home.svg b/public/img/icons_dark_theme/icon_home.svg new file mode 100644 index 00000000000..a4f2608d576 --- /dev/null +++ b/public/img/icons_dark_theme/icon_home.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/public/img/icons_dark_theme/icon_import_dashboard.svg b/public/img/icons_dark_theme/icon_import_dashboard.svg new file mode 100644 index 00000000000..d21511c31c5 --- /dev/null +++ b/public/img/icons_dark_theme/icon_import_dashboard.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_json.svg b/public/img/icons_dark_theme/icon_json.svg new file mode 100644 index 00000000000..30b0d9d0972 --- /dev/null +++ b/public/img/icons_dark_theme/icon_json.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_link.svg b/public/img/icons_dark_theme/icon_link.svg new file mode 100644 index 00000000000..ac4bba7fe46 --- /dev/null +++ b/public/img/icons_dark_theme/icon_link.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_new_dashboard.svg b/public/img/icons_dark_theme/icon_new_dashboard.svg new file mode 100644 index 00000000000..33f116dcab1 --- /dev/null +++ b/public/img/icons_dark_theme/icon_new_dashboard.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_notification_channels.svg b/public/img/icons_dark_theme/icon_notification_channels.svg new file mode 100644 index 00000000000..d3bad3f78f4 --- /dev/null +++ b/public/img/icons_dark_theme/icon_notification_channels.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/public/img/icons_dark_theme/icon_org.svg b/public/img/icons_dark_theme/icon_org.svg new file mode 100644 index 00000000000..b9aa21c3a9c --- /dev/null +++ b/public/img/icons_dark_theme/icon_org.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_playlist.svg b/public/img/icons_dark_theme/icon_playlist.svg new file mode 100644 index 00000000000..bf8fc2f80a3 --- /dev/null +++ b/public/img/icons_dark_theme/icon_playlist.svg @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_plugins.svg b/public/img/icons_dark_theme/icon_plugins.svg new file mode 100644 index 00000000000..f00eefea975 --- /dev/null +++ b/public/img/icons_dark_theme/icon_plugins.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/public/img/icons_dark_theme/icon_preferences.svg b/public/img/icons_dark_theme/icon_preferences.svg new file mode 100644 index 00000000000..ed985c39f68 --- /dev/null +++ b/public/img/icons_dark_theme/icon_preferences.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_question.svg b/public/img/icons_dark_theme/icon_question.svg new file mode 100644 index 00000000000..aa33cf29467 --- /dev/null +++ b/public/img/icons_dark_theme/icon_question.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_shield.svg b/public/img/icons_dark_theme/icon_shield.svg new file mode 100644 index 00000000000..7709351a56e --- /dev/null +++ b/public/img/icons_dark_theme/icon_shield.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_sitemap.svg b/public/img/icons_dark_theme/icon_sitemap.svg new file mode 100644 index 00000000000..499273a399e --- /dev/null +++ b/public/img/icons_dark_theme/icon_sitemap.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/public/img/icons_dark_theme/icon_snapshots.svg b/public/img/icons_dark_theme/icon_snapshots.svg new file mode 100644 index 00000000000..5f3543623c7 --- /dev/null +++ b/public/img/icons_dark_theme/icon_snapshots.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_team.svg b/public/img/icons_dark_theme/icon_team.svg new file mode 100644 index 00000000000..1248f5d5e39 --- /dev/null +++ b/public/img/icons_dark_theme/icon_team.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_user.svg b/public/img/icons_dark_theme/icon_user.svg new file mode 100644 index 00000000000..9479e9452ce --- /dev/null +++ b/public/img/icons_dark_theme/icon_user.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_variable.svg b/public/img/icons_dark_theme/icon_variable.svg new file mode 100644 index 00000000000..4fffa63329a --- /dev/null +++ b/public/img/icons_dark_theme/icon_variable.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/public/img/icons_dark_theme/icon_zoom_out.svg b/public/img/icons_dark_theme/icon_zoom_out.svg new file mode 100644 index 00000000000..f94f0fa665e --- /dev/null +++ b/public/img/icons_dark_theme/icon_zoom_out.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_add_annotation.svg b/public/img/icons_light_theme/icon_add_annotation.svg new file mode 100644 index 00000000000..cb306bdd18b --- /dev/null +++ b/public/img/icons_light_theme/icon_add_annotation.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_add_annotation_alt.svg b/public/img/icons_light_theme/icon_add_annotation_alt.svg new file mode 100644 index 00000000000..2565f188d57 --- /dev/null +++ b/public/img/icons_light_theme/icon_add_annotation_alt.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_add_data_sources.svg b/public/img/icons_light_theme/icon_add_data_sources.svg new file mode 100644 index 00000000000..554af86bfc8 --- /dev/null +++ b/public/img/icons_light_theme/icon_add_data_sources.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_add_folder.svg b/public/img/icons_light_theme/icon_add_folder.svg new file mode 100644 index 00000000000..81bbf349616 --- /dev/null +++ b/public/img/icons_light_theme/icon_add_folder.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_add_link.svg b/public/img/icons_light_theme/icon_add_link.svg new file mode 100644 index 00000000000..df31a3e99a3 --- /dev/null +++ b/public/img/icons_light_theme/icon_add_link.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_add_panel.svg b/public/img/icons_light_theme/icon_add_panel.svg new file mode 100644 index 00000000000..c04c6f719e2 --- /dev/null +++ b/public/img/icons_light_theme/icon_add_panel.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_add_team.svg b/public/img/icons_light_theme/icon_add_team.svg new file mode 100644 index 00000000000..dd1580f3a3d --- /dev/null +++ b/public/img/icons_light_theme/icon_add_team.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_add_user.svg b/public/img/icons_light_theme/icon_add_user.svg new file mode 100644 index 00000000000..ef0d1d785f0 --- /dev/null +++ b/public/img/icons_light_theme/icon_add_user.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_add_variable.svg b/public/img/icons_light_theme/icon_add_variable.svg new file mode 100644 index 00000000000..da50f124e2b --- /dev/null +++ b/public/img/icons_light_theme/icon_add_variable.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_alert.svg b/public/img/icons_light_theme/icon_alert.svg new file mode 100644 index 00000000000..a3109c23fa1 --- /dev/null +++ b/public/img/icons_light_theme/icon_alert.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_alert_alt.svg b/public/img/icons_light_theme/icon_alert_alt.svg new file mode 100644 index 00000000000..0d105481a7d --- /dev/null +++ b/public/img/icons_light_theme/icon_alert_alt.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_alert_off.svg b/public/img/icons_light_theme/icon_alert_off.svg new file mode 100644 index 00000000000..b8fd5b000a3 --- /dev/null +++ b/public/img/icons_light_theme/icon_alert_off.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_alert_rules.svg b/public/img/icons_light_theme/icon_alert_rules.svg new file mode 100644 index 00000000000..0c429a93e48 --- /dev/null +++ b/public/img/icons_light_theme/icon_alert_rules.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_annotation.svg b/public/img/icons_light_theme/icon_annotation.svg new file mode 100644 index 00000000000..ce8aa4e3372 --- /dev/null +++ b/public/img/icons_light_theme/icon_annotation.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_annotation_alt.svg b/public/img/icons_light_theme/icon_annotation_alt.svg new file mode 100644 index 00000000000..5379a6185c4 --- /dev/null +++ b/public/img/icons_light_theme/icon_annotation_alt.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_apikeys.svg b/public/img/icons_light_theme/icon_apikeys.svg new file mode 100644 index 00000000000..c54aabe4e30 --- /dev/null +++ b/public/img/icons_light_theme/icon_apikeys.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/public/img/icons_light_theme/icon_cog.svg b/public/img/icons_light_theme/icon_cog.svg new file mode 100644 index 00000000000..c8ccfaa38f3 --- /dev/null +++ b/public/img/icons_light_theme/icon_cog.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/public/img/icons_light_theme/icon_dashboard.svg b/public/img/icons_light_theme/icon_dashboard.svg new file mode 100644 index 00000000000..2e275b87568 --- /dev/null +++ b/public/img/icons_light_theme/icon_dashboard.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_dashboard_fav.svg b/public/img/icons_light_theme/icon_dashboard_fav.svg new file mode 100644 index 00000000000..6ec8730e3ad --- /dev/null +++ b/public/img/icons_light_theme/icon_dashboard_fav.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_dashboard_list.svg b/public/img/icons_light_theme/icon_dashboard_list.svg new file mode 100644 index 00000000000..c6d8ddd6c5e --- /dev/null +++ b/public/img/icons_light_theme/icon_dashboard_list.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_data_sources.svg b/public/img/icons_light_theme/icon_data_sources.svg new file mode 100644 index 00000000000..3a6e65e8761 --- /dev/null +++ b/public/img/icons_light_theme/icon_data_sources.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_home.svg b/public/img/icons_light_theme/icon_home.svg new file mode 100644 index 00000000000..a7f0fde536a --- /dev/null +++ b/public/img/icons_light_theme/icon_home.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/public/img/icons_light_theme/icon_import_dashboard.svg b/public/img/icons_light_theme/icon_import_dashboard.svg new file mode 100644 index 00000000000..d1b5f2fe17e --- /dev/null +++ b/public/img/icons_light_theme/icon_import_dashboard.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_json.svg b/public/img/icons_light_theme/icon_json.svg new file mode 100644 index 00000000000..42ea7083403 --- /dev/null +++ b/public/img/icons_light_theme/icon_json.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_link.svg b/public/img/icons_light_theme/icon_link.svg new file mode 100644 index 00000000000..7201eb6aa3a --- /dev/null +++ b/public/img/icons_light_theme/icon_link.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_new_dashboard.svg b/public/img/icons_light_theme/icon_new_dashboard.svg new file mode 100644 index 00000000000..9ed93e4732b --- /dev/null +++ b/public/img/icons_light_theme/icon_new_dashboard.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_notification_channels.svg b/public/img/icons_light_theme/icon_notification_channels.svg new file mode 100644 index 00000000000..aa6ada13c8a --- /dev/null +++ b/public/img/icons_light_theme/icon_notification_channels.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/public/img/icons_light_theme/icon_org.svg b/public/img/icons_light_theme/icon_org.svg new file mode 100644 index 00000000000..2c4b738bda2 --- /dev/null +++ b/public/img/icons_light_theme/icon_org.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_playlist.svg b/public/img/icons_light_theme/icon_playlist.svg new file mode 100644 index 00000000000..772ad4f7a6f --- /dev/null +++ b/public/img/icons_light_theme/icon_playlist.svg @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_plugins.svg b/public/img/icons_light_theme/icon_plugins.svg new file mode 100644 index 00000000000..7f5dd78e1da --- /dev/null +++ b/public/img/icons_light_theme/icon_plugins.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/public/img/icons_light_theme/icon_preferences.svg b/public/img/icons_light_theme/icon_preferences.svg new file mode 100644 index 00000000000..ea2f7d70def --- /dev/null +++ b/public/img/icons_light_theme/icon_preferences.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_question.svg b/public/img/icons_light_theme/icon_question.svg new file mode 100644 index 00000000000..90e284508ff --- /dev/null +++ b/public/img/icons_light_theme/icon_question.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_shield.svg b/public/img/icons_light_theme/icon_shield.svg new file mode 100644 index 00000000000..675cad7c2ad --- /dev/null +++ b/public/img/icons_light_theme/icon_shield.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_sitemap.svg b/public/img/icons_light_theme/icon_sitemap.svg new file mode 100644 index 00000000000..c19c62cb4b0 --- /dev/null +++ b/public/img/icons_light_theme/icon_sitemap.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/public/img/icons_light_theme/icon_snapshots.svg b/public/img/icons_light_theme/icon_snapshots.svg new file mode 100644 index 00000000000..7c29338a52f --- /dev/null +++ b/public/img/icons_light_theme/icon_snapshots.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_team.svg b/public/img/icons_light_theme/icon_team.svg new file mode 100644 index 00000000000..21efa62218e --- /dev/null +++ b/public/img/icons_light_theme/icon_team.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_user.svg b/public/img/icons_light_theme/icon_user.svg new file mode 100644 index 00000000000..205a345755a --- /dev/null +++ b/public/img/icons_light_theme/icon_user.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/public/img/icons_light_theme/icon_variable.svg b/public/img/icons_light_theme/icon_variable.svg new file mode 100644 index 00000000000..8624f74bc63 --- /dev/null +++ b/public/img/icons_light_theme/icon_variable.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_zoom_out.svg b/public/img/icons_light_theme/icon_zoom_out.svg new file mode 100644 index 00000000000..b85181c1a31 --- /dev/null +++ b/public/img/icons_light_theme/icon_zoom_out.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/public/img/resize-handle-white.svg b/public/img/resize-handle-white.svg new file mode 100644 index 00000000000..3b439663f8a --- /dev/null +++ b/public/img/resize-handle-white.svg @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/public/img/tgr288gear_line6.pdf b/public/img/tgr288gear_line6.pdf new file mode 100644 index 00000000000..21cc75e1e33 Binary files /dev/null and b/public/img/tgr288gear_line6.pdf differ diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 79ee1799b90..3232e9c7f8e 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -21,6 +21,7 @@ @import "base/grid"; @import "base/fonts"; @import "base/code"; +@import "base/icons"; // UTILS @import "utils/utils"; @@ -34,6 +35,7 @@ @import "layout/page"; // COMPONENTS +@import "components/scrollbar"; @import "components/cards"; @import "components/buttons"; @import "components/navs"; @@ -52,6 +54,8 @@ @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"; @@ -62,7 +66,6 @@ @import "components/filter-controls"; @import "components/filter-list"; @import "components/filter-table"; -@import "components/scrollbar"; @import "components/old_stuff"; @import "components/typeahead"; @import "components/modals"; @@ -76,11 +79,15 @@ @import "components/tabbed_view"; @import "components/query_part"; @import "components/jsontree"; -@import "components/edit_sidemenu.scss"; +@import "components/edit_sidemenu"; @import "components/row.scss"; -@import "components/icon-picker.scss"; -@import "components/json_explorer.scss"; -@import "components/code_editor.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"; // PAGES @import "pages/login"; diff --git a/public/sass/_old_responsive.scss b/public/sass/_old_responsive.scss index be56bbae485..164d6dd09c7 100644 --- a/public/sass/_old_responsive.scss +++ b/public/sass/_old_responsive.scss @@ -1,95 +1,56 @@ +.navbar-buttons--zoom { + display: none; +} -.dashnav-back-to-dashboard { +.navbar-page-btn { + max-width: 200px; +} + +.gf-timepicker-nav-btn { + max-width: 120px; +} + +.navbar-buttons--actions { display: none; } // Media queries // --------------------- -@include media-breakpoint-down(sm) { - div.panel { - width: 100% !important; - padding: 0px !important; - } - .panel-margin { - margin-right: 0; - margin-left: 0; - } - body { - padding: 0; - } - .page-dashboard .navbar-page-btn { - max-width: 200px; - } - .gf-timepicker-nav-btn { - max-width: 120px; - } - .dashnav-zoom-out, - .dashnav-move-timeframe, - .dashnav-action-icons { - display: none; - } - .page-container { - padding: ($spacer * 1) ($spacer * 2); - } - - .dash-row-menu-container { - display: none; +@include media-breakpoint-down(xs) { + input[type="text"], + input[type="number"], + textarea { + font-size: 16px; } } -@include media-breakpoint-down(xs) { - .page-dashboard .navbar-page-btn { +@include media-breakpoint-up(sm) { + .navbar-page-btn { max-width: 250px; } -} - -// form styles -@include media-breakpoint-up(md) { - .page-dashboard .navbar-page-btn { - max-width: 280px; - } .gf-timepicker-nav-btn { - max-width: 120px; - } - - .dashnav-move-timeframe { - display: none; - } - - .panel-in-fullscreen { - .dashnav-action-icons { - display: none; - } - .dashnav-back-to-dashboard { - display: block; - } + max-width: 200px; } } -@include media-breakpoint-up(lg) { - .page-dashboard .navbar-page-btn { - max-width: 290px; +@include media-breakpoint-up(md) { + .navbar-buttons--actions { + display: flex; + } + .navbar-page-btn { + max-width: 325px; } .gf-timepicker-nav-btn { max-width: 240px; } - .dashnav-zoom-out { - display: block; - } - .dashnav-move-timeframe { - display: block; - } } -@include media-breakpoint-up(xl) { - .panel-in-fullscreen { - .dashnav-action-icons { - display: block; - } +@include media-breakpoint-up(lg) { + .navbar-buttons--zoom { + display: flex; } - - .page-dashboard .navbar-page-btn { + .navbar-page-btn { max-width: none; } .gf-timepicker-nav-btn { diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 27f0e959d34..2b734ea9533 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -1,287 +1,343 @@ // Global values // -------------------------------------------------- +$theme-name: dark; // Grays // ------------------------- -$black: #000; +$black: #000; // ------------------------- -$black: #000; -$dark-1: #141414; -$dark-2: #1f1d1d; -$dark-3: #292929; -$dark-4: #333333; -$dark-5: #444444; -$gray-1: #555555; -$gray-2: #7B7B7B; -$gray-3: #b3b3b3; -$gray-4: #D8D9DA; -$gray-5: #ECECEC; -$gray-6: #f4f5f8; -$gray-7: #fbfbfb; +$black: #000; +$dark-1: #141414; +$dark-2: #1f1f20; +$dark-3: #262628; +$dark-4: #333333; +$dark-5: #444444; +$gray-1: #555555; +$gray-2: #8e8e8e; +$gray-3: #b3b3b3; +$gray-4: #d8d9da; +$gray-5: #ececec; +$gray-6: #f4f5f8; +$gray-7: #fbfbfb; -$white: #fff; +$gray-blue: #212327; +$input-black: #09090b; + +$white: #fff; // Accent colors // ------------------------- -$blue: #33B5E5; -$blue-dark: #005f81; -$green: #299c46; -$red: #d44a3a; -$yellow: #ECBB13; -$pink: #FF4444; -$purple: #9933CC; -$variable: #32D1DF; -$orange: #eb7b18; +$blue: #33b5e5; +$blue-dark: #005f81; +$green: #299c46; +$red: #d44a3a; +$yellow: #ecbb13; +$pink: #ff4444; +$purple: #9933cc; +$variable: #32d1df; +$orange: #eb7b18; -$brand-primary: $orange; -$brand-success: $green; -$brand-warning: $brand-primary; -$brand-danger: $red; +$brand-primary: $orange; +$brand-success: $green; +$brand-warning: $brand-primary; +$brand-danger: $red; + +$query-blue: $blue; // Status colors // ------------------------- -$online: #10a345; -$warn: #F79520; -$critical: #ed2e18; +$online: #10a345; +$warn: #f79520; +$critical: #ed2e18; // Scaffolding // ------------------------- -$body-bg: rgb(20,20,20); -$page-bg: $dark-2; -$body-color: $gray-4; -$text-color: $gray-4; -$text-color-strong: $white; -$text-color-weak: $gray-2; -$text-color-faint: $dark-5; -$text-color-emphasis: $gray-5; +$body-bg: rgb(23, 24, 25); +$page-bg: rgb(22, 23, 25); + +$body-color: $gray-4; +$text-color: $gray-4; +$text-color-strong: $white; +$text-color-weak: $gray-2; +$text-color-faint: $dark-5; +$text-color-emphasis: $gray-5; $text-shadow-strong: 1px 1px 4px $black; $text-shadow-faint: 1px 1px 4px rgb(45, 45, 45); // gradients -$brand-gradient: linear-gradient(to right, rgba(255,213,0,0.7) 0%, rgba(255,68,0,0.7) 99%, rgba(255,68,0,0.7) 100%); -$page-gradient: linear-gradient(60deg, transparent 70%, darken($page-bg, 4%) 98%); +$brand-gradient: linear-gradient( + to right, + rgba(255, 213, 0, 0.7) 0%, + rgba(255, 68, 0, 0.7) 99%, + rgba(255, 68, 0, 0.7) 100% +); +$page-gradient: linear-gradient(180deg, #222426 10px, rgb(22, 23, 25) 100px); // Links // ------------------------- -$link-color: darken($white, 11%); -$link-color-disabled: darken($link-color, 30%); -$link-hover-color: $white; -$external-link-color: $blue; +$link-color: darken($white, 11%); +$link-color-disabled: darken($link-color, 30%); +$link-hover-color: $white; +$external-link-color: $blue; // Typography // ------------------------- -$headings-color: darken($white,11%); -$abbr-border-color: $gray-3 !default; -$text-muted: $text-color-weak; +$headings-color: darken($white, 11%); +$abbr-border-color: $gray-3 !default; +$text-muted: $text-color-weak; -$blockquote-small-color: $gray-3 !default; +$blockquote-small-color: $gray-3 !default; $blockquote-border-color: $gray-4 !default; -$hr-border-color: rgba(0,0,0,.1) !default; +$hr-border-color: rgba(0, 0, 0, 0.1) !default; // Components $component-active-color: #fff !default; -$component-active-bg: $brand-primary !default; +$component-active-bg: $brand-primary !default; // Panel // ------------------------- -$panel-bg: $dark-2; -$panel-border: solid 1px $dark-3; -$panel-drop-zone-bg: repeating-linear-gradient(-128deg, #111, #111 10px, #191919 10px, #222 20px); -$panel-menu-border: solid 1px black; +$panel-bg: #212124; +$panel-border-color: $dark-1; +$panel-border: solid 1px $panel-border-color; +$panel-drop-zone-bg: repeating-linear-gradient( + -128deg, + #111, + #111 10px, + #191919 10px, + #222 20px +); +$panel-header-hover-bg: $dark-4; +$panel-header-menu-hover-bg: $dark-5; +$panel-edit-shadow: 0 -30px 30px -30px $black; -$divider-border-color: #555; +// page header +$page-header-bg: linear-gradient(90deg, #292a2d, black); +$page-header-shadow: inset 0px -4px 14px $dark-2; +$page-header-border-color: $dark-4; + +$divider-border-color: #555; // Graphite Target Editor -$tight-form-border: #050505; -$tight-form-bg: $dark-3; +$tight-form-bg: $dark-3; -$tight-form-func-bg: #333; -$tight-form-func-highlight-bg: #444; +$tight-form-func-bg: #333334; +$tight-form-func-highlight-bg: #444445; -$modal-background: $black; -$code-tag-bg: $gray-1; -$code-tag-border: lighten($code-tag-bg, 2%); +$modal-backdrop-bg: #353c42; +$code-tag-bg: $gray-1; +$code-tag-border: lighten($code-tag-bg, 2%); +// cards +$card-background: linear-gradient(135deg, #2f2f32, #262628); +$card-background-hover: linear-gradient(135deg, #343436, #262628); +$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, 0.1), + 1px 1px 0 0 rgba(0, 0, 0, 0.3); // Lists -$grafanaListBackground: $dark-3; -$grafanaListAccent: lighten($dark-2, 2%); -$grafanaListBorderTop: $dark-3; -$grafanaListBorderBottom: $black; -$grafanaListHighlight: #333; -$grafanaListMainLinkColor: $text-color; +$list-item-bg: $card-background; +$list-item-hover-bg: lighten($gray-blue, 2%); +$list-item-link-color: $text-color; +$list-item-shadow: $card-shadow; // Scrollbars -$scrollbarBackground: #3a3a3a; +$scrollbarBackground: #404357; $scrollbarBackground2: #3a3a3a; + $scrollbarBorder: black; // Tables // ------------------------- -$table-bg: transparent; // overall background-color -$table-bg-accent: $dark-3; // for striping -$table-bg-hover: $dark-4; // for hover -$table-border: $dark-3; // table and cell border +$table-bg: transparent; // overall background-color +$table-bg-accent: $dark-3; // for striping +$table-bg-hover: $dark-4; // for hover +$table-border: $dark-3; // table and cell border + +$table-bg-odd: $dark-2; // Buttons // ------------------------- -$btn-primary-bg: #ff6600; -$btn-primary-bg-hl: #bc3e06; +$btn-primary-bg: #ff6600; +$btn-primary-bg-hl: #bc3e06; -$btn-secondary-bg: $blue-dark; -$btn-secondary-bg-hl: lighten($blue-dark, 5%); +$btn-secondary-bg: $blue-dark; +$btn-secondary-bg-hl: lighten($blue-dark, 5%); -$btn-success-bg: $green; -$btn-success-bg-hl: darken($green, 6%); +$btn-success-bg: $green; +$btn-success-bg-hl: darken($green, 6%); -$btn-warning-bg: $brand-warning; -$btn-warning-bg-hl: lighten($brand-warning, 8%); +$btn-warning-bg: $brand-warning; +$btn-warning-bg-hl: lighten($brand-warning, 8%); -$btn-danger-bg: $red; -$btn-danger-bg-hl: darken($red, 8%); +$btn-danger-bg: $red; +$btn-danger-bg-hl: darken($red, 8%); -$btn-inverse-bg: $dark-3; -$btn-inverse-bg-hl: lighten($dark-3, 4%); -$btn-inverse-text-color: $link-color; +$btn-inverse-bg: $dark-3; +$btn-inverse-bg-hl: lighten($dark-3, 4%); +$btn-inverse-text-color: $link-color; +$btn-inverse-text-shadow: 0px 1px 0 rgba(0, 0, 0, 0.1); -$btn-link-color: $gray-3; +$btn-link-color: $gray-3; -$iconContainerBackground: $black; +$iconContainerBackground: $black; -$btn-divider-left: $dark-4; -$btn-divider-right: $dark-2; +$btn-divider-left: $dark-4; +$btn-divider-right: $dark-2; -$btn-drag-image: '../img/grab_dark.svg'; +$btn-drag-image: "../img/grab_dark.svg"; // Forms // ------------------------- -$input-bg: $dark-4; -$input-bg-disabled: $dark-3; +$input-bg: $input-black; +$input-bg-disabled: $dark-3; -$input-color: $gray-4; -$input-border-color: $dark-4; -$input-box-shadow: inset 1px 0px 0.3rem 0px rgba(150, 150, 150, 0.10); -$input-border-focus: $input-border-color !default; -$input-box-shadow-focus: rgba(102,175,233,.6) !default; -$input-color-placeholder: $gray-1 !default; -$input-label-bg: $dark-3; -$input-invalid-border-color: lighten($red, 5%); +$input-color: $gray-4; +$input-border-color: $dark-3; +$input-box-shadow: inset 1px 0px 0.3rem 0px rgba(150, 150, 150, 0.1); +$input-border-focus: $input-border-color; +$input-box-shadow-focus: rgba(102, 175, 233, 0.6); +$input-color-placeholder: $gray-1 !default; +$input-label-bg: $gray-blue; +$input-label-border-color: $dark-3; +$input-invalid-border-color: lighten($red, 5%); // Search -$search-shadow: 0 0 35px 0 $body-bg; +$search-shadow: 0 0 30px 0 $black; +$search-filter-box-bg: $gray-blue; // Dropdowns // ------------------------- -$dropdownBackground: $dark-3; -$dropdownBorder: rgba(0,0,0,.2); -$dropdownDividerTop: transparent; -$dropdownDividerBottom: #444; -$dropdownDivider: $dropdownDividerBottom; -$dropdownTitle: $link-color-disabled; +$dropdownBackground: $dark-3; +$dropdownBorder: rgba(0, 0, 0, 0.2); +$dropdownDividerTop: transparent; +$dropdownDividerBottom: #444; +$dropdownDivider: $dropdownDividerBottom; +$dropdownTitle: $link-color-disabled; -$dropdownLinkColor: $text-color; -$dropdownLinkColorHover: $white; -$dropdownLinkColorActive: $white; +$dropdownLinkColor: $text-color; +$dropdownLinkColorHover: $white; +$dropdownLinkColorActive: $white; -$dropdownLinkBackgroundActive: $dark-4; -$dropdownLinkBackgroundHover: $dark-4; +$dropdownLinkBackgroundActive: $dark-4; +$dropdownLinkBackgroundHover: $dark-4; + +$dropdown-link-color: $gray-3; // COMPONENT VARIABLES // -------------------------------------------------- // ------------------------- -$placeholderText: darken($text-color, 25%); - +$placeholderText: darken($text-color, 25%); // Horizontal forms & lists // ------------------------- -$horizontalComponentOffset: 180px; - +$horizontalComponentOffset: 180px; // Wells // ------------------------- -$wellBackground: #131517; +$wellBackground: #131517; -$navbarHeight: 52px; -$navbarBackgroundHighlight: $dark-3; -$navbarBackground: $dark-3; -$navbarBorder: 1px solid $body-bg; +$navbarHeight: 55px; +$navbarBackgroundHighlight: $dark-3; +$navbarBackground: $panel-bg; +$navbarBorder: 1px solid $dark-3; +$navbarShadow: 0 0 20px black; -$navbarText: $gray-4; -$navbarLinkColor: $gray-4; -$navbarLinkColorHover: $white; -$navbarLinkColorActive: $navbarLinkColorHover; -$navbarLinkBackgroundHover: transparent; -$navbarLinkBackgroundActive: $navbarBackground; -$navbarBrandColor: $link-color; -$navbarDropdownShadow: inset 0px 4px 10px -4px $body-bg; +$navbarText: $gray-4; +$navbarLinkColor: $gray-4; +$navbarLinkColorHover: $white; +$navbarLinkColorActive: $navbarLinkColorHover; +$navbarLinkBackgroundHover: transparent; +$navbarLinkBackgroundActive: $navbarBackground; +$navbarBrandColor: $link-color; +$navbarDropdownShadow: inset 0px 4px 10px -4px $body-bg; -$navbarButtonBackground: lighten($navbarBackground, 3%); -$navbarButtonBackgroundHighlight: lighten($navbarBackground, 5%); +$navbarButtonBackground: $navbarBackground; +$navbarButtonBackgroundHighlight: $body-bg; + +$navbar-button-border: #151515; // Sidemenu // ------------------------- -$side-menu-bg: $body-bg; -$side-menu-item-hover-bg: $dark-3; -$side-menu-opacity: 0.97; +$side-menu-bg: $black; +$side-menu-item-hover-bg: $dark-2; +$side-menu-shadow: 0 0 20px black; +$side-menu-link-color: $link-color; +$breadcrumb-hover-hl: #111; + +// Menu dropdowns +// ------------------------- +$menu-dropdown-bg: $body-bg; +$menu-dropdown-hover-bg: $dark-2; +$menu-dropdown-border-color: $dark-3; +$menu-dropdown-shadow: 5px 5px 20px -5px $black; + +// Breadcrumb +// ------------------------- +$page-nav-bg: $black; +$page-nav-shadow: 5px 5px 20px -5px $black; +$page-nav-breadcrumb-color: $gray-3; + +// Tabs +// ------------------------- +$tab-border-color: $dark-4; // Pagination // ------------------------- -$paginationBackground: $body-bg; -$paginationBorder: transparent; -$paginationActiveBackground: $blue; +$paginationBackground: $body-bg; +$paginationBorder: transparent; +$paginationActiveBackground: $blue; // Form states and alerts // ------------------------- -$warning-text-color: $warn; -$error-text-color: #E84D4D; -$success-text-color: #12D95A; -$info-text-color: $blue-dark; +$warning-text-color: $warn; +$error-text-color: #e84d4d; +$success-text-color: #12d95a; +$info-text-color: $blue-dark; -$alert-error-bg: linear-gradient(90deg, #d44939, #e0603d); -$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274); -$alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d); -$alert-info-bg: linear-gradient(100deg, #1a4552, #00374a); +$alert-error-bg: linear-gradient(90deg, #d44939, #e0603d); +$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274); +$alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d); +$alert-info-bg: linear-gradient(100deg, #1a4552, #00374a); // popover -$popover-bg: $panel-bg; -$popover-color: $text-color; -$popover-border-color: $dark-4; -$popover-shadow: 0 0 20px black; +$popover-bg: $page-bg; +$popover-color: $text-color; +$popover-border-color: $dark-4; +$popover-shadow: 0 0 20px black; -$popover-help-bg: $btn-secondary-bg; -$popover-help-color: $text-color; +$popover-help-bg: $btn-secondary-bg; +$popover-help-color: $text-color; -$popover-error-bg: $btn-danger-bg; +$popover-error-bg: $btn-danger-bg; // Tooltips and popovers // ------------------------- -$tooltipColor: $popover-help-color; -$tooltipBackground: $popover-help-bg; -$tooltipArrowWidth: 5px; -$tooltipArrowColor: $tooltipBackground; -$tooltipLinkColor: $link-color; -$graph-tooltip-bg: $dark-1; +$tooltipColor: $popover-help-color; +$tooltipBackground: $popover-help-bg; +$tooltipArrowWidth: 5px; +$tooltipArrowColor: $tooltipBackground; +$tooltipLinkColor: $link-color; +$graph-tooltip-bg: $dark-1; // images -$checkboxImageUrl: '../img/checkbox.png'; - -// cards -$card-background: linear-gradient(135deg, #2f2f2f, #262626); -$card-background-hover: linear-gradient(135deg, #343434, #262626); -$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3); +$checkboxImageUrl: "../img/checkbox.png"; // info box -$info-box-background: linear-gradient(100deg, #1a4552, #00374a); +$info-box-background: linear-gradient( + 100deg, + $blue-dark, + darken($blue-dark, 5%) +); +$info-box-color: $gray-4; // footer -$footer-link-color: $gray-1; -$footer-link-hover: $gray-4; +$footer-link-color: $gray-2; +$footer-link-hover: $gray-4; // collapse box $collapse-box-body-border: $dark-5; @@ -292,34 +348,34 @@ $json-explorer-default-color: $text-color; $json-explorer-string-color: #23d662; $json-explorer-number-color: $variable; $json-explorer-boolean-color: $variable; -$json-explorer-null-color: #EEC97D; +$json-explorer-null-color: #eec97d; $json-explorer-undefined-color: rgb(239, 143, 190); -$json-explorer-function-color: #FD48CB; +$json-explorer-function-color: #fd48cb; $json-explorer-rotate-time: 100ms; $json-explorer-toggler-opacity: 0.6; -$json-explorer-toggler-color: #45376F; -$json-explorer-bracket-color: #9494FF; -$json-explorer-key-color: #23A0DB; -$json-explorer-url-color: #027BFF; +$json-explorer-toggler-color: #45376f; +$json-explorer-bracket-color: #9494ff; +$json-explorer-key-color: #23a0db; +$json-explorer-url-color: #027bff; // Changelog and diff // ------------------------- -$diff-label-bg: $dark-2; -$diff-label-fg: $white; +$diff-label-bg: $dark-2; +$diff-label-fg: $white; -$diff-group-bg: $dark-4; -$diff-arrow-color: $white; +$diff-group-bg: $dark-4; +$diff-arrow-color: $white; -$diff-json-bg: $dark-4; -$diff-json-fg: $gray-5; +$diff-json-bg: $dark-4; +$diff-json-fg: $gray-5; -$diff-json-added: #457740; -$diff-json-deleted: #a04338; +$diff-json-added: #457740; +$diff-json-deleted: #a04338; -$diff-json-old: #a04338; -$diff-json-new: #457740; +$diff-json-old: #a04338; +$diff-json-new: #457740; -$diff-json-changed-fg: $gray-5; +$diff-json-changed-fg: $gray-5; $diff-json-changed-num: $text-color; -$diff-json-icon: $gray-7; +$diff-json-icon: $gray-7; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index eccebe1debc..a529f33a52b 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -2,306 +2,349 @@ // Variables // -------------------------------------------------- - // Global values // -------------------------------------------------- +$theme-name: light; // Grays // ------------------------- -$black: #000; +$black: #000; // ------------------------- -$black: #000; -$dark-1: #141414; -$dark-2: #1f1d1d; -$dark-3: #292929; -$dark-4: #373737; -$dark-5: #444444; -$gray-1: #555555; -$gray-2: #7B7B7B; -$gray-3: #b3b3b3; -$gray-4: #D8D9DA; -$gray-5: #ECECEC; -$gray-6: #f4f5f8; -$gray-7: #fbfbfb; +$black: #000; +$dark-1: #13161d; +$dark-2: #1e2028; +$dark-3: #303133; +$dark-4: #35373f; +$dark-5: #41444b; +$gray-1: #52545c; +$gray-2: #767980; +$gray-3: #acb6bf; +$gray-4: #c7d0d9; +$gray-5: #dde4ed; +$gray-6: #e9edf2; +$gray-7: #f7f8fa; -$white: #fff; +$white: #fff; // Accent colors // ------------------------- -$blue: #2AB2E4; -$blue-dark: #3CAAD6; -$green: #3aa655; -$red: #d44939; -$yellow: #FF851B; -$orange: #Ff7941; -$pink: #E671B8; -$purple: #9954BB; -$variable: #2AB2E4; +$blue: #61c2f2; +$blue-dark: #0083b3; +$green: #3aa655; +$red: #d44939; +$yellow: #ff851b; +$orange: #ff7941; +$pink: #e671b8; +$purple: #9954bb; +$variable: $blue; -$brand-primary: $orange; -$brand-success: $green; -$brand-warning: $orange; -$brand-danger: $red; +$brand-primary: $orange; +$brand-success: $green; +$brand-warning: $orange; +$brand-danger: $red; + +$query-blue: $blue-dark; // Status colors // ------------------------- -$online: #01A64F; -$warn: #F79520; -$critical: #EC2128; +$online: #01a64f; +$warn: #f79520; +$critical: #ec2128; // Scaffolding // ------------------------- -$body-bg: $white; -$page-bg: $white; -$body-color: $gray-1; -$text-color: $gray-1; -$text-color-strong: $white; -$text-color-weak: $gray-3; -$text-color-faint: $gray-4; -$text-color-emphasis: $dark-5; +$body-bg: $gray-7; +$page-bg: $gray-7; +$body-color: $gray-1; +$text-color: $dark-4; +$text-color-strong: $white; +$text-color-weak: $gray-2; +$text-color-faint: $gray-4; +$text-color-emphasis: $dark-5; $text-shadow-strong: none; $text-shadow-faint: none; +$textShadow: none; // gradients -$brand-gradient: linear-gradient(to right, rgba(255,213,0,1.0) 0%, rgba(255,68,0,1.0) 99%, rgba(255,68,0,1.0) 100%); -$page-gradient: linear-gradient(60deg, transparent 70%, darken($page-bg, 4%) 98%); +$brand-gradient: linear-gradient( + to right, + rgba(255, 213, 0, 1) 0%, + rgba(255, 68, 0, 1) 99%, + rgba(255, 68, 0, 1) 100% +); +$page-gradient: linear-gradient(-60deg, transparent 70%, $gray-7 98%); // Links // ------------------------- -$link-color: $gray-1; -$link-color-disabled: lighten($link-color, 30%); -$link-hover-color: darken($link-color, 20%); -$external-link-color: $blue; +$link-color: $gray-1; +$link-color-disabled: lighten($link-color, 30%); +$link-hover-color: darken($link-color, 20%); +$external-link-color: $blue; // Typography // ------------------------- -$headings-color: $text-color; -$abbr-border-color: $gray-2 !default; -$text-muted: $text-color-weak; +$headings-color: $text-color; +$abbr-border-color: $gray-2 !default; +$text-muted: $text-color-weak; -$blockquote-small-color: $gray-2 !default; +$blockquote-small-color: $gray-2 !default; $blockquote-border-color: $gray-3 !default; $hr-border-color: $dark-3 !default; // Components $component-active-color: $white !default; -$component-active-bg: $brand-primary !default; +$component-active-bg: $brand-primary !default; // Panel // ------------------------- -$panel-bg: $gray-7; -$panel-border: solid 1px $gray-6; -$panel-drop-zone-bg: repeating-linear-gradient(-128deg, $body-bg, $body-bg 10px, $gray-6 10px, $gray-6 20px); -$panel-menu-border: solid 1px white; +$panel-bg: $white; +$panel-border-color: $gray-5; +$panel-border: solid 1px $panel-border-color; +$panel-drop-zone-bg: repeating-linear-gradient( + -128deg, + $body-bg, + $body-bg 10px, + $gray-6 10px, + $gray-6 20px +); +$panel-header-hover-bg: $gray-6; +$panel-header-menu-hover-bg: $gray-4; +$panel-edit-shadow: 0 0 30px 20px $black; -$divider-border-color: $gray-2; +// Page header +$page-header-bg: linear-gradient(90deg, $white, $gray-7); +$page-header-shadow: inset 0px -3px 10px $gray-6; +$page-header-border-color: $gray-4; + +$divider-border-color: $gray-2; // Graphite Target Editor -$tight-form-border: $gray-4; -$tight-form-bg: $gray-6; +$tight-form-bg: #eaebee; -$tight-form-func-bg: $gray-5; -$tight-form-func-highlight-bg: $gray-6; +$tight-form-func-bg: $gray-5; +$tight-form-func-highlight-bg: $gray-6; -$modal-background: $body-bg; -$code-tag-bg: $gray-6; -$code-tag-border: darken($code-tag-bg, 3%); +$modal-backdrop-bg: $body-bg; +$code-tag-bg: $gray-6; +$code-tag-border: darken($code-tag-bg, 3%); + +// cards +$card-background: linear-gradient(135deg, $gray-6, $gray-5); +$card-background-hover: linear-gradient(135deg, $gray-5, $gray-6); +$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, 0.1), + 1px 1px 0 0 rgba(0, 0, 0, 0.1); // Lists -$grafanaListBackground: $gray-6; -$grafanaListAccent: $gray-5; -$grafanaListBorderTop: $gray-3; -$grafanaListBorderBottom: $gray-3; -$grafanaListHighlight: $gray-5; -$grafanaListMainLinkColor: $text-color; - +$list-item-bg: linear-gradient(135deg, $gray-5, $gray-6); //$card-background; +$list-item-hover-bg: darken($gray-5, 5%); +$list-item-link-color: $text-color; +$list-item-shadow: $card-shadow; // Tables // ------------------------- -$table-bg: transparent; // overall background-color -$table-bg-accent: $gray-5; // for striping -$table-bg-hover: $gray-5; // for hover -$table-bg-active: $table-bg-hover !default; -$table-border: $gray-3; // table and cell border +$table-bg: transparent; // overall background-color +$table-bg-accent: $gray-5; // for striping +$table-bg-hover: $gray-5; // for hover +$table-bg-active: $table-bg-hover !default; +$table-border: $gray-3; // table and cell border + +$table-bg-odd: $gray-5; // Scrollbars -$scrollbarBackground: $gray-5; -$scrollbarBackground2: $gray-5; -$scrollbarBorder: $gray-4; +$scrollbarBackground: $gray-5; +$scrollbarBackground2: $gray-5; +$scrollbarBorder: $gray-4; // Buttons // ------------------------- -$btn-primary-bg: $brand-primary; -$btn-primary-bg-hl: lighten($brand-primary, 8%); +$btn-primary-bg: $brand-primary; +$btn-primary-bg-hl: lighten($brand-primary, 8%); -$btn-secondary-bg: $blue-dark; -$btn-secondary-bg-hl: lighten($blue-dark, 4%); +$btn-secondary-bg: $blue-dark; +$btn-secondary-bg-hl: lighten($blue-dark, 4%); -$btn-success-bg: lighten($green, 3%); -$btn-success-bg-hl: darken($green, 3%); +$btn-success-bg: lighten($green, 3%); +$btn-success-bg-hl: darken($green, 3%); -$btn-warning-bg: lighten($orange, 3%); -$btn-warning-bg-hl: darken($orange, 3%); +$btn-warning-bg: lighten($orange, 3%); +$btn-warning-bg-hl: darken($orange, 3%); -$btn-danger-bg: lighten($red, 3%); -$btn-danger-bg-hl: darken($red, 3%); +$btn-danger-bg: lighten($red, 3%); +$btn-danger-bg-hl: darken($red, 3%); -$btn-inverse-bg: $gray-5; -$btn-inverse-bg-hl: darken($gray-5, 5%); -$btn-inverse-text-color: $dark-4; +$btn-inverse-bg: $gray-6; +$btn-inverse-bg-hl: darken($gray-6, 5%); +$btn-inverse-text-color: $gray-1; +$btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4); $btn-link-color: $gray-1; -$btn-divider-left: $gray-4; -$btn-divider-right: $gray-7; -$btn-drag-image: '../img/grab_light.svg'; +$btn-divider-left: $gray-4; +$btn-divider-right: $gray-7; +$btn-drag-image: "../img/grab_light.svg"; $iconContainerBackground: $white; // Forms // ------------------------- -$input-bg: $gray-7; -$input-bg-disabled: $gray-5; +$input-bg: $white; +$input-bg-disabled: $gray-5; -$input-color: $dark-3; -$input-border-color: $gray-5; -$input-box-shadow: none; -$input-border-focus: $blue !default; -$input-box-shadow-focus: $blue !default; -$input-color-placeholder: $gray-4 !default; -$input-label-bg: $gray-6; -$input-invalid-border-color: lighten($red, 5%); +$input-color: $dark-3; +$input-border-color: $gray-5; +$input-box-shadow: none; +$input-border-focus: $blue !default; +$input-box-shadow-focus: $blue !default; +$input-color-placeholder: $gray-4 !default; +$input-label-bg: $gray-5; +$input-label-border-color: $gray-5; +$input-invalid-border-color: lighten($red, 5%); // Sidemenu // ------------------------- -$side-menu-bg: $body-bg; -$side-menu-item-hover-bg: $gray-6; -$side-menu-opacity: 0.97; +$side-menu-bg: $dark-2; +$side-menu-item-hover-bg: $gray-1; +$side-menu-shadow: 5px 0px 10px -5px $gray-1; +$side-menu-link-color: $gray-6; + +// Menu dropdowns +// ------------------------- +$menu-dropdown-bg: $gray-7; +$menu-dropdown-hover-bg: $gray-6; +$menu-dropdown-border-color: $gray-4; +$menu-dropdown-shadow: 5px 5px 10px -5px $gray-1; + +// Breadcrumb +// ------------------------- +$page-nav-bg: $gray-5; +$page-nav-shadow: 5px 5px 20px -5px $gray-4; +$page-nav-breadcrumb-color: $black; +$breadcrumb-hover-hl: #d9dadd; + +// Tabs +// ------------------------- +$tab-border-color: $gray-5; // search -$search-shadow: 0 5px 30px 0 lighten($gray-2, 30%); +$search-shadow: 0 5px 30px 0 $gray-4; +$search-filter-box-bg: $gray-7; // Dropdowns // ------------------------- -$dropdownBackground: $white; -$dropdownBorder: $tight-form-border; -$dropdownDividerTop: $gray-6; -$dropdownDividerBottom: $white; -$dropdownDivider: $dropdownDividerTop; -$dropdownTitle: $gray-3; +$dropdownBackground: $white; +$dropdownBorder: $gray-4; +$dropdownDividerTop: $gray-6; +$dropdownDividerBottom: $white; +$dropdownDivider: $dropdownDividerTop; +$dropdownTitle: $gray-3; -$dropdownLinkColor: $dark-3; -$dropdownLinkColorHover: $link-color; -$dropdownLinkColorActive: $link-color; - -$dropdownLinkBackgroundActive: $gray-6; -$dropdownLinkBackgroundHover: $gray-6; +$dropdownLinkColor: $dark-3; +$dropdownLinkColorHover: $link-color; +$dropdownLinkColorActive: $link-color; +$dropdownLinkBackgroundActive: $gray-6; +$dropdownLinkBackgroundHover: $gray-6; // COMPONENT VARIABLES // -------------------------------------------------- - // Input placeholder text color // ------------------------- -$placeholderText: $gray-2; - +$placeholderText: $gray-2; // Hr border color // ------------------------- -$hrBorder: $gray-3; - +$hrBorder: $gray-3; // Horizontal forms & lists // ------------------------- -$horizontalComponentOffset: 180px; - +$horizontalComponentOffset: 180px; // Wells // ------------------------- -$wellBackground: $gray-3; - +$wellBackground: $gray-3; // Navbar // ------------------------- -$navbarHeight: 52px; -$navbarBackgroundHighlight: #f8f8f8; -$navbarBackground: #f8f8f8; -$navbarBorder: 1px solid $tight-form-border; +$navbarHeight: 52px; +$navbarBackgroundHighlight: $white; +$navbarBackground: $white; +$navbarBorder: 1px solid $gray-4; +$navbarShadow: 0 0 3px #c1c1c1; -$navbarText: #666; -$navbarLinkColor: #666; -$navbarLinkColorHover: #333; -$navbarLinkColorActive: #555; -$navbarLinkBackgroundHover: transparent; -$navbarLinkBackgroundActive: darken($navbarBackground, 6.5%); -$navbarDropdownShadow: inset 0px 4px 7px -4px darken($body-bg, 20%); +$navbarText: #444; +$navbarLinkColor: #444; +$navbarLinkColorHover: #000; +$navbarLinkColorActive: #333; +$navbarLinkBackgroundHover: transparent; +$navbarLinkBackgroundActive: darken($navbarBackground, 6.5%); +$navbarDropdownShadow: inset 0px 4px 7px -4px darken($body-bg, 20%); -$navbarBrandColor: $navbarLinkColor; +$navbarBrandColor: $navbarLinkColor; -$navbarButtonBackground: lighten($navbarBackground, 3%); +$navbarButtonBackground: lighten($navbarBackground, 3%); $navbarButtonBackgroundHighlight: lighten($navbarBackground, 5%); +$navbar-button-border: $gray-4; // Pagination // ------------------------- -$paginationBackground: $gray-2; -$paginationBorder: transparent; -$paginationActiveBackground: $blue; - +$paginationBackground: $gray-2; +$paginationBorder: transparent; +$paginationActiveBackground: $blue; // Form states and alerts // ------------------------- -$warning-text-color: lighten($orange, 10%); -$error-text-color: lighten($red, 10%); -$success-text-color: lighten($green, 10%); -$info-text-color: $blue; +$warning-text-color: lighten($orange, 10%); +$error-text-color: lighten($red, 10%); +$success-text-color: lighten($green, 10%); +$info-text-color: $blue; -$alert-error-bg: linear-gradient(90deg, #d44939, #e0603d); -$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274); -$alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d); -$alert-info-bg: $blue-dark; +$alert-error-bg: linear-gradient(90deg, #d44939, #e04d3d); +$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274); +$alert-warning-bg: linear-gradient(90deg, #d44939, #e04d3d); +$alert-info-bg: $blue-dark; // popover -$popover-bg: $panel-bg; -$popover-color: $text-color; -$popover-border-color: $gray-5; -$popover-shadow: 0 0 20px $white; +$popover-bg: $page-bg; +$popover-color: $text-color; +$popover-border-color: $gray-5; +$popover-shadow: 0 0 20px $white; -$popover-help-bg: $blue-dark; -$popover-help-color: $gray-6; -$popover-error-bg: $btn-danger-bg; +$popover-help-bg: $blue-dark; +$popover-help-color: $gray-6; +$popover-error-bg: $btn-danger-bg; // Tooltips and popovers // ------------------------- -$tooltipColor: $popover-help-color; -$tooltipBackground: $popover-help-bg; -$tooltipArrowWidth: 5px; -$tooltipArrowColor: $tooltipBackground; -$tooltipLinkColor: lighten($popover-help-color, 5%); -$graph-tooltip-bg: $gray-5; +$tooltipColor: $popover-help-color; +$tooltipBackground: $popover-help-bg; +$tooltipArrowWidth: 5px; +$tooltipArrowColor: $tooltipBackground; +$tooltipLinkColor: lighten($popover-help-color, 5%); +$graph-tooltip-bg: $gray-5; // images -$checkboxImageUrl: '../img/checkbox_white.png'; - -// cards -$card-background: linear-gradient(135deg, $gray-5, $gray-6); -$card-background-hover: linear-gradient(135deg, $gray-6, $gray-7); -$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1); +$checkboxImageUrl: "../img/checkbox_white.png"; // info box -$info-box-background: linear-gradient(135deg, #f1fbff, #d7ebff); +$info-box-background: linear-gradient( + 100deg, + $blue-dark, + darken($blue-dark, 5%) +); +$info-box-color: $gray-7; // footer -$footer-link-color: $gray-3; -$footer-link-hover: $dark-5; +$footer-link-color: $gray-3; +$footer-link-hover: $dark-5; // collapse box $collapse-box-body-border: $gray-4; @@ -312,36 +355,36 @@ $json-explorer-default-color: black; $json-explorer-string-color: green; $json-explorer-number-color: blue; $json-explorer-boolean-color: red; -$json-explorer-null-color: #855A00; +$json-explorer-null-color: #855a00; $json-explorer-undefined-color: rgb(202, 11, 105); -$json-explorer-function-color: #FF20ED; +$json-explorer-function-color: #ff20ed; $json-explorer-rotate-time: 100ms; $json-explorer-toggler-opacity: 0.6; -$json-explorer-toggler-color: #45376F; +$json-explorer-toggler-color: #45376f; $json-explorer-bracket-color: blue; -$json-explorer-key-color: #00008B; +$json-explorer-key-color: #00008b; $json-explorer-url-color: blue; // Changelog and diff // ------------------------- -$diff-label-bg: $gray-5; -$diff-label-fg: $gray-2; +$diff-label-bg: $gray-5; +$diff-label-fg: $gray-2; -$diff-switch-bg: $gray-7; -$diff-switch-disabled: $gray-5; +$diff-switch-bg: $gray-7; +$diff-switch-disabled: $gray-5; -$diff-arrow-color: $dark-3; -$diff-group-bg: $gray-7; +$diff-arrow-color: $dark-3; +$diff-group-bg: $gray-7; -$diff-json-bg: $gray-5; -$diff-json-fg: $gray-2; +$diff-json-bg: $gray-5; +$diff-json-fg: $gray-2; -$diff-json-added: lighten(desaturate($green, 30%), 10%); -$diff-json-deleted: desaturate($red, 35%); +$diff-json-added: lighten(desaturate($green, 30%), 10%); +$diff-json-deleted: desaturate($red, 35%); -$diff-json-old: #5a372a; -$diff-json-new: #664e33; +$diff-json-old: #5a372a; +$diff-json-new: #664e33; -$diff-json-changed-fg: $gray-6; +$diff-json-changed-fg: $gray-6; $diff-json-changed-num: $gray-4; -$diff-json-icon: $gray-4; +$diff-json-icon: $gray-4; diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 186b22f2390..f46cacb0dd1 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -1,105 +1,79 @@ - - // Options // // Quickly modify global styling by enabling or disabling optional features. -$enable-flex: true !default; -$enable-rounded: true !default; -$enable-shadows: false !default; -$enable-gradients: false !default; -$enable-transitions: false !default; -$enable-hover-media-query: false !default; -$enable-grid-classes: true !default; -$enable-print-styles: true !default; - +$enable-flex: true !default; +$enable-rounded: true !default; +$enable-shadows: false !default; +$enable-gradients: false !default; +$enable-transitions: false !default; +$enable-hover-media-query: false !default; +$enable-grid-classes: true !default; +$enable-print-styles: true !default; // Spacing // // Control the default styling of most Bootstrap elements by modifying these // variables. Mostly focused on spacing. -$spacer: 1rem !default; +$spacer: 1rem !default; $spacer-x: $spacer !default; $spacer-y: $spacer !default; $spacers: ( - 0: ( - x: 0, - y: 0 - ), - 1: ( - x: $spacer-x, - y: $spacer-y - ), - 2: ( - x: ($spacer-x * 1.5), - y: ($spacer-y * 1.5) - ), - 3: ( - x: ($spacer-x * 3), - y: ($spacer-y * 3) + 0: (x: 0, y: 0), + 1: (x: $spacer-x, y: $spacer-y), + 2: (x: ($spacer-x * 1.5), y: ($spacer-y * 1.5)), + 3: (x: ($spacer-x * 3), y: ($spacer-y * 3)) ) -) !default; + !default; $border-width: 1px !default; - // Grid breakpoints // // Define the minimum and maximum dimensions at which your layout will change, // adapting to different screen sizes, for use in media queries. -$grid-breakpoints: ( - xs: 0, - sm: 544px, - md: 768px, - lg: 992px, - xl: 1200px -) !default; - +$grid-breakpoints: (xs: 0, sm: 544px, md: 768px, lg: 992px, xl: 1200px) !default; // Grid containers // // Define the maximum width of `.container` for different screen sizes. -$container-max-widths: ( - sm: 576px, - md: 720px, - lg: 940px, - xl: 1140px -) !default; +$container-max-widths: (sm: 576px, md: 720px, lg: 940px, xl: 1080px) !default; // Grid columns // // Set the number of columns and specify the width of the gutters. -$grid-columns: 12 !default; +$grid-columns: 12 !default; $grid-gutter-width: 30px !default; -$enable-flex: false; +$enable-flex: true; // Typography // ------------------------- -$font-family-sans-serif: "Open Sans", Helvetica, Arial, sans-serif; -$font-family-serif: Georgia, "Times New Roman", Times, serif; -$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; -$font-family-base: $font-family-sans-serif !default; +$font-family-sans-serif: "Roboto", Helvetica, Arial, sans-serif; +$font-family-serif: Georgia, "Times New Roman", Times, serif; +$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; +$font-family-base: $font-family-sans-serif !default; $font-size-root: 14px !default; +$font-size-base: 13px !default; -$font-size-base: 1rem !default; -$font-size-lg: 1.25rem !default; -$font-size-sm: .875rem !default; -$font-size-xs: .75rem !default; +$font-size-lg: 18px !default; +$font-size-md: 14px !default; +$font-size-sm: 12px !default; +$font-size-xs: 10px !default; $line-height-base: 1.5 !default; -$font-weight-semi-bold: 600; +$font-weight-semi-bold: 500; -$font-size-h1: 2.0rem !default; +$font-size-h1: 2rem !default; $font-size-h2: 1.75rem !default; -$font-size-h3: 1.50rem !default; -$font-size-h4: 1.30rem !default; -$font-size-h5: 1.20rem !default; +$font-size-h3: 1.5rem !default; +$font-size-h4: 1.3rem !default; +$font-size-h5: 1.2rem !default; $font-size-h6: 1rem !default; $display1-size: 6rem !default; @@ -112,16 +86,16 @@ $display2-weight: 400 !default; $display3-weight: 400 !default; $display4-weight: 400 !default; -$lead-font-size: 1.25rem !default; +$lead-font-size: 1.25rem !default; $lead-font-weight: 300 !default; $headings-margin-bottom: ($spacer / 2) !default; -$headings-font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; -$headings-font-weight: 400 !default; -$headings-line-height: 1.1 !default; +$headings-font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; +$headings-font-weight: 400 !default; +$headings-line-height: 1.1 !default; -$blockquote-font-size: ($font-size-base * 1.25) !default; -$blockquote-border-width: .25rem !default; +$blockquote-font-size: ($font-size-base * 1.25) !default; +$blockquote-border-width: 0.25rem !default; $hr-border-width: $border-width !default; $dt-font-weight: bold !default; @@ -131,15 +105,15 @@ $list-inline-padding: 5px !default; // // Define common padding and border radius sizes and more. -$line-height-lg: (4 / 3) !default; -$line-height-sm: 1.5 !default; +$line-height-lg: (4 / 3) !default; +$line-height-sm: 1.5 !default; -$border-radius: 0.2rem !default; -$border-radius-lg: 0.3rem !default; -$border-radius-sm: 0.1rem !default; +$border-radius: 3px !default; +$border-radius-lg: 5px !default; +$border-radius-sm: 2px!default; -$caret-width: .3em !default; -$caret-width-lg: $caret-width !default; +$caret-width: 0.3em !default; +$caret-width-lg: $caret-width !default; // Page @@ -148,85 +122,108 @@ $page-sidebar-margin: 4rem; // Links // ------------------------- -$link-decoration: none !default; -$link-hover-decoration: none !default; +$link-decoration: none !default; +$link-hover-decoration: none !default; // Tables // // Customizes the `.table` component with basic values, each used across all table variations. -$table-cell-padding: .75rem !default; -$table-sm-cell-padding: .3rem !default; +$table-cell-padding: 4px 10px !default; +$table-sm-cell-padding: 0.3rem !default; // Forms -$input-padding-x: .75rem !default; -$input-padding-y: .6rem !default; -$input-line-height: 1.35rem !default; +$input-padding-x: 10px !default; +$input-padding-y: 8px !default; +$input-line-height: 18px !default; -$input-btn-border-width: 1px; -$input-border-radius: 0 $border-radius $border-radius 0 !default; -$input-border-radius-lg: 0 $border-radius-lg $border-radius-lg 0 !default; -$input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default; +$input-btn-border-width: 1px; +$input-border-radius: 0 $border-radius $border-radius 0 !default; +$input-border-radius-lg: 0 $border-radius-lg $border-radius-lg 0 !default; +$input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default; -$label-border-radius: $border-radius 0 0 $border-radius !default; -$label-border-radius-lg: $border-radius-lg 0 0 $border-radius-lg !default; -$label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default; +$label-border-radius: $border-radius 0 0 $border-radius !default; +$label-border-radius-lg: $border-radius-lg 0 0 $border-radius-lg !default; +$label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default; -$input-padding-x-sm: .5rem !default; -$input-padding-y-sm: .25rem !default; +$input-padding-x-sm: 7px !default; +$input-padding-y-sm: 4px !default; -$input-padding-x-lg: 1.5rem !default; -$input-padding-y-lg: .75rem !default; +$input-padding-x-lg: 20px !default; +$input-padding-y-lg: 10px !default; -$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) !default; -$input-height-lg: (($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2)) !default; -$input-height-sm: (($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2)) !default; +$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) + !default; +$input-height-lg: ( + ($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2) + ) + !default; +$input-height-sm: ( + ($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2) + ) + !default; -$form-group-margin-bottom: $spacer-y !default; +$form-group-margin-bottom: $spacer-y !default; $gf-form-margin: 0.2rem; -$cursor-disabled: not-allowed !default; +$cursor-disabled: not-allowed !default; // Form validation icons -$form-icon-success: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E") !default; -$form-icon-warning: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E") !default; -$form-icon-danger: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E") !default; +$form-icon-success: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E") + !default; +$form-icon-warning: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E") + !default; +$form-icon-danger: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E") + !default; // Z-index master list // ------------------------- // Used for a bird's eye view of components dependent on the z-axis // Try to avoid customizing these :) -$zindex-dropdown: 1000; -$zindex-tooltip: 1020; -$zindex-navbar-fixed: 1030; -$zindex-modal-backdrop: 1040; -$zindex-modal: 1050; +$zindex-dropdown: 1000; +$zindex-navbar-fixed: 1020; +$zindex-sidemenu: 1025; +$zindex-tooltip: 1030; +$zindex-modal-backdrop: 1040; +$zindex-modal: 1050; +$zindex-typeahead: 1060; // Buttons // -$btn-padding-x: 1rem !default; -$btn-padding-y: .6rem !default; -$btn-line-height: 1.25 !default; -$btn-font-weight: normal !default; +$btn-padding-x: 1rem !default; +$btn-padding-y: 0.7rem !default; +$btn-line-height: 1 !default; +$btn-font-weight: 500 !default; -$btn-padding-x-sm: .5rem !default; -$btn-padding-y-sm: .25rem !default; +$btn-padding-x-sm: 0.5rem !default; +$btn-padding-y-sm: 0.25rem !default; -$btn-padding-x-lg: 1.5rem !default; -$btn-padding-y-lg: .75rem !default; +$btn-padding-x-lg: 21px !default; +$btn-padding-y-lg: 11px !default; -$btn-border-radius: 2px; +$btn-padding-x-xl: 21px !default; +$btn-padding-y-xl: 11px !default; + +$btn-border-radius: 2px; + +$btn-semi-transparent: rgba(0, 0, 0, 0.2) !default; // sidemenu -$side-menu-width: 14rem; +$side-menu-width: 60px; // dashboard -$panel-margin: 0.4rem; -$dashboard-padding: ($panel-margin * 2) $panel-margin $panel-margin $panel-margin; +$panel-margin: 10px; +$dashboard-padding: $panel-margin * 2; +$panel-padding: 0px 10px 5px 10px; // tabs -$tabs-padding-top: 0.6rem; -$tabs-padding-bottom: 0.4rem; -$tabs-top-margin: 0.5rem; +$tabs-padding: 10px 15px 9px; +$external-services: ( + github: (bgColor: #464646, borderColor: #393939, icon: ""), + google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ""), + grafanacom: (bgColor: inherit, borderColor: #393939, icon: ""), + oauth: (bgColor: inherit, borderColor: #393939, icon: "") + ) + !default; diff --git a/public/sass/base/_code.scss b/public/sass/base/_code.scss index a97f04bc84b..f3584285108 100644 --- a/public/sass/base/_code.scss +++ b/public/sass/base/_code.scss @@ -2,7 +2,6 @@ // Code (inline and blocK) // -------------------------------------------------- - // Inline and block code styles code, pre { diff --git a/public/sass/base/_fonts.scss b/public/sass/base/_fonts.scss index fb360ba5870..4e680872b5a 100644 --- a/public/sass/base/_fonts.scss +++ b/public/sass/base/_fonts.scss @@ -1,285 +1,290 @@ @import "base/font_awesome"; @import "base/grafana_icons"; -/* Open sans fonts*/ - /* cyrillic-ext */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 300; - src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTa-j2U0lmluP9RWlSytm3ho.woff2) format('woff2'); - unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; + font-weight: 400; + src: local("Roboto"), local("Roboto-Regular"), + url(../fonts/roboto/ek4gzZ-GeXAPcSbHtCeQI_esZW2xOQ-xsNqO47m55DA.woff2) + format("woff2"); + unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F; } /* cyrillic */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 300; - src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTZX5f-9o1vgP2EXwfjgl7AY.woff2) format('woff2'); - unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; + font-weight: 400; + src: local("Roboto"), local("Roboto-Regular"), + url(../fonts/roboto/mErvLBYg_cXG3rLvUsKT_fesZW2xOQ-xsNqO47m55DA.woff2) + format("woff2"); + unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116; } /* greek-ext */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 300; - src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTRWV49_lSm1NYrwo-zkhivY.woff2) format('woff2'); - unicode-range: U+1F00-1FFF; + font-weight: 400; + src: local("Roboto"), local("Roboto-Regular"), + url(../fonts/roboto/-2n2p-_Y08sg57CNWQfKNvesZW2xOQ-xsNqO47m55DA.woff2) + format("woff2"); + unicode-range: U+1f00-1fff; } /* greek */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 300; - src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTaaRobkAwv3vxw3jMhVENGA.woff2) format('woff2'); - unicode-range: U+0370-03FF; + font-weight: 400; + src: local("Roboto"), local("Roboto-Regular"), + url(../fonts/roboto/u0TOpm082MNkS5K0Q4rhqvesZW2xOQ-xsNqO47m55DA.woff2) + format("woff2"); + unicode-range: U+0370-03ff; } /* vietnamese */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 300; - src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTf8zf_FOSsgRmwsS7Aa9k2w.woff2) format('woff2'); - unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; + font-weight: 400; + src: local("Roboto"), local("Roboto-Regular"), + url(../fonts/roboto/NdF9MtnOpLzo-noMoG0miPesZW2xOQ-xsNqO47m55DA.woff2) + format("woff2"); + unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab; } /* latin-ext */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 300; - src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTT0LW-43aMEzIO6XUTLjad8.woff2) format('woff2'); - unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; + font-weight: 400; + src: local("Roboto"), local("Roboto-Regular"), + url(../fonts/roboto/Fcx7Wwv8OzT71A3E1XOAjvesZW2xOQ-xsNqO47m55DA.woff2) + format("woff2"); + unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f, + U+A720-A7FF; } /* latin */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 300; - src: local('Open Sans Light'), local('OpenSans-Light'), url(../fonts/opensans/DXI1ORHCpsQm3Vp6mXoaTegdm0LZdjqr5-oayXSOefg.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; + font-weight: 400; + src: local("Roboto"), local("Roboto-Regular"), + url(../fonts/roboto/CWB0XYA8bzo0kSThX0UTuA.woff2) format("woff2"); + unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc, + U+2000-206f, U+2074, U+20ac, U+2212, U+2215; } /* cyrillic-ext */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 400; - src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/K88pR3goAWT7BTt32Z01mxJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); - unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; + font-weight: 500; + src: local("Roboto Medium"), local("Roboto-Medium"), + url(../fonts/roboto/ZLqKeelYbATG60EpZBSDyxJtnKITppOI_IvcXXDNrsc.woff2) + format("woff2"); + unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F; } /* cyrillic */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 400; - src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/RjgO7rYTmqiVp7vzi-Q5URJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); - unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; + font-weight: 500; + src: local("Roboto Medium"), local("Roboto-Medium"), + url(../fonts/roboto/oHi30kwQWvpCWqAhzHcCSBJtnKITppOI_IvcXXDNrsc.woff2) + format("woff2"); + unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116; } /* greek-ext */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 400; - src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/LWCjsQkB6EMdfHrEVqA1KRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); - unicode-range: U+1F00-1FFF; + font-weight: 500; + src: local("Roboto Medium"), local("Roboto-Medium"), + url(../fonts/roboto/rGvHdJnr2l75qb0YND9NyBJtnKITppOI_IvcXXDNrsc.woff2) + format("woff2"); + unicode-range: U+1f00-1fff; } /* greek */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 400; - src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/xozscpT2726on7jbcb_pAhJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); - unicode-range: U+0370-03FF; + font-weight: 500; + src: local("Roboto Medium"), local("Roboto-Medium"), + url(../fonts/roboto/mx9Uck6uB63VIKFYnEMXrRJtnKITppOI_IvcXXDNrsc.woff2) + format("woff2"); + unicode-range: U+0370-03ff; } /* vietnamese */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 400; - src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/59ZRklaO5bWGqF5A9baEERJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); - unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; + font-weight: 500; + src: local("Roboto Medium"), local("Roboto-Medium"), + url(../fonts/roboto/mbmhprMH69Zi6eEPBYVFhRJtnKITppOI_IvcXXDNrsc.woff2) + format("woff2"); + unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab; } /* latin-ext */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 400; - src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/u-WUoqrET9fUeobQW7jkRRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2'); - unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; + font-weight: 500; + src: local("Roboto Medium"), local("Roboto-Medium"), + url(../fonts/roboto/oOeFwZNlrTefzLYmlVV1UBJtnKITppOI_IvcXXDNrsc.woff2) + format("woff2"); + unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f, + U+A720-A7FF; } /* latin */ @font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: normal; - font-weight: 400; - src: local('Open Sans'), local('OpenSans'), url(../fonts/opensans/cJZKeOuBrn4kERxqtaUH3VtXRa8TVwTICgirnJhmVJw.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; + font-weight: 500; + src: local("Roboto Medium"), local("Roboto-Medium"), + url(../fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2) + format("woff2"); + unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc, + U+2000-206f, U+2074, U+20ac, U+2212, U+2215; } /* cyrillic-ext */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 600; - src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSq-j2U0lmluP9RWlSytm3ho.woff2) format('woff2'); - unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; + font-family: "Roboto"; + font-style: italic; + font-weight: 400; + src: local("Roboto Italic"), local("Roboto-Italic"), + url(../fonts/roboto/WxrXJa0C3KdtC7lMafG4dRTbgVql8nDJpwnrE27mub0.woff2) + format("woff2"); + unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F; } /* cyrillic */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 600; - src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSpX5f-9o1vgP2EXwfjgl7AY.woff2) format('woff2'); - unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; + font-family: "Roboto"; + font-style: italic; + font-weight: 400; + src: local("Roboto Italic"), local("Roboto-Italic"), + url(../fonts/roboto/OpXUqTo0UgQQhGj_SFdLWBTbgVql8nDJpwnrE27mub0.woff2) + format("woff2"); + unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116; } /* greek-ext */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 600; - src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNShWV49_lSm1NYrwo-zkhivY.woff2) format('woff2'); - unicode-range: U+1F00-1FFF; + font-family: "Roboto"; + font-style: italic; + font-weight: 400; + src: local("Roboto Italic"), local("Roboto-Italic"), + url(../fonts/roboto/1hZf02POANh32k2VkgEoUBTbgVql8nDJpwnrE27mub0.woff2) + format("woff2"); + unicode-range: U+1f00-1fff; } /* greek */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 600; - src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSqaRobkAwv3vxw3jMhVENGA.woff2) format('woff2'); - unicode-range: U+0370-03FF; + font-family: "Roboto"; + font-style: italic; + font-weight: 400; + src: local("Roboto Italic"), local("Roboto-Italic"), + url(../fonts/roboto/cDKhRaXnQTOVbaoxwdOr9xTbgVql8nDJpwnrE27mub0.woff2) + format("woff2"); + unicode-range: U+0370-03ff; } /* vietnamese */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 600; - src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSv8zf_FOSsgRmwsS7Aa9k2w.woff2) format('woff2'); - unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; + font-family: "Roboto"; + font-style: italic; + font-weight: 400; + src: local("Roboto Italic"), local("Roboto-Italic"), + url(../fonts/roboto/K23cxWVTrIFD6DJsEVi07RTbgVql8nDJpwnrE27mub0.woff2) + format("woff2"); + unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab; } /* latin-ext */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 600; - src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSj0LW-43aMEzIO6XUTLjad8.woff2) format('woff2'); - unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; + font-family: "Roboto"; + font-style: italic; + font-weight: 400; + src: local("Roboto Italic"), local("Roboto-Italic"), + url(../fonts/roboto/vSzulfKSK0LLjjfeaxcREhTbgVql8nDJpwnrE27mub0.woff2) + format("woff2"); + unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f, + U+A720-A7FF; } /* latin */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 600; - src: local('Open Sans Semibold'), local('OpenSans-Semibold'), url(../fonts/opensans/MTP_ySUJH_bn48VBG8sNSugdm0LZdjqr5-oayXSOefg.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; + font-family: "Roboto"; + font-style: italic; + font-weight: 400; + src: local("Roboto Italic"), local("Roboto-Italic"), + url(../fonts/roboto/vPcynSL0qHq_6dX7lKVByfesZW2xOQ-xsNqO47m55DA.woff2) + format("woff2"); + unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc, + U+2000-206f, U+2074, U+20ac, U+2212, U+2215; } /* cyrillic-ext */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 700; - src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzK-j2U0lmluP9RWlSytm3ho.woff2) format('woff2'); - unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; + font-family: "Roboto"; + font-style: italic; + font-weight: 500; + src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"), + url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0TTOQ_MqJVwkKsUn0wKzc2I.woff2) + format("woff2"); + unicode-range: U+0460-052f, U+20b4, U+2de0-2dff, U+A640-A69F; } /* cyrillic */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 700; - src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzJX5f-9o1vgP2EXwfjgl7AY.woff2) format('woff2'); - unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; + font-family: "Roboto"; + font-style: italic; + font-weight: 500; + src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"), + url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0TUj_cnvWIuuBMVgbX098Mw.woff2) + format("woff2"); + unicode-range: U+0400-045f, U+0490-0491, U+04b0-04b1, U+2116; } /* greek-ext */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 700; - src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzBWV49_lSm1NYrwo-zkhivY.woff2) format('woff2'); - unicode-range: U+1F00-1FFF; + font-family: "Roboto"; + font-style: italic; + font-weight: 500; + src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"), + url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0UbcKLIaa1LC45dFaAfauRA.woff2) + format("woff2"); + unicode-range: U+1f00-1fff; } /* greek */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 700; - src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzKaRobkAwv3vxw3jMhVENGA.woff2) format('woff2'); - unicode-range: U+0370-03FF; + font-family: "Roboto"; + font-style: italic; + font-weight: 500; + src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"), + url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0Wo_sUJ8uO4YLWRInS22T3Y.woff2) + format("woff2"); + unicode-range: U+0370-03ff; } /* vietnamese */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 700; - src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzP8zf_FOSsgRmwsS7Aa9k2w.woff2) format('woff2'); - unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; + font-family: "Roboto"; + font-style: italic; + font-weight: 500; + src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"), + url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0b6up8jxqWt8HVA3mDhkV_0.woff2) + format("woff2"); + unicode-range: U+0102-0103, U+1ea0-1ef9, U+20ab; } /* latin-ext */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 700; - src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzD0LW-43aMEzIO6XUTLjad8.woff2) format('woff2'); - unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; + font-family: "Roboto"; + font-style: italic; + font-weight: 500; + src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"), + url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0SYE0-AqJ3nfInTTiDXDjU4.woff2) + format("woff2"); + unicode-range: U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f, + U+A720-A7FF; } /* latin */ @font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 700; - src: local('Open Sans Bold'), local('OpenSans-Bold'), url(../fonts/opensans/k3k702ZOKiLJc3WVjuplzOgdm0LZdjqr5-oayXSOefg.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; -} -/* cyrillic-ext */ -@font-face { - font-family: 'Open Sans'; + font-family: "Roboto"; font-style: italic; - font-weight: 400; - src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBjTOQ_MqJVwkKsUn0wKzc2I.woff2) format('woff2'); - unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; -} -/* cyrillic */ -@font-face { - font-family: 'Open Sans'; - font-style: italic; - font-weight: 400; - src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBjUj_cnvWIuuBMVgbX098Mw.woff2) format('woff2'); - unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; -} -/* greek-ext */ -@font-face { - font-family: 'Open Sans'; - font-style: italic; - font-weight: 400; - src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBkbcKLIaa1LC45dFaAfauRA.woff2) format('woff2'); - unicode-range: U+1F00-1FFF; -} -/* greek */ -@font-face { - font-family: 'Open Sans'; - font-style: italic; - font-weight: 400; - src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBmo_sUJ8uO4YLWRInS22T3Y.woff2) format('woff2'); - unicode-range: U+0370-03FF; -} -/* vietnamese */ -@font-face { - font-family: 'Open Sans'; - font-style: italic; - font-weight: 400; - src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBr6up8jxqWt8HVA3mDhkV_0.woff2) format('woff2'); - unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; -} -/* latin-ext */ -@font-face { - font-family: 'Open Sans'; - font-style: italic; - font-weight: 400; - src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBiYE0-AqJ3nfInTTiDXDjU4.woff2) format('woff2'); - unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: 'Open Sans'; - font-style: italic; - font-weight: 400; - src: local('Open Sans Italic'), local('OpenSans-Italic'), url(../fonts/opensans/xjAJXh38I15wypJXxuGMBo4P5ICox8Kq3LLUNMylGO4.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; + font-weight: 500; + src: local("Roboto Medium Italic"), local("Roboto-MediumItalic"), + url(../fonts/roboto/OLffGBTaF0XFOW1gnuHF0Y4P5ICox8Kq3LLUNMylGO4.woff2) + format("woff2"); + unicode-range: U+0000-00ff, U+0131, U+0152-0153, U+02c6, U+02da, U+02dc, + U+2000-206f, U+2074, U+20ac, U+2212, U+2215; } diff --git a/public/sass/base/_forms.scss b/public/sass/base/_forms.scss index 3c74c7d81ef..3197eb57991 100644 --- a/public/sass/base/_forms.scss +++ b/public/sass/base/_forms.scss @@ -2,7 +2,6 @@ // Forms // -------------------------------------------------- - // GENERAL STYLES // -------------- @@ -20,7 +19,7 @@ legend { // Small small { - font-size: $line-height-base * .75; + font-size: $line-height-base * 0.75; color: $gray-2; } } @@ -100,11 +99,11 @@ input[type="checkbox"]:focus { // not a big fan of number fields input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; + -webkit-appearance: none; + margin: 0; } input[type="number"] { - -moz-appearance: textfield; + -moz-appearance: textfield; } // Placeholder // ------------------------- @@ -119,12 +118,24 @@ textarea { // ----------- // General classes for quick sizes -.input-mini { width: 60px; } -.input-small { width: 90px; } -.input-medium { width: 150px; } -.input-large { width: 210px; } -.input-xlarge { width: 270px; } -.input-xxlarge { width: 530px; } +.input-mini { + width: 60px; +} +.input-small { + width: 90px; +} +.input-medium { + width: 150px; +} +.input-large { + width: 210px; +} +.input-xlarge { + width: 270px; +} +.input-xxlarge { + width: 530px; +} // GRID SIZING FOR INPUTS // ---------------------- @@ -152,7 +163,7 @@ input[type="checkbox"][readonly] { background-color: transparent; } -input[type=text].input-fluid { +input[type="text"].input-fluid { width: 100%; box-sizing: border-box; padding: 10px; @@ -180,10 +191,10 @@ label.cr1 { padding: 0 0 0 20px; vertical-align: top; background: url($checkboxImageUrl) left top no-repeat; - cursor:pointer; + cursor: pointer; } -input[type="checkbox"].cr1:checked+label { +input[type="checkbox"].cr1:checked + label { background: url($checkboxImageUrl) 0px -18px no-repeat; } @@ -192,7 +203,7 @@ input[type="checkbox"].cr1:checked+label { display: block; overflow: hidden; padding-right: 10px; - input[type=text] { + input[type="text"] { width: 100%; padding: 5px 6px; height: 100%; diff --git a/public/sass/base/_grafana_icons.scss b/public/sass/base/_grafana_icons.scss index 42834e704b3..55e57f6d087 100644 --- a/public/sass/base/_grafana_icons.scss +++ b/public/sass/base/_grafana_icons.scss @@ -1,194 +1,201 @@ @font-face { - font-family: 'grafana-icons'; - src: url('../fonts/grafana-icons.eot?okx5td'); - src: url('../fonts/grafana-icons.eot?okx5td#iefix') format('embedded-opentype'), - url('../fonts/grafana-icons.ttf?okx5td') format('truetype'), - url('../fonts/grafana-icons.woff?okx5td') format('woff'), - url('../fonts/grafana-icons.svg?okx5td#grafana-icons') format('svg'); - font-weight: normal; - font-style: normal; + font-family: "grafana-icons"; + src: url("../fonts/grafana-icons.eot?okx5td"); + src: url("../fonts/grafana-icons.eot?okx5td#iefix") + format("embedded-opentype"), + url("../fonts/grafana-icons.ttf?okx5td") format("truetype"), + url("../fonts/grafana-icons.woff?okx5td") format("woff"), + url("../fonts/grafana-icons.svg?okx5td#grafana-icons") format("svg"); + font-weight: normal; + font-style: normal; } .icon-gf { - /* use !important to prevent issues with browser extensions that change fonts */ - font-family: 'grafana-icons' !important; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: "grafana-icons" !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + display: inline-block; + vertical-align: middle; - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-gf-fw { + width: 1.2857142857em; + text-align: center; } .inline-icon-gf { - vertical-align: middle; + vertical-align: middle; } .icon-gf-raintank_wordmark:before { - content: "\e600"; + content: "\e600"; } .micon-gf-raintank_icn:before { - content: "\e601"; + content: "\e601"; } .icon-gf-raintank_r-icn:before { - content: "\e905"; + content: "\e905"; } .icon-gf-check-alt:before { - content: "\e603"; + content: "\e603"; } .icon-gf-check:before { - content: "\e604"; + content: "\e604"; } .icon-gf-collector:before { - content: "\e605"; + content: "\e605"; } .icon-gf-dashboard:before { - content: "\e606"; + content: "\e606"; } .icon-gf-panel:before { - content: "\e904"; + content: "\e904"; } .icon-gf-datasources:before { - content: "\e607"; + content: "\e607"; } .icon-gf-endpoint-tiny:before { - content: "\e608"; + content: "\e608"; } .icon-gf-endpoint:before { - content: "\e609"; + content: "\e609"; } .icon-gf-page:before { - content: "\e908"; + content: "\e908"; } .icon-gf-filter:before { - content: "\e60a"; + content: "\e60a"; } .icon-gf-status:before { - content: "\e60b"; + content: "\e60b"; } .icon-gf-monitoring:before { - content: "\e60c"; + content: "\e60c"; } .icon-gf-monitoring-tiny:before { - content: "\e620"; + content: "\e620"; } .icon-gf-jump-to-dashboard:before { - content: "\e60d"; + content: "\e60d"; } .icon-gf-warn, .icon-gf-warning:before { - content: "\e60e"; + content: "\e60e"; } .icon-gf-nodata:before { - content: "\e60f"; + content: "\e60f"; } .icon-gf-critical:before { - content: "\e610"; + content: "\e610"; } .icon-gf-crit:before { content: "\e610"; } .icon-gf-online:before { - content: "\e611"; + content: "\e611"; } .icon-gf-event-error:before { - content: "\e623"; + content: "\e623"; } .icon-gf-event:before { - content: "\e624"; + content: "\e624"; } .icon-gf-sadface:before { - content: "\e907"; + content: "\e907"; } .icon-gf-private-collector:before { - content: "\e612"; + content: "\e612"; } .icon-gf-alert:before { - content: "\e61f"; + content: "\e61f"; } .icon-gf-alert-disabled:before { - content: "\e621"; + content: "\e621"; } .icon-gf-refresh:before { - content: "\e613"; + content: "\e613"; } .icon-gf-save:before { - content: "\e614"; + content: "\e614"; } .icon-gf-share:before { - content: "\e616"; + content: "\e616"; } .icon-gf-star:before { - content: "\e617"; + content: "\e617"; } .icon-gf-search:before { - content: "\e618"; + content: "\e618"; } .icon-gf-settings:before { - content: "\e615"; + content: "\e615"; } .icon-gf-add:before { - content: "\e619"; + content: "\e619"; } .icon-gf-remove:before { - content: "\e61a"; + content: "\e61a"; } .icon-gf-video:before { - content: "\e61b"; + content: "\e61b"; } .icon-gf-bulk_action:before { - content: "\e61c"; + content: "\e61c"; } .icon-gf-grabber:before { content: "\e90b"; -} +} .icon-gf-users:before { - content: "\e622"; + content: "\e622"; } .icon-gf-globe:before { - content: "\e61d"; + content: "\e61d"; } .icon-gf-snapshot:before { - content: "\e61e"; + content: "\e61e"; } .icon-gf-play-grafana-icon:before { - content: "\e629"; + content: "\e629"; } .icon-gf-grafana-icon:before { - content: "\e625"; + content: "\e625"; } .icon-gf-email:before { - content: "\e628"; + content: "\e628"; } .icon-gf-stopwatch:before { - content: "\e626"; + content: "\e626"; } .icon-gf-skull:before { - content: "\e900"; + content: "\e900"; } .icon-gf-probe:before { - content: "\e901"; + content: "\e901"; } .icon-gf-apps:before { - content: "\e902"; + content: "\e902"; } .icon-gf-scale:before { - content: "\e906"; + content: "\e906"; } .icon-gf-pending:before { - content: "\e909"; + content: "\e909"; } .icon-gf-verified:before { - content: "\e90a"; + content: "\e90a"; } .icon-gf-worldping:before { - content: "\e627"; + content: "\e627"; } .icon-gf-grafana_wordmark:before { - content: "\e903"; + content: "\e903"; } - diff --git a/public/sass/base/_grid.scss b/public/sass/base/_grid.scss index 32cd79f0094..bf825a155e9 100644 --- a/public/sass/base/_grid.scss +++ b/public/sass/base/_grid.scss @@ -1,4 +1,3 @@ - // Container widths // // Set the container width, and override it for fixed navbars in media queries. diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss new file mode 100644 index 00000000000..f8bc0f08076 --- /dev/null +++ b/public/sass/base/_icons.scss @@ -0,0 +1,188 @@ +.gicon { + line-height: 1; + display: inline-block; + width: 1.1057142857em; + height: 1.1057142857em; + text-align: center; + background-repeat: no-repeat; + background-position: center; + background-size: contain; + display: inline-block; + vertical-align: middle; +} + +.mini { + width: 0.8em; + height: 0.8em; +} + +.gicon-add-annotation { + 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"); +} + +.gicon-add-datasources { + 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"); +} + +.gicon-add-team { + 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"); +} + +.gicon-add-link { + 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"); +} + +.gicon-alert { + 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"); +} + +.gicon-alert-rules { + 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"); +} + +.gicon-annotation { + 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"); +} + +.gicon-apikeys { + background-image: url("../img/icons_#{$theme-name}_theme/icon_apikeys.svg"); +} + +.gicon-branding { + background-image: url("../img/grafana_icon.svg"); +} + +.gicon-cog { + background-image: url("../img/icons_#{$theme-name}_theme/icon_cog.svg"); +} + +.gicon-dashboard { + 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"); +} + +.gicon-dashboard-list { + 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"); +} + +.gicon-dashboard-import { + 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"); +} + +.gicon-folder-new { + 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"); +} + +.gicon-json { + background-image: url("../img/icons_#{$theme-name}_theme/icon_json.svg"); +} + +.gicon-link { + background-image: url("../img/icons_#{$theme-name}_theme/icon_link.svg"); +} + +.gicon-manage { + background-image: url("../img/icons_#{$theme-name}_theme/icon_sitemap.svg"); +} + +.gicon-org { + background-image: url("../img/icons_#{$theme-name}_theme/icon_org.svg"); +} + +.gicon-playlists { + background-image: url("../img/icons_#{$theme-name}_theme/icon_playlist.svg"); +} + +.gicon-plugins { + background-image: url("../img/icons_#{$theme-name}_theme/icon_plugins.svg"); +} + +.gicon-preferences { + background-image: url("../img/icons_#{$theme-name}_theme/icon_preferences.svg"); +} + +.gicon-question { + background-image: url("../img/icons_#{$theme-name}_theme/icon_question.svg"); +} + +.gicon-shield { + background-image: url("../img/icons_#{$theme-name}_theme/icon_shield.svg"); +} + +.gicon-snapshots { + background-image: url("../img/icons_#{$theme-name}_theme/icon_snapshots.svg"); +} + +.gicon-team { + background-image: url("../img/icons_#{$theme-name}_theme/icon_team.svg"); +} + +.gicon-user { + background-image: url("../img/icons_#{$theme-name}_theme/icon_user.svg"); +} + +.gicon-variable { + 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"); +} + +.sidemenu { + .gicon-dashboard { + background-image: url("../img/icons_dark_theme/icon_dashboard.svg"); + } + .gicon-alert { + background-image: url("../img/icons_dark_theme/icon_alert.svg"); + } + .gicon-cog { + background-image: url("../img/icons_dark_theme/icon_cog.svg"); + } + .gicon-question { + background-image: url("../img/icons_dark_theme/icon_question.svg"); + } +} diff --git a/public/sass/base/_normalize.scss b/public/sass/base/_normalize.scss index 93dd4521727..057f5c63602 100644 --- a/public/sass/base/_normalize.scss +++ b/public/sass/base/_normalize.scss @@ -291,8 +291,8 @@ select { // button, -html input[type="button"], // 1 -input[type="reset"], +html input[type="button"], +// 1 input[type="reset"], input[type="submit"] { -webkit-appearance: button; // 2 cursor: pointer; // 3 diff --git a/public/sass/base/_reboot.scss b/public/sass/base/_reboot.scss index 806ba2aa43d..e34bdfdb9ed 100644 --- a/public/sass/base/_reboot.scss +++ b/public/sass/base/_reboot.scss @@ -5,7 +5,6 @@ // Global resets to common HTML elements and more for easier usage by Bootstrap. // Adds additional rules on top of Normalize.css, including several overrides. - // Reset the box-sizing // // Change from `box-sizing: content-box` to `border-box` so that when you add @@ -29,7 +28,6 @@ html { box-sizing: inherit; } - // Make viewport responsive // // @viewport is needed because IE 10+ doesn't honor in @@ -46,10 +44,11 @@ html { // Wrap `@viewport` with `@at-root` for when folks do a nested import (e.g., // `.class-name { @import "bootstrap"; }`). @at-root { - @-ms-viewport { width: device-width; } + @-ms-viewport { + width: device-width; + } } - // // Reset HTML, body, and more // @@ -65,7 +64,7 @@ html { // See https://github.com/twbs/bootstrap/issues/18543 -ms-overflow-style: scrollbar; // Changes the default tap highlight to be completely transparent in iOS. - -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); height: 100%; } @@ -79,6 +78,8 @@ body { // By default, `` has no `background-color` so we set one as a best practice. background-color: $body-bg; height: 100%; + width: 100%; + position: absolute; } // Suppress the focus outline on elements that cannot be accessed via keyboard. @@ -90,7 +91,6 @@ body { outline: none !important; } - // // Typography // @@ -99,9 +99,14 @@ body { // // By default, `

    `-`

    ` all receive top and bottom margins. We nuke the top // margin for easier control within type scales as it avoids margin collapsing. -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { margin-top: 0; - margin-bottom: .5rem; + margin-bottom: 0.5rem; } // Reset margins on paragraphs @@ -114,9 +119,7 @@ p { } // Abbreviations and acronyms -abbr[title], -// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 -abbr[data-original-title] { +abbr[title] { cursor: help; border-bottom: 1px dotted $abbr-border-color; } @@ -146,7 +149,7 @@ dt { } dd { - margin-bottom: .5rem; + margin-bottom: 0.5rem; margin-left: 0; // Undo browser default } @@ -154,7 +157,6 @@ blockquote { margin: 0 0 1rem; } - // // Links // @@ -173,7 +175,6 @@ a { } } - // // Code // @@ -185,7 +186,6 @@ pre { margin-bottom: 1rem; } - // // Figures // @@ -196,7 +196,6 @@ figure { margin: 0 0 1rem; } - // // Images // @@ -209,7 +208,6 @@ img { // For the rationale behind this, see the comments on the `.img-fluid` class. } - // iOS "clickable elements" fix for role="button" // // Fixes "clickability" issue (and more generally, the firing of events such as focus as well) @@ -220,7 +218,6 @@ img { cursor: pointer; } - // Avoid 300ms click delay on touch devices that support the `touch-action` CSS property. // // In particular, unlike most other browsers, IE11+Edge on Windows 10 on touch devices and IE Mobile 10-11 @@ -243,7 +240,6 @@ textarea { touch-action: manipulation; } - // // Tables // @@ -266,7 +262,6 @@ th { text-align: left; } - // // Forms // @@ -319,10 +314,10 @@ legend { display: block; width: 100%; padding: 0; - margin-bottom: .5rem; + margin-bottom: 0.5rem; font-size: 1.5rem; line-height: inherit; -// border: 0; + // border: 0; } input[type="search"] { @@ -336,9 +331,9 @@ input[type="search"] { // todo: needed? output { display: inline-block; -// font-size: $font-size-base; -// line-height: $line-height; -// color: $input-color; + // font-size: $font-size-base; + // line-height: $line-height; + // color: $input-color; } // Always hide an element with the `hidden` HTML attribute (from PureCSS). diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index b2b43257654..aeddbc5ffb7 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -2,7 +2,6 @@ // Typography // -------------------------------------------------- - // Body text // ------------------------- @@ -21,48 +20,103 @@ p { // ------------------------- // Ex: 14px base font * 85% = about 12px -small { font-size: 85%; } -strong { font-weight: bold; } -em { font-style: italic; color: $headings-color; } -cite { font-style: normal; } +small { + font-size: 85%; +} +strong { + font-weight: bold; +} +em { + font-style: italic; + color: $headings-color; +} +cite { + font-style: normal; +} // Utility classes -.muted { color: $text-muted; } +.muted { + color: $text-muted; +} a.muted:hover, -a.muted:focus { color: darken($text-muted, 10%); } +a.muted:focus { + color: darken($text-muted, 10%); +} -.text-warning { color: $warning-text-color; } +.text-warning { + color: $warning-text-color; +} a.text-warning:hover, -a.text-warning:focus { color: darken($warning-text-color, 10%); } +a.text-warning:focus { + color: darken($warning-text-color, 10%); +} -.text-error { color: $error-text-color; } +.text-error { + color: $error-text-color; +} a.text-error:hover, -a.text-error:focus { color: darken($error-text-color, 10%); } +a.text-error:focus { + color: darken($error-text-color, 10%); +} -.text-info { color: $info-text-color; } +.text-info { + color: $info-text-color; +} a.text-info:hover, -a.text-info:focus { color: darken($info-text-color, 10%); } +a.text-info:focus { + color: darken($info-text-color, 10%); +} -.text-success { color: $success-text-color; } +.text-success { + color: $success-text-color; +} a.text-success:hover, -a.text-success:focus { color: darken($success-text-color, 10%); } -a { cursor: pointer; } +a.text-success:focus { + color: darken($success-text-color, 10%); +} +a { + cursor: pointer; +} + +.text-link { + text-decoration: underline; +} + +a:focus { + outline: 0 none !important; +} a[disabled] { cursor: default; pointer-events: none !important; } -.text-left { text-align: left; } -.text-right { text-align: right; } -.text-center { text-align: center; } +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} // // Headings // -h1, h2, h3, h4, h5, h6, -.h1, .h2, .h3, .h4, .h5, .h6 { +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { margin-bottom: $headings-margin-bottom; font-family: $headings-font-family; font-weight: $headings-font-weight; @@ -70,12 +124,30 @@ h1, h2, h3, h4, h5, h6, color: $headings-color; } -h1, .h1 { font-size: $font-size-h1; font-style: italic; } -h2, .h2 { font-size: $font-size-h2; } -h3, .h3 { font-size: $font-size-h3; } -h4, .h4 { font-size: $font-size-h4; } -h5, .h5 { font-size: $font-size-h5; } -h6, .h6 { font-size: $font-size-h6; } +h1, +.h1 { + font-size: $font-size-h1; +} +h2, +.h2 { + font-size: $font-size-h2; +} +h3, +.h3 { + font-size: $font-size-h3; +} +h4, +.h4 { + font-size: $font-size-h4; +} +h5, +.h5 { + font-size: $font-size-h5; +} +h6, +.h6 { + font-size: $font-size-h6; +} .lead { font-size: $lead-font-size; @@ -100,7 +172,6 @@ h6, .h6 { font-size: $font-size-h6; } font-weight: $display4-weight; } - // // Horizontal rules // @@ -112,7 +183,6 @@ hr { border-top: $hr-border-width solid $hr-border-color; } - // // Emphasis // @@ -129,16 +199,16 @@ small, mark, .mark { - padding: .2em; + padding: 0.2em; background: $alert-warning-bg; } - // Lists // -------------------------------------------------- // Unordered and Ordered lists -ul, ol { +ul, +ol { padding: 0; } ul ul, @@ -203,12 +273,11 @@ dd { // ---- // Abbreviations and acronyms -abbr[title], -// Added data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 -abbr[data-original-title] { +abbr[title] { cursor: help; border-bottom: 1px dotted $gray-2; } + abbr.initialism { font-size: 90%; text-transform: uppercase; @@ -230,7 +299,7 @@ blockquote { line-height: $line-height-base; color: $gray-2; &:before { - content: '\2014 \00A0'; + content: "\2014 \00A0"; } } @@ -247,10 +316,10 @@ blockquote { } small { &:before { - content: ''; + content: ""; } &:after { - content: '\00A0 \2014'; + content: "\00A0 \2014"; } } } @@ -273,11 +342,10 @@ address { } a.external-link { - color: $blue; + color: $external-link-color; text-decoration: underline; } - .link { color: $link-color; cursor: pointer; @@ -296,14 +364,16 @@ a.external-link { max-width: 100%; } - ul, ol { + ul, + ol { padding-left: $spacer*1.5; margin-bottom: $spacer; } table { - td, th { - padding: $spacer*.5 $spacer; + td, + th { + padding: $spacer*0.5 $spacer; } th { font-weight: normal; @@ -311,7 +381,9 @@ a.external-link { } } - table, th, td { + table, + th, + td { border: 1px solid $table-border; border-collapse: collapse; } @@ -328,8 +400,12 @@ a.external-link { margin-bottom: 0; } - ul:last-child, ol:last-child { + ul:last-child, + ol:last-child { margin-bottom: 0; } } +.no-wrap { + white-space: nowrap; +} diff --git a/public/sass/base/font-awesome/_animated.scss b/public/sass/base/font-awesome/_animated.scss index 8a020dbfff7..230df6b9ea8 100644 --- a/public/sass/base/font-awesome/_animated.scss +++ b/public/sass/base/font-awesome/_animated.scss @@ -3,32 +3,32 @@ .#{$fa-css-prefix}-spin { -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; } .#{$fa-css-prefix}-pulse { -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); } @-webkit-keyframes fa-spin { 0% { -webkit-transform: rotate(0deg); - transform: rotate(0deg); + transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); - transform: rotate(359deg); + transform: rotate(359deg); } } @keyframes fa-spin { 0% { -webkit-transform: rotate(0deg); - transform: rotate(0deg); + transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); - transform: rotate(359deg); + transform: rotate(359deg); } } diff --git a/public/sass/base/font-awesome/_bordered-pulled.scss b/public/sass/base/font-awesome/_bordered-pulled.scss index d4b85a02f24..c9b6b38ea6f 100644 --- a/public/sass/base/font-awesome/_bordered-pulled.scss +++ b/public/sass/base/font-awesome/_bordered-pulled.scss @@ -2,24 +2,40 @@ // ------------------------- .#{$fa-css-prefix}-border { - padding: .2em .25em .15em; - border: solid .08em $fa-border-color; - border-radius: .1em; + padding: 0.2em 0.25em 0.15em; + border: solid 0.08em $fa-border-color; + border-radius: 0.1em; } -.#{$fa-css-prefix}-pull-left { float: left; } -.#{$fa-css-prefix}-pull-right { float: right; } +.#{$fa-css-prefix}-pull-left { + float: left; +} +.#{$fa-css-prefix}-pull-right { + float: right; +} .#{$fa-css-prefix} { - &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } - &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } + &.#{$fa-css-prefix}-pull-left { + margin-right: 0.3em; + } + &.#{$fa-css-prefix}-pull-right { + margin-left: 0.3em; + } } /* Deprecated as of 4.4.0 */ -.pull-right { float: right; } -.pull-left { float: left; } +.pull-right { + float: right; +} +.pull-left { + float: left; +} .#{$fa-css-prefix} { - &.pull-left { margin-right: .3em; } - &.pull-right { margin-left: .3em; } + &.pull-left { + margin-right: 0.3em; + } + &.pull-right { + margin-left: 0.3em; + } } diff --git a/public/sass/base/font-awesome/_core.scss b/public/sass/base/font-awesome/_core.scss index 7425ef85fc8..9940b63463d 100644 --- a/public/sass/base/font-awesome/_core.scss +++ b/public/sass/base/font-awesome/_core.scss @@ -3,10 +3,10 @@ .#{$fa-css-prefix} { display: inline-block; - font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration + font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} + FontAwesome; // shortening font declaration font-size: inherit; // can't have font-size inherit on line above, so need to override text-rendering: auto; // optimizelegibility throws things off #1094 -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - } diff --git a/public/sass/base/font-awesome/_icons.scss b/public/sass/base/font-awesome/_icons.scss index e63e702c4d9..6ee6171f359 100644 --- a/public/sass/base/font-awesome/_icons.scss +++ b/public/sass/base/font-awesome/_icons.scss @@ -1,789 +1,2139 @@ /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen readers do not read off random characters that represent icons */ -.#{$fa-css-prefix}-glass:before { content: $fa-var-glass; } -.#{$fa-css-prefix}-music:before { content: $fa-var-music; } -.#{$fa-css-prefix}-search:before { content: $fa-var-search; } -.#{$fa-css-prefix}-envelope-o:before { content: $fa-var-envelope-o; } -.#{$fa-css-prefix}-heart:before { content: $fa-var-heart; } -.#{$fa-css-prefix}-star:before { content: $fa-var-star; } -.#{$fa-css-prefix}-star-o:before { content: $fa-var-star-o; } -.#{$fa-css-prefix}-user:before { content: $fa-var-user; } -.#{$fa-css-prefix}-film:before { content: $fa-var-film; } -.#{$fa-css-prefix}-th-large:before { content: $fa-var-th-large; } -.#{$fa-css-prefix}-th:before { content: $fa-var-th; } -.#{$fa-css-prefix}-th-list:before { content: $fa-var-th-list; } -.#{$fa-css-prefix}-check:before { content: $fa-var-check; } +.#{$fa-css-prefix}-glass:before { + content: $fa-var-glass; +} +.#{$fa-css-prefix}-music:before { + content: $fa-var-music; +} +.#{$fa-css-prefix}-search:before { + content: $fa-var-search; +} +.#{$fa-css-prefix}-envelope-o:before { + content: $fa-var-envelope-o; +} +.#{$fa-css-prefix}-heart:before { + content: $fa-var-heart; +} +.#{$fa-css-prefix}-star:before { + content: $fa-var-star; +} +.#{$fa-css-prefix}-star-o:before { + content: $fa-var-star-o; +} +.#{$fa-css-prefix}-user:before { + content: $fa-var-user; +} +.#{$fa-css-prefix}-film:before { + content: $fa-var-film; +} +.#{$fa-css-prefix}-th-large:before { + content: $fa-var-th-large; +} +.#{$fa-css-prefix}-th:before { + content: $fa-var-th; +} +.#{$fa-css-prefix}-th-list:before { + content: $fa-var-th-list; +} +.#{$fa-css-prefix}-check:before { + content: $fa-var-check; +} .#{$fa-css-prefix}-remove:before, .#{$fa-css-prefix}-close:before, -.#{$fa-css-prefix}-times:before { content: $fa-var-times; } -.#{$fa-css-prefix}-search-plus:before { content: $fa-var-search-plus; } -.#{$fa-css-prefix}-search-minus:before { content: $fa-var-search-minus; } -.#{$fa-css-prefix}-power-off:before { content: $fa-var-power-off; } -.#{$fa-css-prefix}-signal:before { content: $fa-var-signal; } +.#{$fa-css-prefix}-times:before { + content: $fa-var-times; +} +.#{$fa-css-prefix}-search-plus:before { + content: $fa-var-search-plus; +} +.#{$fa-css-prefix}-search-minus:before { + content: $fa-var-search-minus; +} +.#{$fa-css-prefix}-power-off:before { + content: $fa-var-power-off; +} +.#{$fa-css-prefix}-signal:before { + content: $fa-var-signal; +} .#{$fa-css-prefix}-gear:before, -.#{$fa-css-prefix}-cog:before { content: $fa-var-cog; } -.#{$fa-css-prefix}-trash-o:before { content: $fa-var-trash-o; } -.#{$fa-css-prefix}-home:before { content: $fa-var-home; } -.#{$fa-css-prefix}-file-o:before { content: $fa-var-file-o; } -.#{$fa-css-prefix}-clock-o:before { content: $fa-var-clock-o; } -.#{$fa-css-prefix}-road:before { content: $fa-var-road; } -.#{$fa-css-prefix}-download:before { content: $fa-var-download; } -.#{$fa-css-prefix}-arrow-circle-o-down:before { content: $fa-var-arrow-circle-o-down; } -.#{$fa-css-prefix}-arrow-circle-o-up:before { content: $fa-var-arrow-circle-o-up; } -.#{$fa-css-prefix}-inbox:before { content: $fa-var-inbox; } -.#{$fa-css-prefix}-play-circle-o:before { content: $fa-var-play-circle-o; } +.#{$fa-css-prefix}-cog:before { + content: $fa-var-cog; +} +.#{$fa-css-prefix}-trash-o:before { + content: $fa-var-trash-o; +} +.#{$fa-css-prefix}-home:before { + content: $fa-var-home; +} +.#{$fa-css-prefix}-file-o:before { + content: $fa-var-file-o; +} +.#{$fa-css-prefix}-clock-o:before { + content: $fa-var-clock-o; +} +.#{$fa-css-prefix}-road:before { + content: $fa-var-road; +} +.#{$fa-css-prefix}-download:before { + content: $fa-var-download; +} +.#{$fa-css-prefix}-arrow-circle-o-down:before { + content: $fa-var-arrow-circle-o-down; +} +.#{$fa-css-prefix}-arrow-circle-o-up:before { + content: $fa-var-arrow-circle-o-up; +} +.#{$fa-css-prefix}-inbox:before { + content: $fa-var-inbox; +} +.#{$fa-css-prefix}-play-circle-o:before { + content: $fa-var-play-circle-o; +} .#{$fa-css-prefix}-rotate-right:before, -.#{$fa-css-prefix}-repeat:before { content: $fa-var-repeat; } -.#{$fa-css-prefix}-refresh:before { content: $fa-var-refresh; } -.#{$fa-css-prefix}-list-alt:before { content: $fa-var-list-alt; } -.#{$fa-css-prefix}-lock:before { content: $fa-var-lock; } -.#{$fa-css-prefix}-flag:before { content: $fa-var-flag; } -.#{$fa-css-prefix}-headphones:before { content: $fa-var-headphones; } -.#{$fa-css-prefix}-volume-off:before { content: $fa-var-volume-off; } -.#{$fa-css-prefix}-volume-down:before { content: $fa-var-volume-down; } -.#{$fa-css-prefix}-volume-up:before { content: $fa-var-volume-up; } -.#{$fa-css-prefix}-qrcode:before { content: $fa-var-qrcode; } -.#{$fa-css-prefix}-barcode:before { content: $fa-var-barcode; } -.#{$fa-css-prefix}-tag:before { content: $fa-var-tag; } -.#{$fa-css-prefix}-tags:before { content: $fa-var-tags; } -.#{$fa-css-prefix}-book:before { content: $fa-var-book; } -.#{$fa-css-prefix}-bookmark:before { content: $fa-var-bookmark; } -.#{$fa-css-prefix}-print:before { content: $fa-var-print; } -.#{$fa-css-prefix}-camera:before { content: $fa-var-camera; } -.#{$fa-css-prefix}-font:before { content: $fa-var-font; } -.#{$fa-css-prefix}-bold:before { content: $fa-var-bold; } -.#{$fa-css-prefix}-italic:before { content: $fa-var-italic; } -.#{$fa-css-prefix}-text-height:before { content: $fa-var-text-height; } -.#{$fa-css-prefix}-text-width:before { content: $fa-var-text-width; } -.#{$fa-css-prefix}-align-left:before { content: $fa-var-align-left; } -.#{$fa-css-prefix}-align-center:before { content: $fa-var-align-center; } -.#{$fa-css-prefix}-align-right:before { content: $fa-var-align-right; } -.#{$fa-css-prefix}-align-justify:before { content: $fa-var-align-justify; } -.#{$fa-css-prefix}-list:before { content: $fa-var-list; } +.#{$fa-css-prefix}-repeat:before { + content: $fa-var-repeat; +} +.#{$fa-css-prefix}-refresh:before { + content: $fa-var-refresh; +} +.#{$fa-css-prefix}-list-alt:before { + content: $fa-var-list-alt; +} +.#{$fa-css-prefix}-lock:before { + content: $fa-var-lock; +} +.#{$fa-css-prefix}-flag:before { + content: $fa-var-flag; +} +.#{$fa-css-prefix}-headphones:before { + content: $fa-var-headphones; +} +.#{$fa-css-prefix}-volume-off:before { + content: $fa-var-volume-off; +} +.#{$fa-css-prefix}-volume-down:before { + content: $fa-var-volume-down; +} +.#{$fa-css-prefix}-volume-up:before { + content: $fa-var-volume-up; +} +.#{$fa-css-prefix}-qrcode:before { + content: $fa-var-qrcode; +} +.#{$fa-css-prefix}-barcode:before { + content: $fa-var-barcode; +} +.#{$fa-css-prefix}-tag:before { + content: $fa-var-tag; +} +.#{$fa-css-prefix}-tags:before { + content: $fa-var-tags; +} +.#{$fa-css-prefix}-book:before { + content: $fa-var-book; +} +.#{$fa-css-prefix}-bookmark:before { + content: $fa-var-bookmark; +} +.#{$fa-css-prefix}-print:before { + content: $fa-var-print; +} +.#{$fa-css-prefix}-camera:before { + content: $fa-var-camera; +} +.#{$fa-css-prefix}-font:before { + content: $fa-var-font; +} +.#{$fa-css-prefix}-bold:before { + content: $fa-var-bold; +} +.#{$fa-css-prefix}-italic:before { + content: $fa-var-italic; +} +.#{$fa-css-prefix}-text-height:before { + content: $fa-var-text-height; +} +.#{$fa-css-prefix}-text-width:before { + content: $fa-var-text-width; +} +.#{$fa-css-prefix}-align-left:before { + content: $fa-var-align-left; +} +.#{$fa-css-prefix}-align-center:before { + content: $fa-var-align-center; +} +.#{$fa-css-prefix}-align-right:before { + content: $fa-var-align-right; +} +.#{$fa-css-prefix}-align-justify:before { + content: $fa-var-align-justify; +} +.#{$fa-css-prefix}-list:before { + content: $fa-var-list; +} .#{$fa-css-prefix}-dedent:before, -.#{$fa-css-prefix}-outdent:before { content: $fa-var-outdent; } -.#{$fa-css-prefix}-indent:before { content: $fa-var-indent; } -.#{$fa-css-prefix}-video-camera:before { content: $fa-var-video-camera; } +.#{$fa-css-prefix}-outdent:before { + content: $fa-var-outdent; +} +.#{$fa-css-prefix}-indent:before { + content: $fa-var-indent; +} +.#{$fa-css-prefix}-video-camera:before { + content: $fa-var-video-camera; +} .#{$fa-css-prefix}-photo:before, .#{$fa-css-prefix}-image:before, -.#{$fa-css-prefix}-picture-o:before { content: $fa-var-picture-o; } -.#{$fa-css-prefix}-pencil:before { content: $fa-var-pencil; } -.#{$fa-css-prefix}-map-marker:before { content: $fa-var-map-marker; } -.#{$fa-css-prefix}-adjust:before { content: $fa-var-adjust; } -.#{$fa-css-prefix}-tint:before { content: $fa-var-tint; } +.#{$fa-css-prefix}-picture-o:before { + content: $fa-var-picture-o; +} +.#{$fa-css-prefix}-pencil:before { + content: $fa-var-pencil; +} +.#{$fa-css-prefix}-map-marker:before { + content: $fa-var-map-marker; +} +.#{$fa-css-prefix}-adjust:before { + content: $fa-var-adjust; +} +.#{$fa-css-prefix}-tint:before { + content: $fa-var-tint; +} .#{$fa-css-prefix}-edit:before, -.#{$fa-css-prefix}-pencil-square-o:before { content: $fa-var-pencil-square-o; } -.#{$fa-css-prefix}-share-square-o:before { content: $fa-var-share-square-o; } -.#{$fa-css-prefix}-check-square-o:before { content: $fa-var-check-square-o; } -.#{$fa-css-prefix}-arrows:before { content: $fa-var-arrows; } -.#{$fa-css-prefix}-step-backward:before { content: $fa-var-step-backward; } -.#{$fa-css-prefix}-fast-backward:before { content: $fa-var-fast-backward; } -.#{$fa-css-prefix}-backward:before { content: $fa-var-backward; } -.#{$fa-css-prefix}-play:before { content: $fa-var-play; } -.#{$fa-css-prefix}-pause:before { content: $fa-var-pause; } -.#{$fa-css-prefix}-stop:before { content: $fa-var-stop; } -.#{$fa-css-prefix}-forward:before { content: $fa-var-forward; } -.#{$fa-css-prefix}-fast-forward:before { content: $fa-var-fast-forward; } -.#{$fa-css-prefix}-step-forward:before { content: $fa-var-step-forward; } -.#{$fa-css-prefix}-eject:before { content: $fa-var-eject; } -.#{$fa-css-prefix}-chevron-left:before { content: $fa-var-chevron-left; } -.#{$fa-css-prefix}-chevron-right:before { content: $fa-var-chevron-right; } -.#{$fa-css-prefix}-plus-circle:before { content: $fa-var-plus-circle; } -.#{$fa-css-prefix}-minus-circle:before { content: $fa-var-minus-circle; } -.#{$fa-css-prefix}-times-circle:before { content: $fa-var-times-circle; } -.#{$fa-css-prefix}-check-circle:before { content: $fa-var-check-circle; } -.#{$fa-css-prefix}-question-circle:before { content: $fa-var-question-circle; } -.#{$fa-css-prefix}-info-circle:before { content: $fa-var-info-circle; } -.#{$fa-css-prefix}-crosshairs:before { content: $fa-var-crosshairs; } -.#{$fa-css-prefix}-times-circle-o:before { content: $fa-var-times-circle-o; } -.#{$fa-css-prefix}-check-circle-o:before { content: $fa-var-check-circle-o; } -.#{$fa-css-prefix}-ban:before { content: $fa-var-ban; } -.#{$fa-css-prefix}-arrow-left:before { content: $fa-var-arrow-left; } -.#{$fa-css-prefix}-arrow-right:before { content: $fa-var-arrow-right; } -.#{$fa-css-prefix}-arrow-up:before { content: $fa-var-arrow-up; } -.#{$fa-css-prefix}-arrow-down:before { content: $fa-var-arrow-down; } +.#{$fa-css-prefix}-pencil-square-o:before { + content: $fa-var-pencil-square-o; +} +.#{$fa-css-prefix}-share-square-o:before { + content: $fa-var-share-square-o; +} +.#{$fa-css-prefix}-check-square-o:before { + content: $fa-var-check-square-o; +} +.#{$fa-css-prefix}-arrows:before { + content: $fa-var-arrows; +} +.#{$fa-css-prefix}-step-backward:before { + content: $fa-var-step-backward; +} +.#{$fa-css-prefix}-fast-backward:before { + content: $fa-var-fast-backward; +} +.#{$fa-css-prefix}-backward:before { + content: $fa-var-backward; +} +.#{$fa-css-prefix}-play:before { + content: $fa-var-play; +} +.#{$fa-css-prefix}-pause:before { + content: $fa-var-pause; +} +.#{$fa-css-prefix}-stop:before { + content: $fa-var-stop; +} +.#{$fa-css-prefix}-forward:before { + content: $fa-var-forward; +} +.#{$fa-css-prefix}-fast-forward:before { + content: $fa-var-fast-forward; +} +.#{$fa-css-prefix}-step-forward:before { + content: $fa-var-step-forward; +} +.#{$fa-css-prefix}-eject:before { + content: $fa-var-eject; +} +.#{$fa-css-prefix}-chevron-left:before { + content: $fa-var-chevron-left; +} +.#{$fa-css-prefix}-chevron-right:before { + content: $fa-var-chevron-right; +} +.#{$fa-css-prefix}-plus-circle:before { + content: $fa-var-plus-circle; +} +.#{$fa-css-prefix}-minus-circle:before { + content: $fa-var-minus-circle; +} +.#{$fa-css-prefix}-times-circle:before { + content: $fa-var-times-circle; +} +.#{$fa-css-prefix}-check-circle:before { + content: $fa-var-check-circle; +} +.#{$fa-css-prefix}-question-circle:before { + content: $fa-var-question-circle; +} +.#{$fa-css-prefix}-info-circle:before { + content: $fa-var-info-circle; +} +.#{$fa-css-prefix}-crosshairs:before { + content: $fa-var-crosshairs; +} +.#{$fa-css-prefix}-times-circle-o:before { + content: $fa-var-times-circle-o; +} +.#{$fa-css-prefix}-check-circle-o:before { + content: $fa-var-check-circle-o; +} +.#{$fa-css-prefix}-ban:before { + content: $fa-var-ban; +} +.#{$fa-css-prefix}-arrow-left:before { + content: $fa-var-arrow-left; +} +.#{$fa-css-prefix}-arrow-right:before { + content: $fa-var-arrow-right; +} +.#{$fa-css-prefix}-arrow-up:before { + content: $fa-var-arrow-up; +} +.#{$fa-css-prefix}-arrow-down:before { + content: $fa-var-arrow-down; +} .#{$fa-css-prefix}-mail-forward:before, -.#{$fa-css-prefix}-share:before { content: $fa-var-share; } -.#{$fa-css-prefix}-expand:before { content: $fa-var-expand; } -.#{$fa-css-prefix}-compress:before { content: $fa-var-compress; } -.#{$fa-css-prefix}-plus:before { content: $fa-var-plus; } -.#{$fa-css-prefix}-minus:before { content: $fa-var-minus; } -.#{$fa-css-prefix}-asterisk:before { content: $fa-var-asterisk; } -.#{$fa-css-prefix}-exclamation-circle:before { content: $fa-var-exclamation-circle; } -.#{$fa-css-prefix}-gift:before { content: $fa-var-gift; } -.#{$fa-css-prefix}-leaf:before { content: $fa-var-leaf; } -.#{$fa-css-prefix}-fire:before { content: $fa-var-fire; } -.#{$fa-css-prefix}-eye:before { content: $fa-var-eye; } -.#{$fa-css-prefix}-eye-slash:before { content: $fa-var-eye-slash; } +.#{$fa-css-prefix}-share:before { + content: $fa-var-share; +} +.#{$fa-css-prefix}-expand:before { + content: $fa-var-expand; +} +.#{$fa-css-prefix}-compress:before { + content: $fa-var-compress; +} +.#{$fa-css-prefix}-plus:before { + content: $fa-var-plus; +} +.#{$fa-css-prefix}-minus:before { + content: $fa-var-minus; +} +.#{$fa-css-prefix}-asterisk:before { + content: $fa-var-asterisk; +} +.#{$fa-css-prefix}-exclamation-circle:before { + content: $fa-var-exclamation-circle; +} +.#{$fa-css-prefix}-gift:before { + content: $fa-var-gift; +} +.#{$fa-css-prefix}-leaf:before { + content: $fa-var-leaf; +} +.#{$fa-css-prefix}-fire:before { + content: $fa-var-fire; +} +.#{$fa-css-prefix}-eye:before { + content: $fa-var-eye; +} +.#{$fa-css-prefix}-eye-slash:before { + content: $fa-var-eye-slash; +} .#{$fa-css-prefix}-warning:before, -.#{$fa-css-prefix}-exclamation-triangle:before { content: $fa-var-exclamation-triangle; } -.#{$fa-css-prefix}-plane:before { content: $fa-var-plane; } -.#{$fa-css-prefix}-calendar:before { content: $fa-var-calendar; } -.#{$fa-css-prefix}-random:before { content: $fa-var-random; } -.#{$fa-css-prefix}-comment:before { content: $fa-var-comment; } -.#{$fa-css-prefix}-magnet:before { content: $fa-var-magnet; } -.#{$fa-css-prefix}-chevron-up:before { content: $fa-var-chevron-up; } -.#{$fa-css-prefix}-chevron-down:before { content: $fa-var-chevron-down; } -.#{$fa-css-prefix}-retweet:before { content: $fa-var-retweet; } -.#{$fa-css-prefix}-shopping-cart:before { content: $fa-var-shopping-cart; } -.#{$fa-css-prefix}-folder:before { content: $fa-var-folder; } -.#{$fa-css-prefix}-folder-open:before { content: $fa-var-folder-open; } -.#{$fa-css-prefix}-arrows-v:before { content: $fa-var-arrows-v; } -.#{$fa-css-prefix}-arrows-h:before { content: $fa-var-arrows-h; } +.#{$fa-css-prefix}-exclamation-triangle:before { + content: $fa-var-exclamation-triangle; +} +.#{$fa-css-prefix}-plane:before { + content: $fa-var-plane; +} +.#{$fa-css-prefix}-calendar:before { + content: $fa-var-calendar; +} +.#{$fa-css-prefix}-random:before { + content: $fa-var-random; +} +.#{$fa-css-prefix}-comment:before { + content: $fa-var-comment; +} +.#{$fa-css-prefix}-magnet:before { + content: $fa-var-magnet; +} +.#{$fa-css-prefix}-chevron-up:before { + content: $fa-var-chevron-up; +} +.#{$fa-css-prefix}-chevron-down:before { + content: $fa-var-chevron-down; +} +.#{$fa-css-prefix}-retweet:before { + content: $fa-var-retweet; +} +.#{$fa-css-prefix}-shopping-cart:before { + content: $fa-var-shopping-cart; +} +.#{$fa-css-prefix}-folder:before { + content: $fa-var-folder; +} +.#{$fa-css-prefix}-folder-open:before { + content: $fa-var-folder-open; +} +.#{$fa-css-prefix}-arrows-v:before { + content: $fa-var-arrows-v; +} +.#{$fa-css-prefix}-arrows-h:before { + content: $fa-var-arrows-h; +} .#{$fa-css-prefix}-bar-chart-o:before, -.#{$fa-css-prefix}-bar-chart:before { content: $fa-var-bar-chart; } -.#{$fa-css-prefix}-twitter-square:before { content: $fa-var-twitter-square; } -.#{$fa-css-prefix}-facebook-square:before { content: $fa-var-facebook-square; } -.#{$fa-css-prefix}-camera-retro:before { content: $fa-var-camera-retro; } -.#{$fa-css-prefix}-key:before { content: $fa-var-key; } +.#{$fa-css-prefix}-bar-chart:before { + content: $fa-var-bar-chart; +} +.#{$fa-css-prefix}-twitter-square:before { + content: $fa-var-twitter-square; +} +.#{$fa-css-prefix}-facebook-square:before { + content: $fa-var-facebook-square; +} +.#{$fa-css-prefix}-camera-retro:before { + content: $fa-var-camera-retro; +} +.#{$fa-css-prefix}-key:before { + content: $fa-var-key; +} .#{$fa-css-prefix}-gears:before, -.#{$fa-css-prefix}-cogs:before { content: $fa-var-cogs; } -.#{$fa-css-prefix}-comments:before { content: $fa-var-comments; } -.#{$fa-css-prefix}-thumbs-o-up:before { content: $fa-var-thumbs-o-up; } -.#{$fa-css-prefix}-thumbs-o-down:before { content: $fa-var-thumbs-o-down; } -.#{$fa-css-prefix}-star-half:before { content: $fa-var-star-half; } -.#{$fa-css-prefix}-heart-o:before { content: $fa-var-heart-o; } -.#{$fa-css-prefix}-sign-out:before { content: $fa-var-sign-out; } -.#{$fa-css-prefix}-linkedin-square:before { content: $fa-var-linkedin-square; } -.#{$fa-css-prefix}-thumb-tack:before { content: $fa-var-thumb-tack; } -.#{$fa-css-prefix}-external-link:before { content: $fa-var-external-link; } -.#{$fa-css-prefix}-sign-in:before { content: $fa-var-sign-in; } -.#{$fa-css-prefix}-trophy:before { content: $fa-var-trophy; } -.#{$fa-css-prefix}-github-square:before { content: $fa-var-github-square; } -.#{$fa-css-prefix}-upload:before { content: $fa-var-upload; } -.#{$fa-css-prefix}-lemon-o:before { content: $fa-var-lemon-o; } -.#{$fa-css-prefix}-phone:before { content: $fa-var-phone; } -.#{$fa-css-prefix}-square-o:before { content: $fa-var-square-o; } -.#{$fa-css-prefix}-bookmark-o:before { content: $fa-var-bookmark-o; } -.#{$fa-css-prefix}-phone-square:before { content: $fa-var-phone-square; } -.#{$fa-css-prefix}-twitter:before { content: $fa-var-twitter; } +.#{$fa-css-prefix}-cogs:before { + content: $fa-var-cogs; +} +.#{$fa-css-prefix}-comments:before { + content: $fa-var-comments; +} +.#{$fa-css-prefix}-thumbs-o-up:before { + content: $fa-var-thumbs-o-up; +} +.#{$fa-css-prefix}-thumbs-o-down:before { + content: $fa-var-thumbs-o-down; +} +.#{$fa-css-prefix}-star-half:before { + content: $fa-var-star-half; +} +.#{$fa-css-prefix}-heart-o:before { + content: $fa-var-heart-o; +} +.#{$fa-css-prefix}-sign-out:before { + content: $fa-var-sign-out; +} +.#{$fa-css-prefix}-linkedin-square:before { + content: $fa-var-linkedin-square; +} +.#{$fa-css-prefix}-thumb-tack:before { + content: $fa-var-thumb-tack; +} +.#{$fa-css-prefix}-external-link:before { + content: $fa-var-external-link; +} +.#{$fa-css-prefix}-sign-in:before { + content: $fa-var-sign-in; +} +.#{$fa-css-prefix}-trophy:before { + content: $fa-var-trophy; +} +.#{$fa-css-prefix}-github-square:before { + content: $fa-var-github-square; +} +.#{$fa-css-prefix}-upload:before { + content: $fa-var-upload; +} +.#{$fa-css-prefix}-lemon-o:before { + content: $fa-var-lemon-o; +} +.#{$fa-css-prefix}-phone:before { + content: $fa-var-phone; +} +.#{$fa-css-prefix}-square-o:before { + content: $fa-var-square-o; +} +.#{$fa-css-prefix}-bookmark-o:before { + content: $fa-var-bookmark-o; +} +.#{$fa-css-prefix}-phone-square:before { + content: $fa-var-phone-square; +} +.#{$fa-css-prefix}-twitter:before { + content: $fa-var-twitter; +} .#{$fa-css-prefix}-facebook-f:before, -.#{$fa-css-prefix}-facebook:before { content: $fa-var-facebook; } -.#{$fa-css-prefix}-github:before { content: $fa-var-github; } -.#{$fa-css-prefix}-unlock:before { content: $fa-var-unlock; } -.#{$fa-css-prefix}-credit-card:before { content: $fa-var-credit-card; } +.#{$fa-css-prefix}-facebook:before { + content: $fa-var-facebook; +} +.#{$fa-css-prefix}-github:before { + content: $fa-var-github; +} +.#{$fa-css-prefix}-unlock:before { + content: $fa-var-unlock; +} +.#{$fa-css-prefix}-credit-card:before { + content: $fa-var-credit-card; +} .#{$fa-css-prefix}-feed:before, -.#{$fa-css-prefix}-rss:before { content: $fa-var-rss; } -.#{$fa-css-prefix}-hdd-o:before { content: $fa-var-hdd-o; } -.#{$fa-css-prefix}-bullhorn:before { content: $fa-var-bullhorn; } -.#{$fa-css-prefix}-bell:before { content: $fa-var-bell; } -.#{$fa-css-prefix}-certificate:before { content: $fa-var-certificate; } -.#{$fa-css-prefix}-hand-o-right:before { content: $fa-var-hand-o-right; } -.#{$fa-css-prefix}-hand-o-left:before { content: $fa-var-hand-o-left; } -.#{$fa-css-prefix}-hand-o-up:before { content: $fa-var-hand-o-up; } -.#{$fa-css-prefix}-hand-o-down:before { content: $fa-var-hand-o-down; } -.#{$fa-css-prefix}-arrow-circle-left:before { content: $fa-var-arrow-circle-left; } -.#{$fa-css-prefix}-arrow-circle-right:before { content: $fa-var-arrow-circle-right; } -.#{$fa-css-prefix}-arrow-circle-up:before { content: $fa-var-arrow-circle-up; } -.#{$fa-css-prefix}-arrow-circle-down:before { content: $fa-var-arrow-circle-down; } -.#{$fa-css-prefix}-globe:before { content: $fa-var-globe; } -.#{$fa-css-prefix}-wrench:before { content: $fa-var-wrench; } -.#{$fa-css-prefix}-tasks:before { content: $fa-var-tasks; } -.#{$fa-css-prefix}-filter:before { content: $fa-var-filter; } -.#{$fa-css-prefix}-briefcase:before { content: $fa-var-briefcase; } -.#{$fa-css-prefix}-arrows-alt:before { content: $fa-var-arrows-alt; } +.#{$fa-css-prefix}-rss:before { + content: $fa-var-rss; +} +.#{$fa-css-prefix}-hdd-o:before { + content: $fa-var-hdd-o; +} +.#{$fa-css-prefix}-bullhorn:before { + content: $fa-var-bullhorn; +} +.#{$fa-css-prefix}-bell:before { + content: $fa-var-bell; +} +.#{$fa-css-prefix}-certificate:before { + content: $fa-var-certificate; +} +.#{$fa-css-prefix}-hand-o-right:before { + content: $fa-var-hand-o-right; +} +.#{$fa-css-prefix}-hand-o-left:before { + content: $fa-var-hand-o-left; +} +.#{$fa-css-prefix}-hand-o-up:before { + content: $fa-var-hand-o-up; +} +.#{$fa-css-prefix}-hand-o-down:before { + content: $fa-var-hand-o-down; +} +.#{$fa-css-prefix}-arrow-circle-left:before { + content: $fa-var-arrow-circle-left; +} +.#{$fa-css-prefix}-arrow-circle-right:before { + content: $fa-var-arrow-circle-right; +} +.#{$fa-css-prefix}-arrow-circle-up:before { + content: $fa-var-arrow-circle-up; +} +.#{$fa-css-prefix}-arrow-circle-down:before { + content: $fa-var-arrow-circle-down; +} +.#{$fa-css-prefix}-globe:before { + content: $fa-var-globe; +} +.#{$fa-css-prefix}-wrench:before { + content: $fa-var-wrench; +} +.#{$fa-css-prefix}-tasks:before { + content: $fa-var-tasks; +} +.#{$fa-css-prefix}-filter:before { + content: $fa-var-filter; +} +.#{$fa-css-prefix}-briefcase:before { + content: $fa-var-briefcase; +} +.#{$fa-css-prefix}-arrows-alt:before { + content: $fa-var-arrows-alt; +} .#{$fa-css-prefix}-group:before, -.#{$fa-css-prefix}-users:before { content: $fa-var-users; } +.#{$fa-css-prefix}-users:before { + content: $fa-var-users; +} .#{$fa-css-prefix}-chain:before, -.#{$fa-css-prefix}-link:before { content: $fa-var-link; } -.#{$fa-css-prefix}-cloud:before { content: $fa-var-cloud; } -.#{$fa-css-prefix}-flask:before { content: $fa-var-flask; } +.#{$fa-css-prefix}-link:before { + content: $fa-var-link; +} +.#{$fa-css-prefix}-cloud:before { + content: $fa-var-cloud; +} +.#{$fa-css-prefix}-flask:before { + content: $fa-var-flask; +} .#{$fa-css-prefix}-cut:before, -.#{$fa-css-prefix}-scissors:before { content: $fa-var-scissors; } +.#{$fa-css-prefix}-scissors:before { + content: $fa-var-scissors; +} .#{$fa-css-prefix}-copy:before, -.#{$fa-css-prefix}-files-o:before { content: $fa-var-files-o; } -.#{$fa-css-prefix}-paperclip:before { content: $fa-var-paperclip; } +.#{$fa-css-prefix}-files-o:before { + content: $fa-var-files-o; +} +.#{$fa-css-prefix}-paperclip:before { + content: $fa-var-paperclip; +} .#{$fa-css-prefix}-save:before, -.#{$fa-css-prefix}-floppy-o:before { content: $fa-var-floppy-o; } -.#{$fa-css-prefix}-square:before { content: $fa-var-square; } +.#{$fa-css-prefix}-floppy-o:before { + content: $fa-var-floppy-o; +} +.#{$fa-css-prefix}-square:before { + content: $fa-var-square; +} .#{$fa-css-prefix}-navicon:before, .#{$fa-css-prefix}-reorder:before, -.#{$fa-css-prefix}-bars:before { content: $fa-var-bars; } -.#{$fa-css-prefix}-list-ul:before { content: $fa-var-list-ul; } -.#{$fa-css-prefix}-list-ol:before { content: $fa-var-list-ol; } -.#{$fa-css-prefix}-strikethrough:before { content: $fa-var-strikethrough; } -.#{$fa-css-prefix}-underline:before { content: $fa-var-underline; } -.#{$fa-css-prefix}-table:before { content: $fa-var-table; } -.#{$fa-css-prefix}-magic:before { content: $fa-var-magic; } -.#{$fa-css-prefix}-truck:before { content: $fa-var-truck; } -.#{$fa-css-prefix}-pinterest:before { content: $fa-var-pinterest; } -.#{$fa-css-prefix}-pinterest-square:before { content: $fa-var-pinterest-square; } -.#{$fa-css-prefix}-google-plus-square:before { content: $fa-var-google-plus-square; } -.#{$fa-css-prefix}-google-plus:before { content: $fa-var-google-plus; } -.#{$fa-css-prefix}-money:before { content: $fa-var-money; } -.#{$fa-css-prefix}-caret-down:before { content: $fa-var-caret-down; } -.#{$fa-css-prefix}-caret-up:before { content: $fa-var-caret-up; } -.#{$fa-css-prefix}-caret-left:before { content: $fa-var-caret-left; } -.#{$fa-css-prefix}-caret-right:before { content: $fa-var-caret-right; } -.#{$fa-css-prefix}-columns:before { content: $fa-var-columns; } +.#{$fa-css-prefix}-bars:before { + content: $fa-var-bars; +} +.#{$fa-css-prefix}-list-ul:before { + content: $fa-var-list-ul; +} +.#{$fa-css-prefix}-list-ol:before { + content: $fa-var-list-ol; +} +.#{$fa-css-prefix}-strikethrough:before { + content: $fa-var-strikethrough; +} +.#{$fa-css-prefix}-underline:before { + content: $fa-var-underline; +} +.#{$fa-css-prefix}-table:before { + content: $fa-var-table; +} +.#{$fa-css-prefix}-magic:before { + content: $fa-var-magic; +} +.#{$fa-css-prefix}-truck:before { + content: $fa-var-truck; +} +.#{$fa-css-prefix}-pinterest:before { + content: $fa-var-pinterest; +} +.#{$fa-css-prefix}-pinterest-square:before { + content: $fa-var-pinterest-square; +} +.#{$fa-css-prefix}-google-plus-square:before { + content: $fa-var-google-plus-square; +} +.#{$fa-css-prefix}-google-plus:before { + content: $fa-var-google-plus; +} +.#{$fa-css-prefix}-money:before { + content: $fa-var-money; +} +.#{$fa-css-prefix}-caret-down:before { + content: $fa-var-caret-down; +} +.#{$fa-css-prefix}-caret-up:before { + content: $fa-var-caret-up; +} +.#{$fa-css-prefix}-caret-left:before { + content: $fa-var-caret-left; +} +.#{$fa-css-prefix}-caret-right:before { + content: $fa-var-caret-right; +} +.#{$fa-css-prefix}-columns:before { + content: $fa-var-columns; +} .#{$fa-css-prefix}-unsorted:before, -.#{$fa-css-prefix}-sort:before { content: $fa-var-sort; } +.#{$fa-css-prefix}-sort:before { + content: $fa-var-sort; +} .#{$fa-css-prefix}-sort-down:before, -.#{$fa-css-prefix}-sort-desc:before { content: $fa-var-sort-desc; } +.#{$fa-css-prefix}-sort-desc:before { + content: $fa-var-sort-desc; +} .#{$fa-css-prefix}-sort-up:before, -.#{$fa-css-prefix}-sort-asc:before { content: $fa-var-sort-asc; } -.#{$fa-css-prefix}-envelope:before { content: $fa-var-envelope; } -.#{$fa-css-prefix}-linkedin:before { content: $fa-var-linkedin; } +.#{$fa-css-prefix}-sort-asc:before { + content: $fa-var-sort-asc; +} +.#{$fa-css-prefix}-envelope:before { + content: $fa-var-envelope; +} +.#{$fa-css-prefix}-linkedin:before { + content: $fa-var-linkedin; +} .#{$fa-css-prefix}-rotate-left:before, -.#{$fa-css-prefix}-undo:before { content: $fa-var-undo; } +.#{$fa-css-prefix}-undo:before { + content: $fa-var-undo; +} .#{$fa-css-prefix}-legal:before, -.#{$fa-css-prefix}-gavel:before { content: $fa-var-gavel; } +.#{$fa-css-prefix}-gavel:before { + content: $fa-var-gavel; +} .#{$fa-css-prefix}-dashboard:before, -.#{$fa-css-prefix}-tachometer:before { content: $fa-var-tachometer; } -.#{$fa-css-prefix}-comment-o:before { content: $fa-var-comment-o; } -.#{$fa-css-prefix}-comments-o:before { content: $fa-var-comments-o; } +.#{$fa-css-prefix}-tachometer:before { + content: $fa-var-tachometer; +} +.#{$fa-css-prefix}-comment-o:before { + content: $fa-var-comment-o; +} +.#{$fa-css-prefix}-comments-o:before { + content: $fa-var-comments-o; +} .#{$fa-css-prefix}-flash:before, -.#{$fa-css-prefix}-bolt:before { content: $fa-var-bolt; } -.#{$fa-css-prefix}-sitemap:before { content: $fa-var-sitemap; } -.#{$fa-css-prefix}-umbrella:before { content: $fa-var-umbrella; } +.#{$fa-css-prefix}-bolt:before { + content: $fa-var-bolt; +} +.#{$fa-css-prefix}-sitemap:before { + content: $fa-var-sitemap; +} +.#{$fa-css-prefix}-umbrella:before { + content: $fa-var-umbrella; +} .#{$fa-css-prefix}-paste:before, -.#{$fa-css-prefix}-clipboard:before { content: $fa-var-clipboard; } -.#{$fa-css-prefix}-lightbulb-o:before { content: $fa-var-lightbulb-o; } -.#{$fa-css-prefix}-exchange:before { content: $fa-var-exchange; } -.#{$fa-css-prefix}-cloud-download:before { content: $fa-var-cloud-download; } -.#{$fa-css-prefix}-cloud-upload:before { content: $fa-var-cloud-upload; } -.#{$fa-css-prefix}-user-md:before { content: $fa-var-user-md; } -.#{$fa-css-prefix}-stethoscope:before { content: $fa-var-stethoscope; } -.#{$fa-css-prefix}-suitcase:before { content: $fa-var-suitcase; } -.#{$fa-css-prefix}-bell-o:before { content: $fa-var-bell-o; } -.#{$fa-css-prefix}-coffee:before { content: $fa-var-coffee; } -.#{$fa-css-prefix}-cutlery:before { content: $fa-var-cutlery; } -.#{$fa-css-prefix}-file-text-o:before { content: $fa-var-file-text-o; } -.#{$fa-css-prefix}-building-o:before { content: $fa-var-building-o; } -.#{$fa-css-prefix}-hospital-o:before { content: $fa-var-hospital-o; } -.#{$fa-css-prefix}-ambulance:before { content: $fa-var-ambulance; } -.#{$fa-css-prefix}-medkit:before { content: $fa-var-medkit; } -.#{$fa-css-prefix}-fighter-jet:before { content: $fa-var-fighter-jet; } -.#{$fa-css-prefix}-beer:before { content: $fa-var-beer; } -.#{$fa-css-prefix}-h-square:before { content: $fa-var-h-square; } -.#{$fa-css-prefix}-plus-square:before { content: $fa-var-plus-square; } -.#{$fa-css-prefix}-angle-double-left:before { content: $fa-var-angle-double-left; } -.#{$fa-css-prefix}-angle-double-right:before { content: $fa-var-angle-double-right; } -.#{$fa-css-prefix}-angle-double-up:before { content: $fa-var-angle-double-up; } -.#{$fa-css-prefix}-angle-double-down:before { content: $fa-var-angle-double-down; } -.#{$fa-css-prefix}-angle-left:before { content: $fa-var-angle-left; } -.#{$fa-css-prefix}-angle-right:before { content: $fa-var-angle-right; } -.#{$fa-css-prefix}-angle-up:before { content: $fa-var-angle-up; } -.#{$fa-css-prefix}-angle-down:before { content: $fa-var-angle-down; } -.#{$fa-css-prefix}-desktop:before { content: $fa-var-desktop; } -.#{$fa-css-prefix}-laptop:before { content: $fa-var-laptop; } -.#{$fa-css-prefix}-tablet:before { content: $fa-var-tablet; } +.#{$fa-css-prefix}-clipboard:before { + content: $fa-var-clipboard; +} +.#{$fa-css-prefix}-lightbulb-o:before { + content: $fa-var-lightbulb-o; +} +.#{$fa-css-prefix}-exchange:before { + content: $fa-var-exchange; +} +.#{$fa-css-prefix}-cloud-download:before { + content: $fa-var-cloud-download; +} +.#{$fa-css-prefix}-cloud-upload:before { + content: $fa-var-cloud-upload; +} +.#{$fa-css-prefix}-user-md:before { + content: $fa-var-user-md; +} +.#{$fa-css-prefix}-stethoscope:before { + content: $fa-var-stethoscope; +} +.#{$fa-css-prefix}-suitcase:before { + content: $fa-var-suitcase; +} +.#{$fa-css-prefix}-bell-o:before { + content: $fa-var-bell-o; +} +.#{$fa-css-prefix}-coffee:before { + content: $fa-var-coffee; +} +.#{$fa-css-prefix}-cutlery:before { + content: $fa-var-cutlery; +} +.#{$fa-css-prefix}-file-text-o:before { + content: $fa-var-file-text-o; +} +.#{$fa-css-prefix}-building-o:before { + content: $fa-var-building-o; +} +.#{$fa-css-prefix}-hospital-o:before { + content: $fa-var-hospital-o; +} +.#{$fa-css-prefix}-ambulance:before { + content: $fa-var-ambulance; +} +.#{$fa-css-prefix}-medkit:before { + content: $fa-var-medkit; +} +.#{$fa-css-prefix}-fighter-jet:before { + content: $fa-var-fighter-jet; +} +.#{$fa-css-prefix}-beer:before { + content: $fa-var-beer; +} +.#{$fa-css-prefix}-h-square:before { + content: $fa-var-h-square; +} +.#{$fa-css-prefix}-plus-square:before { + content: $fa-var-plus-square; +} +.#{$fa-css-prefix}-angle-double-left:before { + content: $fa-var-angle-double-left; +} +.#{$fa-css-prefix}-angle-double-right:before { + content: $fa-var-angle-double-right; +} +.#{$fa-css-prefix}-angle-double-up:before { + content: $fa-var-angle-double-up; +} +.#{$fa-css-prefix}-angle-double-down:before { + content: $fa-var-angle-double-down; +} +.#{$fa-css-prefix}-angle-left:before { + content: $fa-var-angle-left; +} +.#{$fa-css-prefix}-angle-right:before { + content: $fa-var-angle-right; +} +.#{$fa-css-prefix}-angle-up:before { + content: $fa-var-angle-up; +} +.#{$fa-css-prefix}-angle-down:before { + content: $fa-var-angle-down; +} +.#{$fa-css-prefix}-desktop:before { + content: $fa-var-desktop; +} +.#{$fa-css-prefix}-laptop:before { + content: $fa-var-laptop; +} +.#{$fa-css-prefix}-tablet:before { + content: $fa-var-tablet; +} .#{$fa-css-prefix}-mobile-phone:before, -.#{$fa-css-prefix}-mobile:before { content: $fa-var-mobile; } -.#{$fa-css-prefix}-circle-o:before { content: $fa-var-circle-o; } -.#{$fa-css-prefix}-quote-left:before { content: $fa-var-quote-left; } -.#{$fa-css-prefix}-quote-right:before { content: $fa-var-quote-right; } -.#{$fa-css-prefix}-spinner:before { content: $fa-var-spinner; } -.#{$fa-css-prefix}-circle:before { content: $fa-var-circle; } +.#{$fa-css-prefix}-mobile:before { + content: $fa-var-mobile; +} +.#{$fa-css-prefix}-circle-o:before { + content: $fa-var-circle-o; +} +.#{$fa-css-prefix}-quote-left:before { + content: $fa-var-quote-left; +} +.#{$fa-css-prefix}-quote-right:before { + content: $fa-var-quote-right; +} +.#{$fa-css-prefix}-spinner:before { + content: $fa-var-spinner; +} +.#{$fa-css-prefix}-circle:before { + content: $fa-var-circle; +} .#{$fa-css-prefix}-mail-reply:before, -.#{$fa-css-prefix}-reply:before { content: $fa-var-reply; } -.#{$fa-css-prefix}-github-alt:before { content: $fa-var-github-alt; } -.#{$fa-css-prefix}-folder-o:before { content: $fa-var-folder-o; } -.#{$fa-css-prefix}-folder-open-o:before { content: $fa-var-folder-open-o; } -.#{$fa-css-prefix}-smile-o:before { content: $fa-var-smile-o; } -.#{$fa-css-prefix}-frown-o:before { content: $fa-var-frown-o; } -.#{$fa-css-prefix}-meh-o:before { content: $fa-var-meh-o; } -.#{$fa-css-prefix}-gamepad:before { content: $fa-var-gamepad; } -.#{$fa-css-prefix}-keyboard-o:before { content: $fa-var-keyboard-o; } -.#{$fa-css-prefix}-flag-o:before { content: $fa-var-flag-o; } -.#{$fa-css-prefix}-flag-checkered:before { content: $fa-var-flag-checkered; } -.#{$fa-css-prefix}-terminal:before { content: $fa-var-terminal; } -.#{$fa-css-prefix}-code:before { content: $fa-var-code; } +.#{$fa-css-prefix}-reply:before { + content: $fa-var-reply; +} +.#{$fa-css-prefix}-github-alt:before { + content: $fa-var-github-alt; +} +.#{$fa-css-prefix}-folder-o:before { + content: $fa-var-folder-o; +} +.#{$fa-css-prefix}-folder-open-o:before { + content: $fa-var-folder-open-o; +} +.#{$fa-css-prefix}-smile-o:before { + content: $fa-var-smile-o; +} +.#{$fa-css-prefix}-frown-o:before { + content: $fa-var-frown-o; +} +.#{$fa-css-prefix}-meh-o:before { + content: $fa-var-meh-o; +} +.#{$fa-css-prefix}-gamepad:before { + content: $fa-var-gamepad; +} +.#{$fa-css-prefix}-keyboard-o:before { + content: $fa-var-keyboard-o; +} +.#{$fa-css-prefix}-flag-o:before { + content: $fa-var-flag-o; +} +.#{$fa-css-prefix}-flag-checkered:before { + content: $fa-var-flag-checkered; +} +.#{$fa-css-prefix}-terminal:before { + content: $fa-var-terminal; +} +.#{$fa-css-prefix}-code:before { + content: $fa-var-code; +} .#{$fa-css-prefix}-mail-reply-all:before, -.#{$fa-css-prefix}-reply-all:before { content: $fa-var-reply-all; } +.#{$fa-css-prefix}-reply-all:before { + content: $fa-var-reply-all; +} .#{$fa-css-prefix}-star-half-empty:before, .#{$fa-css-prefix}-star-half-full:before, -.#{$fa-css-prefix}-star-half-o:before { content: $fa-var-star-half-o; } -.#{$fa-css-prefix}-location-arrow:before { content: $fa-var-location-arrow; } -.#{$fa-css-prefix}-crop:before { content: $fa-var-crop; } -.#{$fa-css-prefix}-code-fork:before { content: $fa-var-code-fork; } +.#{$fa-css-prefix}-star-half-o:before { + content: $fa-var-star-half-o; +} +.#{$fa-css-prefix}-location-arrow:before { + content: $fa-var-location-arrow; +} +.#{$fa-css-prefix}-crop:before { + content: $fa-var-crop; +} +.#{$fa-css-prefix}-code-fork:before { + content: $fa-var-code-fork; +} .#{$fa-css-prefix}-unlink:before, -.#{$fa-css-prefix}-chain-broken:before { content: $fa-var-chain-broken; } -.#{$fa-css-prefix}-question:before { content: $fa-var-question; } -.#{$fa-css-prefix}-info:before { content: $fa-var-info; } -.#{$fa-css-prefix}-exclamation:before { content: $fa-var-exclamation; } -.#{$fa-css-prefix}-superscript:before { content: $fa-var-superscript; } -.#{$fa-css-prefix}-subscript:before { content: $fa-var-subscript; } -.#{$fa-css-prefix}-eraser:before { content: $fa-var-eraser; } -.#{$fa-css-prefix}-puzzle-piece:before { content: $fa-var-puzzle-piece; } -.#{$fa-css-prefix}-microphone:before { content: $fa-var-microphone; } -.#{$fa-css-prefix}-microphone-slash:before { content: $fa-var-microphone-slash; } -.#{$fa-css-prefix}-shield:before { content: $fa-var-shield; } -.#{$fa-css-prefix}-calendar-o:before { content: $fa-var-calendar-o; } -.#{$fa-css-prefix}-fire-extinguisher:before { content: $fa-var-fire-extinguisher; } -.#{$fa-css-prefix}-rocket:before { content: $fa-var-rocket; } -.#{$fa-css-prefix}-maxcdn:before { content: $fa-var-maxcdn; } -.#{$fa-css-prefix}-chevron-circle-left:before { content: $fa-var-chevron-circle-left; } -.#{$fa-css-prefix}-chevron-circle-right:before { content: $fa-var-chevron-circle-right; } -.#{$fa-css-prefix}-chevron-circle-up:before { content: $fa-var-chevron-circle-up; } -.#{$fa-css-prefix}-chevron-circle-down:before { content: $fa-var-chevron-circle-down; } -.#{$fa-css-prefix}-html5:before { content: $fa-var-html5; } -.#{$fa-css-prefix}-css3:before { content: $fa-var-css3; } -.#{$fa-css-prefix}-anchor:before { content: $fa-var-anchor; } -.#{$fa-css-prefix}-unlock-alt:before { content: $fa-var-unlock-alt; } -.#{$fa-css-prefix}-bullseye:before { content: $fa-var-bullseye; } -.#{$fa-css-prefix}-ellipsis-h:before { content: $fa-var-ellipsis-h; } -.#{$fa-css-prefix}-ellipsis-v:before { content: $fa-var-ellipsis-v; } -.#{$fa-css-prefix}-rss-square:before { content: $fa-var-rss-square; } -.#{$fa-css-prefix}-play-circle:before { content: $fa-var-play-circle; } -.#{$fa-css-prefix}-ticket:before { content: $fa-var-ticket; } -.#{$fa-css-prefix}-minus-square:before { content: $fa-var-minus-square; } -.#{$fa-css-prefix}-minus-square-o:before { content: $fa-var-minus-square-o; } -.#{$fa-css-prefix}-level-up:before { content: $fa-var-level-up; } -.#{$fa-css-prefix}-level-down:before { content: $fa-var-level-down; } -.#{$fa-css-prefix}-check-square:before { content: $fa-var-check-square; } -.#{$fa-css-prefix}-pencil-square:before { content: $fa-var-pencil-square; } -.#{$fa-css-prefix}-external-link-square:before { content: $fa-var-external-link-square; } -.#{$fa-css-prefix}-share-square:before { content: $fa-var-share-square; } -.#{$fa-css-prefix}-compass:before { content: $fa-var-compass; } +.#{$fa-css-prefix}-chain-broken:before { + content: $fa-var-chain-broken; +} +.#{$fa-css-prefix}-question:before { + content: $fa-var-question; +} +.#{$fa-css-prefix}-info:before { + content: $fa-var-info; +} +.#{$fa-css-prefix}-exclamation:before { + content: $fa-var-exclamation; +} +.#{$fa-css-prefix}-superscript:before { + content: $fa-var-superscript; +} +.#{$fa-css-prefix}-subscript:before { + content: $fa-var-subscript; +} +.#{$fa-css-prefix}-eraser:before { + content: $fa-var-eraser; +} +.#{$fa-css-prefix}-puzzle-piece:before { + content: $fa-var-puzzle-piece; +} +.#{$fa-css-prefix}-microphone:before { + content: $fa-var-microphone; +} +.#{$fa-css-prefix}-microphone-slash:before { + content: $fa-var-microphone-slash; +} +.#{$fa-css-prefix}-shield:before { + content: $fa-var-shield; +} +.#{$fa-css-prefix}-calendar-o:before { + content: $fa-var-calendar-o; +} +.#{$fa-css-prefix}-fire-extinguisher:before { + content: $fa-var-fire-extinguisher; +} +.#{$fa-css-prefix}-rocket:before { + content: $fa-var-rocket; +} +.#{$fa-css-prefix}-maxcdn:before { + content: $fa-var-maxcdn; +} +.#{$fa-css-prefix}-chevron-circle-left:before { + content: $fa-var-chevron-circle-left; +} +.#{$fa-css-prefix}-chevron-circle-right:before { + content: $fa-var-chevron-circle-right; +} +.#{$fa-css-prefix}-chevron-circle-up:before { + content: $fa-var-chevron-circle-up; +} +.#{$fa-css-prefix}-chevron-circle-down:before { + content: $fa-var-chevron-circle-down; +} +.#{$fa-css-prefix}-html5:before { + content: $fa-var-html5; +} +.#{$fa-css-prefix}-css3:before { + content: $fa-var-css3; +} +.#{$fa-css-prefix}-anchor:before { + content: $fa-var-anchor; +} +.#{$fa-css-prefix}-unlock-alt:before { + content: $fa-var-unlock-alt; +} +.#{$fa-css-prefix}-bullseye:before { + content: $fa-var-bullseye; +} +.#{$fa-css-prefix}-ellipsis-h:before { + content: $fa-var-ellipsis-h; +} +.#{$fa-css-prefix}-ellipsis-v:before { + content: $fa-var-ellipsis-v; +} +.#{$fa-css-prefix}-rss-square:before { + content: $fa-var-rss-square; +} +.#{$fa-css-prefix}-play-circle:before { + content: $fa-var-play-circle; +} +.#{$fa-css-prefix}-ticket:before { + content: $fa-var-ticket; +} +.#{$fa-css-prefix}-minus-square:before { + content: $fa-var-minus-square; +} +.#{$fa-css-prefix}-minus-square-o:before { + content: $fa-var-minus-square-o; +} +.#{$fa-css-prefix}-level-up:before { + content: $fa-var-level-up; +} +.#{$fa-css-prefix}-level-down:before { + content: $fa-var-level-down; +} +.#{$fa-css-prefix}-check-square:before { + content: $fa-var-check-square; +} +.#{$fa-css-prefix}-pencil-square:before { + content: $fa-var-pencil-square; +} +.#{$fa-css-prefix}-external-link-square:before { + content: $fa-var-external-link-square; +} +.#{$fa-css-prefix}-share-square:before { + content: $fa-var-share-square; +} +.#{$fa-css-prefix}-compass:before { + content: $fa-var-compass; +} .#{$fa-css-prefix}-toggle-down:before, -.#{$fa-css-prefix}-caret-square-o-down:before { content: $fa-var-caret-square-o-down; } +.#{$fa-css-prefix}-caret-square-o-down:before { + content: $fa-var-caret-square-o-down; +} .#{$fa-css-prefix}-toggle-up:before, -.#{$fa-css-prefix}-caret-square-o-up:before { content: $fa-var-caret-square-o-up; } +.#{$fa-css-prefix}-caret-square-o-up:before { + content: $fa-var-caret-square-o-up; +} .#{$fa-css-prefix}-toggle-right:before, -.#{$fa-css-prefix}-caret-square-o-right:before { content: $fa-var-caret-square-o-right; } +.#{$fa-css-prefix}-caret-square-o-right:before { + content: $fa-var-caret-square-o-right; +} .#{$fa-css-prefix}-euro:before, -.#{$fa-css-prefix}-eur:before { content: $fa-var-eur; } -.#{$fa-css-prefix}-gbp:before { content: $fa-var-gbp; } +.#{$fa-css-prefix}-eur:before { + content: $fa-var-eur; +} +.#{$fa-css-prefix}-gbp:before { + content: $fa-var-gbp; +} .#{$fa-css-prefix}-dollar:before, -.#{$fa-css-prefix}-usd:before { content: $fa-var-usd; } +.#{$fa-css-prefix}-usd:before { + content: $fa-var-usd; +} .#{$fa-css-prefix}-rupee:before, -.#{$fa-css-prefix}-inr:before { content: $fa-var-inr; } +.#{$fa-css-prefix}-inr:before { + content: $fa-var-inr; +} .#{$fa-css-prefix}-cny:before, .#{$fa-css-prefix}-rmb:before, .#{$fa-css-prefix}-yen:before, -.#{$fa-css-prefix}-jpy:before { content: $fa-var-jpy; } +.#{$fa-css-prefix}-jpy:before { + content: $fa-var-jpy; +} .#{$fa-css-prefix}-ruble:before, .#{$fa-css-prefix}-rouble:before, -.#{$fa-css-prefix}-rub:before { content: $fa-var-rub; } +.#{$fa-css-prefix}-rub:before { + content: $fa-var-rub; +} .#{$fa-css-prefix}-won:before, -.#{$fa-css-prefix}-krw:before { content: $fa-var-krw; } +.#{$fa-css-prefix}-krw:before { + content: $fa-var-krw; +} .#{$fa-css-prefix}-bitcoin:before, -.#{$fa-css-prefix}-btc:before { content: $fa-var-btc; } -.#{$fa-css-prefix}-file:before { content: $fa-var-file; } -.#{$fa-css-prefix}-file-text:before { content: $fa-var-file-text; } -.#{$fa-css-prefix}-sort-alpha-asc:before { content: $fa-var-sort-alpha-asc; } -.#{$fa-css-prefix}-sort-alpha-desc:before { content: $fa-var-sort-alpha-desc; } -.#{$fa-css-prefix}-sort-amount-asc:before { content: $fa-var-sort-amount-asc; } -.#{$fa-css-prefix}-sort-amount-desc:before { content: $fa-var-sort-amount-desc; } -.#{$fa-css-prefix}-sort-numeric-asc:before { content: $fa-var-sort-numeric-asc; } -.#{$fa-css-prefix}-sort-numeric-desc:before { content: $fa-var-sort-numeric-desc; } -.#{$fa-css-prefix}-thumbs-up:before { content: $fa-var-thumbs-up; } -.#{$fa-css-prefix}-thumbs-down:before { content: $fa-var-thumbs-down; } -.#{$fa-css-prefix}-youtube-square:before { content: $fa-var-youtube-square; } -.#{$fa-css-prefix}-youtube:before { content: $fa-var-youtube; } -.#{$fa-css-prefix}-xing:before { content: $fa-var-xing; } -.#{$fa-css-prefix}-xing-square:before { content: $fa-var-xing-square; } -.#{$fa-css-prefix}-youtube-play:before { content: $fa-var-youtube-play; } -.#{$fa-css-prefix}-dropbox:before { content: $fa-var-dropbox; } -.#{$fa-css-prefix}-stack-overflow:before { content: $fa-var-stack-overflow; } -.#{$fa-css-prefix}-instagram:before { content: $fa-var-instagram; } -.#{$fa-css-prefix}-flickr:before { content: $fa-var-flickr; } -.#{$fa-css-prefix}-adn:before { content: $fa-var-adn; } -.#{$fa-css-prefix}-bitbucket:before { content: $fa-var-bitbucket; } -.#{$fa-css-prefix}-bitbucket-square:before { content: $fa-var-bitbucket-square; } -.#{$fa-css-prefix}-tumblr:before { content: $fa-var-tumblr; } -.#{$fa-css-prefix}-tumblr-square:before { content: $fa-var-tumblr-square; } -.#{$fa-css-prefix}-long-arrow-down:before { content: $fa-var-long-arrow-down; } -.#{$fa-css-prefix}-long-arrow-up:before { content: $fa-var-long-arrow-up; } -.#{$fa-css-prefix}-long-arrow-left:before { content: $fa-var-long-arrow-left; } -.#{$fa-css-prefix}-long-arrow-right:before { content: $fa-var-long-arrow-right; } -.#{$fa-css-prefix}-apple:before { content: $fa-var-apple; } -.#{$fa-css-prefix}-windows:before { content: $fa-var-windows; } -.#{$fa-css-prefix}-android:before { content: $fa-var-android; } -.#{$fa-css-prefix}-linux:before { content: $fa-var-linux; } -.#{$fa-css-prefix}-dribbble:before { content: $fa-var-dribbble; } -.#{$fa-css-prefix}-skype:before { content: $fa-var-skype; } -.#{$fa-css-prefix}-foursquare:before { content: $fa-var-foursquare; } -.#{$fa-css-prefix}-trello:before { content: $fa-var-trello; } -.#{$fa-css-prefix}-female:before { content: $fa-var-female; } -.#{$fa-css-prefix}-male:before { content: $fa-var-male; } +.#{$fa-css-prefix}-btc:before { + content: $fa-var-btc; +} +.#{$fa-css-prefix}-file:before { + content: $fa-var-file; +} +.#{$fa-css-prefix}-file-text:before { + content: $fa-var-file-text; +} +.#{$fa-css-prefix}-sort-alpha-asc:before { + content: $fa-var-sort-alpha-asc; +} +.#{$fa-css-prefix}-sort-alpha-desc:before { + content: $fa-var-sort-alpha-desc; +} +.#{$fa-css-prefix}-sort-amount-asc:before { + content: $fa-var-sort-amount-asc; +} +.#{$fa-css-prefix}-sort-amount-desc:before { + content: $fa-var-sort-amount-desc; +} +.#{$fa-css-prefix}-sort-numeric-asc:before { + content: $fa-var-sort-numeric-asc; +} +.#{$fa-css-prefix}-sort-numeric-desc:before { + content: $fa-var-sort-numeric-desc; +} +.#{$fa-css-prefix}-thumbs-up:before { + content: $fa-var-thumbs-up; +} +.#{$fa-css-prefix}-thumbs-down:before { + content: $fa-var-thumbs-down; +} +.#{$fa-css-prefix}-youtube-square:before { + content: $fa-var-youtube-square; +} +.#{$fa-css-prefix}-youtube:before { + content: $fa-var-youtube; +} +.#{$fa-css-prefix}-xing:before { + content: $fa-var-xing; +} +.#{$fa-css-prefix}-xing-square:before { + content: $fa-var-xing-square; +} +.#{$fa-css-prefix}-youtube-play:before { + content: $fa-var-youtube-play; +} +.#{$fa-css-prefix}-dropbox:before { + content: $fa-var-dropbox; +} +.#{$fa-css-prefix}-stack-overflow:before { + content: $fa-var-stack-overflow; +} +.#{$fa-css-prefix}-instagram:before { + content: $fa-var-instagram; +} +.#{$fa-css-prefix}-flickr:before { + content: $fa-var-flickr; +} +.#{$fa-css-prefix}-adn:before { + content: $fa-var-adn; +} +.#{$fa-css-prefix}-bitbucket:before { + content: $fa-var-bitbucket; +} +.#{$fa-css-prefix}-bitbucket-square:before { + content: $fa-var-bitbucket-square; +} +.#{$fa-css-prefix}-tumblr:before { + content: $fa-var-tumblr; +} +.#{$fa-css-prefix}-tumblr-square:before { + content: $fa-var-tumblr-square; +} +.#{$fa-css-prefix}-long-arrow-down:before { + content: $fa-var-long-arrow-down; +} +.#{$fa-css-prefix}-long-arrow-up:before { + content: $fa-var-long-arrow-up; +} +.#{$fa-css-prefix}-long-arrow-left:before { + content: $fa-var-long-arrow-left; +} +.#{$fa-css-prefix}-long-arrow-right:before { + content: $fa-var-long-arrow-right; +} +.#{$fa-css-prefix}-apple:before { + content: $fa-var-apple; +} +.#{$fa-css-prefix}-windows:before { + content: $fa-var-windows; +} +.#{$fa-css-prefix}-android:before { + content: $fa-var-android; +} +.#{$fa-css-prefix}-linux:before { + content: $fa-var-linux; +} +.#{$fa-css-prefix}-dribbble:before { + content: $fa-var-dribbble; +} +.#{$fa-css-prefix}-skype:before { + content: $fa-var-skype; +} +.#{$fa-css-prefix}-foursquare:before { + content: $fa-var-foursquare; +} +.#{$fa-css-prefix}-trello:before { + content: $fa-var-trello; +} +.#{$fa-css-prefix}-female:before { + content: $fa-var-female; +} +.#{$fa-css-prefix}-male:before { + content: $fa-var-male; +} .#{$fa-css-prefix}-gittip:before, -.#{$fa-css-prefix}-gratipay:before { content: $fa-var-gratipay; } -.#{$fa-css-prefix}-sun-o:before { content: $fa-var-sun-o; } -.#{$fa-css-prefix}-moon-o:before { content: $fa-var-moon-o; } -.#{$fa-css-prefix}-archive:before { content: $fa-var-archive; } -.#{$fa-css-prefix}-bug:before { content: $fa-var-bug; } -.#{$fa-css-prefix}-vk:before { content: $fa-var-vk; } -.#{$fa-css-prefix}-weibo:before { content: $fa-var-weibo; } -.#{$fa-css-prefix}-renren:before { content: $fa-var-renren; } -.#{$fa-css-prefix}-pagelines:before { content: $fa-var-pagelines; } -.#{$fa-css-prefix}-stack-exchange:before { content: $fa-var-stack-exchange; } -.#{$fa-css-prefix}-arrow-circle-o-right:before { content: $fa-var-arrow-circle-o-right; } -.#{$fa-css-prefix}-arrow-circle-o-left:before { content: $fa-var-arrow-circle-o-left; } +.#{$fa-css-prefix}-gratipay:before { + content: $fa-var-gratipay; +} +.#{$fa-css-prefix}-sun-o:before { + content: $fa-var-sun-o; +} +.#{$fa-css-prefix}-moon-o:before { + content: $fa-var-moon-o; +} +.#{$fa-css-prefix}-archive:before { + content: $fa-var-archive; +} +.#{$fa-css-prefix}-bug:before { + content: $fa-var-bug; +} +.#{$fa-css-prefix}-vk:before { + content: $fa-var-vk; +} +.#{$fa-css-prefix}-weibo:before { + content: $fa-var-weibo; +} +.#{$fa-css-prefix}-renren:before { + content: $fa-var-renren; +} +.#{$fa-css-prefix}-pagelines:before { + content: $fa-var-pagelines; +} +.#{$fa-css-prefix}-stack-exchange:before { + content: $fa-var-stack-exchange; +} +.#{$fa-css-prefix}-arrow-circle-o-right:before { + content: $fa-var-arrow-circle-o-right; +} +.#{$fa-css-prefix}-arrow-circle-o-left:before { + content: $fa-var-arrow-circle-o-left; +} .#{$fa-css-prefix}-toggle-left:before, -.#{$fa-css-prefix}-caret-square-o-left:before { content: $fa-var-caret-square-o-left; } -.#{$fa-css-prefix}-dot-circle-o:before { content: $fa-var-dot-circle-o; } -.#{$fa-css-prefix}-wheelchair:before { content: $fa-var-wheelchair; } -.#{$fa-css-prefix}-vimeo-square:before { content: $fa-var-vimeo-square; } +.#{$fa-css-prefix}-caret-square-o-left:before { + content: $fa-var-caret-square-o-left; +} +.#{$fa-css-prefix}-dot-circle-o:before { + content: $fa-var-dot-circle-o; +} +.#{$fa-css-prefix}-wheelchair:before { + content: $fa-var-wheelchair; +} +.#{$fa-css-prefix}-vimeo-square:before { + content: $fa-var-vimeo-square; +} .#{$fa-css-prefix}-turkish-lira:before, -.#{$fa-css-prefix}-try:before { content: $fa-var-try; } -.#{$fa-css-prefix}-plus-square-o:before { content: $fa-var-plus-square-o; } -.#{$fa-css-prefix}-space-shuttle:before { content: $fa-var-space-shuttle; } -.#{$fa-css-prefix}-slack:before { content: $fa-var-slack; } -.#{$fa-css-prefix}-envelope-square:before { content: $fa-var-envelope-square; } -.#{$fa-css-prefix}-wordpress:before { content: $fa-var-wordpress; } -.#{$fa-css-prefix}-openid:before { content: $fa-var-openid; } +.#{$fa-css-prefix}-try:before { + content: $fa-var-try; +} +.#{$fa-css-prefix}-plus-square-o:before { + content: $fa-var-plus-square-o; +} +.#{$fa-css-prefix}-space-shuttle:before { + content: $fa-var-space-shuttle; +} +.#{$fa-css-prefix}-slack:before { + content: $fa-var-slack; +} +.#{$fa-css-prefix}-envelope-square:before { + content: $fa-var-envelope-square; +} +.#{$fa-css-prefix}-wordpress:before { + content: $fa-var-wordpress; +} +.#{$fa-css-prefix}-openid:before { + content: $fa-var-openid; +} .#{$fa-css-prefix}-institution:before, .#{$fa-css-prefix}-bank:before, -.#{$fa-css-prefix}-university:before { content: $fa-var-university; } +.#{$fa-css-prefix}-university:before { + content: $fa-var-university; +} .#{$fa-css-prefix}-mortar-board:before, -.#{$fa-css-prefix}-graduation-cap:before { content: $fa-var-graduation-cap; } -.#{$fa-css-prefix}-yahoo:before { content: $fa-var-yahoo; } -.#{$fa-css-prefix}-google:before { content: $fa-var-google; } -.#{$fa-css-prefix}-reddit:before { content: $fa-var-reddit; } -.#{$fa-css-prefix}-reddit-square:before { content: $fa-var-reddit-square; } -.#{$fa-css-prefix}-stumbleupon-circle:before { content: $fa-var-stumbleupon-circle; } -.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; } -.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; } -.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; } -.#{$fa-css-prefix}-pied-piper-pp:before { content: $fa-var-pied-piper-pp; } -.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; } -.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; } -.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; } -.#{$fa-css-prefix}-language:before { content: $fa-var-language; } -.#{$fa-css-prefix}-fax:before { content: $fa-var-fax; } -.#{$fa-css-prefix}-building:before { content: $fa-var-building; } -.#{$fa-css-prefix}-child:before { content: $fa-var-child; } -.#{$fa-css-prefix}-paw:before { content: $fa-var-paw; } -.#{$fa-css-prefix}-spoon:before { content: $fa-var-spoon; } -.#{$fa-css-prefix}-cube:before { content: $fa-var-cube; } -.#{$fa-css-prefix}-cubes:before { content: $fa-var-cubes; } -.#{$fa-css-prefix}-behance:before { content: $fa-var-behance; } -.#{$fa-css-prefix}-behance-square:before { content: $fa-var-behance-square; } -.#{$fa-css-prefix}-steam:before { content: $fa-var-steam; } -.#{$fa-css-prefix}-steam-square:before { content: $fa-var-steam-square; } -.#{$fa-css-prefix}-recycle:before { content: $fa-var-recycle; } +.#{$fa-css-prefix}-graduation-cap:before { + content: $fa-var-graduation-cap; +} +.#{$fa-css-prefix}-yahoo:before { + content: $fa-var-yahoo; +} +.#{$fa-css-prefix}-google:before { + content: $fa-var-google; +} +.#{$fa-css-prefix}-reddit:before { + content: $fa-var-reddit; +} +.#{$fa-css-prefix}-reddit-square:before { + content: $fa-var-reddit-square; +} +.#{$fa-css-prefix}-stumbleupon-circle:before { + content: $fa-var-stumbleupon-circle; +} +.#{$fa-css-prefix}-stumbleupon:before { + content: $fa-var-stumbleupon; +} +.#{$fa-css-prefix}-delicious:before { + content: $fa-var-delicious; +} +.#{$fa-css-prefix}-digg:before { + content: $fa-var-digg; +} +.#{$fa-css-prefix}-pied-piper-pp:before { + content: $fa-var-pied-piper-pp; +} +.#{$fa-css-prefix}-pied-piper-alt:before { + content: $fa-var-pied-piper-alt; +} +.#{$fa-css-prefix}-drupal:before { + content: $fa-var-drupal; +} +.#{$fa-css-prefix}-joomla:before { + content: $fa-var-joomla; +} +.#{$fa-css-prefix}-language:before { + content: $fa-var-language; +} +.#{$fa-css-prefix}-fax:before { + content: $fa-var-fax; +} +.#{$fa-css-prefix}-building:before { + content: $fa-var-building; +} +.#{$fa-css-prefix}-child:before { + content: $fa-var-child; +} +.#{$fa-css-prefix}-paw:before { + content: $fa-var-paw; +} +.#{$fa-css-prefix}-spoon:before { + content: $fa-var-spoon; +} +.#{$fa-css-prefix}-cube:before { + content: $fa-var-cube; +} +.#{$fa-css-prefix}-cubes:before { + content: $fa-var-cubes; +} +.#{$fa-css-prefix}-behance:before { + content: $fa-var-behance; +} +.#{$fa-css-prefix}-behance-square:before { + content: $fa-var-behance-square; +} +.#{$fa-css-prefix}-steam:before { + content: $fa-var-steam; +} +.#{$fa-css-prefix}-steam-square:before { + content: $fa-var-steam-square; +} +.#{$fa-css-prefix}-recycle:before { + content: $fa-var-recycle; +} .#{$fa-css-prefix}-automobile:before, -.#{$fa-css-prefix}-car:before { content: $fa-var-car; } +.#{$fa-css-prefix}-car:before { + content: $fa-var-car; +} .#{$fa-css-prefix}-cab:before, -.#{$fa-css-prefix}-taxi:before { content: $fa-var-taxi; } -.#{$fa-css-prefix}-tree:before { content: $fa-var-tree; } -.#{$fa-css-prefix}-spotify:before { content: $fa-var-spotify; } -.#{$fa-css-prefix}-deviantart:before { content: $fa-var-deviantart; } -.#{$fa-css-prefix}-soundcloud:before { content: $fa-var-soundcloud; } -.#{$fa-css-prefix}-database:before { content: $fa-var-database; } -.#{$fa-css-prefix}-file-pdf-o:before { content: $fa-var-file-pdf-o; } -.#{$fa-css-prefix}-file-word-o:before { content: $fa-var-file-word-o; } -.#{$fa-css-prefix}-file-excel-o:before { content: $fa-var-file-excel-o; } -.#{$fa-css-prefix}-file-powerpoint-o:before { content: $fa-var-file-powerpoint-o; } +.#{$fa-css-prefix}-taxi:before { + content: $fa-var-taxi; +} +.#{$fa-css-prefix}-tree:before { + content: $fa-var-tree; +} +.#{$fa-css-prefix}-spotify:before { + content: $fa-var-spotify; +} +.#{$fa-css-prefix}-deviantart:before { + content: $fa-var-deviantart; +} +.#{$fa-css-prefix}-soundcloud:before { + content: $fa-var-soundcloud; +} +.#{$fa-css-prefix}-database:before { + content: $fa-var-database; +} +.#{$fa-css-prefix}-file-pdf-o:before { + content: $fa-var-file-pdf-o; +} +.#{$fa-css-prefix}-file-word-o:before { + content: $fa-var-file-word-o; +} +.#{$fa-css-prefix}-file-excel-o:before { + content: $fa-var-file-excel-o; +} +.#{$fa-css-prefix}-file-powerpoint-o:before { + content: $fa-var-file-powerpoint-o; +} .#{$fa-css-prefix}-file-photo-o:before, .#{$fa-css-prefix}-file-picture-o:before, -.#{$fa-css-prefix}-file-image-o:before { content: $fa-var-file-image-o; } +.#{$fa-css-prefix}-file-image-o:before { + content: $fa-var-file-image-o; +} .#{$fa-css-prefix}-file-zip-o:before, -.#{$fa-css-prefix}-file-archive-o:before { content: $fa-var-file-archive-o; } +.#{$fa-css-prefix}-file-archive-o:before { + content: $fa-var-file-archive-o; +} .#{$fa-css-prefix}-file-sound-o:before, -.#{$fa-css-prefix}-file-audio-o:before { content: $fa-var-file-audio-o; } +.#{$fa-css-prefix}-file-audio-o:before { + content: $fa-var-file-audio-o; +} .#{$fa-css-prefix}-file-movie-o:before, -.#{$fa-css-prefix}-file-video-o:before { content: $fa-var-file-video-o; } -.#{$fa-css-prefix}-file-code-o:before { content: $fa-var-file-code-o; } -.#{$fa-css-prefix}-vine:before { content: $fa-var-vine; } -.#{$fa-css-prefix}-codepen:before { content: $fa-var-codepen; } -.#{$fa-css-prefix}-jsfiddle:before { content: $fa-var-jsfiddle; } +.#{$fa-css-prefix}-file-video-o:before { + content: $fa-var-file-video-o; +} +.#{$fa-css-prefix}-file-code-o:before { + content: $fa-var-file-code-o; +} +.#{$fa-css-prefix}-vine:before { + content: $fa-var-vine; +} +.#{$fa-css-prefix}-codepen:before { + content: $fa-var-codepen; +} +.#{$fa-css-prefix}-jsfiddle:before { + content: $fa-var-jsfiddle; +} .#{$fa-css-prefix}-life-bouy:before, .#{$fa-css-prefix}-life-buoy:before, .#{$fa-css-prefix}-life-saver:before, .#{$fa-css-prefix}-support:before, -.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; } -.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; } +.#{$fa-css-prefix}-life-ring:before { + content: $fa-var-life-ring; +} +.#{$fa-css-prefix}-circle-o-notch:before { + content: $fa-var-circle-o-notch; +} .#{$fa-css-prefix}-ra:before, .#{$fa-css-prefix}-resistance:before, -.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; } +.#{$fa-css-prefix}-rebel:before { + content: $fa-var-rebel; +} .#{$fa-css-prefix}-ge:before, -.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; } -.#{$fa-css-prefix}-git-square:before { content: $fa-var-git-square; } -.#{$fa-css-prefix}-git:before { content: $fa-var-git; } +.#{$fa-css-prefix}-empire:before { + content: $fa-var-empire; +} +.#{$fa-css-prefix}-git-square:before { + content: $fa-var-git-square; +} +.#{$fa-css-prefix}-git:before { + content: $fa-var-git; +} .#{$fa-css-prefix}-y-combinator-square:before, .#{$fa-css-prefix}-yc-square:before, -.#{$fa-css-prefix}-hacker-news:before { content: $fa-var-hacker-news; } -.#{$fa-css-prefix}-tencent-weibo:before { content: $fa-var-tencent-weibo; } -.#{$fa-css-prefix}-qq:before { content: $fa-var-qq; } +.#{$fa-css-prefix}-hacker-news:before { + content: $fa-var-hacker-news; +} +.#{$fa-css-prefix}-tencent-weibo:before { + content: $fa-var-tencent-weibo; +} +.#{$fa-css-prefix}-qq:before { + content: $fa-var-qq; +} .#{$fa-css-prefix}-wechat:before, -.#{$fa-css-prefix}-weixin:before { content: $fa-var-weixin; } +.#{$fa-css-prefix}-weixin:before { + content: $fa-var-weixin; +} .#{$fa-css-prefix}-send:before, -.#{$fa-css-prefix}-paper-plane:before { content: $fa-var-paper-plane; } +.#{$fa-css-prefix}-paper-plane:before { + content: $fa-var-paper-plane; +} .#{$fa-css-prefix}-send-o:before, -.#{$fa-css-prefix}-paper-plane-o:before { content: $fa-var-paper-plane-o; } -.#{$fa-css-prefix}-history:before { content: $fa-var-history; } -.#{$fa-css-prefix}-circle-thin:before { content: $fa-var-circle-thin; } -.#{$fa-css-prefix}-header:before { content: $fa-var-header; } -.#{$fa-css-prefix}-paragraph:before { content: $fa-var-paragraph; } -.#{$fa-css-prefix}-sliders:before { content: $fa-var-sliders; } -.#{$fa-css-prefix}-share-alt:before { content: $fa-var-share-alt; } -.#{$fa-css-prefix}-share-alt-square:before { content: $fa-var-share-alt-square; } -.#{$fa-css-prefix}-bomb:before { content: $fa-var-bomb; } +.#{$fa-css-prefix}-paper-plane-o:before { + content: $fa-var-paper-plane-o; +} +.#{$fa-css-prefix}-history:before { + content: $fa-var-history; +} +.#{$fa-css-prefix}-circle-thin:before { + content: $fa-var-circle-thin; +} +.#{$fa-css-prefix}-header:before { + content: $fa-var-header; +} +.#{$fa-css-prefix}-paragraph:before { + content: $fa-var-paragraph; +} +.#{$fa-css-prefix}-sliders:before { + content: $fa-var-sliders; +} +.#{$fa-css-prefix}-share-alt:before { + content: $fa-var-share-alt; +} +.#{$fa-css-prefix}-share-alt-square:before { + content: $fa-var-share-alt-square; +} +.#{$fa-css-prefix}-bomb:before { + content: $fa-var-bomb; +} .#{$fa-css-prefix}-soccer-ball-o:before, -.#{$fa-css-prefix}-futbol-o:before { content: $fa-var-futbol-o; } -.#{$fa-css-prefix}-tty:before { content: $fa-var-tty; } -.#{$fa-css-prefix}-binoculars:before { content: $fa-var-binoculars; } -.#{$fa-css-prefix}-plug:before { content: $fa-var-plug; } -.#{$fa-css-prefix}-slideshare:before { content: $fa-var-slideshare; } -.#{$fa-css-prefix}-twitch:before { content: $fa-var-twitch; } -.#{$fa-css-prefix}-yelp:before { content: $fa-var-yelp; } -.#{$fa-css-prefix}-newspaper-o:before { content: $fa-var-newspaper-o; } -.#{$fa-css-prefix}-wifi:before { content: $fa-var-wifi; } -.#{$fa-css-prefix}-calculator:before { content: $fa-var-calculator; } -.#{$fa-css-prefix}-paypal:before { content: $fa-var-paypal; } -.#{$fa-css-prefix}-google-wallet:before { content: $fa-var-google-wallet; } -.#{$fa-css-prefix}-cc-visa:before { content: $fa-var-cc-visa; } -.#{$fa-css-prefix}-cc-mastercard:before { content: $fa-var-cc-mastercard; } -.#{$fa-css-prefix}-cc-discover:before { content: $fa-var-cc-discover; } -.#{$fa-css-prefix}-cc-amex:before { content: $fa-var-cc-amex; } -.#{$fa-css-prefix}-cc-paypal:before { content: $fa-var-cc-paypal; } -.#{$fa-css-prefix}-cc-stripe:before { content: $fa-var-cc-stripe; } -.#{$fa-css-prefix}-bell-slash:before { content: $fa-var-bell-slash; } -.#{$fa-css-prefix}-bell-slash-o:before { content: $fa-var-bell-slash-o; } -.#{$fa-css-prefix}-trash:before { content: $fa-var-trash; } -.#{$fa-css-prefix}-copyright:before { content: $fa-var-copyright; } -.#{$fa-css-prefix}-at:before { content: $fa-var-at; } -.#{$fa-css-prefix}-eyedropper:before { content: $fa-var-eyedropper; } -.#{$fa-css-prefix}-paint-brush:before { content: $fa-var-paint-brush; } -.#{$fa-css-prefix}-birthday-cake:before { content: $fa-var-birthday-cake; } -.#{$fa-css-prefix}-area-chart:before { content: $fa-var-area-chart; } -.#{$fa-css-prefix}-pie-chart:before { content: $fa-var-pie-chart; } -.#{$fa-css-prefix}-line-chart:before { content: $fa-var-line-chart; } -.#{$fa-css-prefix}-lastfm:before { content: $fa-var-lastfm; } -.#{$fa-css-prefix}-lastfm-square:before { content: $fa-var-lastfm-square; } -.#{$fa-css-prefix}-toggle-off:before { content: $fa-var-toggle-off; } -.#{$fa-css-prefix}-toggle-on:before { content: $fa-var-toggle-on; } -.#{$fa-css-prefix}-bicycle:before { content: $fa-var-bicycle; } -.#{$fa-css-prefix}-bus:before { content: $fa-var-bus; } -.#{$fa-css-prefix}-ioxhost:before { content: $fa-var-ioxhost; } -.#{$fa-css-prefix}-angellist:before { content: $fa-var-angellist; } -.#{$fa-css-prefix}-cc:before { content: $fa-var-cc; } +.#{$fa-css-prefix}-futbol-o:before { + content: $fa-var-futbol-o; +} +.#{$fa-css-prefix}-tty:before { + content: $fa-var-tty; +} +.#{$fa-css-prefix}-binoculars:before { + content: $fa-var-binoculars; +} +.#{$fa-css-prefix}-plug:before { + content: $fa-var-plug; +} +.#{$fa-css-prefix}-slideshare:before { + content: $fa-var-slideshare; +} +.#{$fa-css-prefix}-twitch:before { + content: $fa-var-twitch; +} +.#{$fa-css-prefix}-yelp:before { + content: $fa-var-yelp; +} +.#{$fa-css-prefix}-newspaper-o:before { + content: $fa-var-newspaper-o; +} +.#{$fa-css-prefix}-wifi:before { + content: $fa-var-wifi; +} +.#{$fa-css-prefix}-calculator:before { + content: $fa-var-calculator; +} +.#{$fa-css-prefix}-paypal:before { + content: $fa-var-paypal; +} +.#{$fa-css-prefix}-google-wallet:before { + content: $fa-var-google-wallet; +} +.#{$fa-css-prefix}-cc-visa:before { + content: $fa-var-cc-visa; +} +.#{$fa-css-prefix}-cc-mastercard:before { + content: $fa-var-cc-mastercard; +} +.#{$fa-css-prefix}-cc-discover:before { + content: $fa-var-cc-discover; +} +.#{$fa-css-prefix}-cc-amex:before { + content: $fa-var-cc-amex; +} +.#{$fa-css-prefix}-cc-paypal:before { + content: $fa-var-cc-paypal; +} +.#{$fa-css-prefix}-cc-stripe:before { + content: $fa-var-cc-stripe; +} +.#{$fa-css-prefix}-bell-slash:before { + content: $fa-var-bell-slash; +} +.#{$fa-css-prefix}-bell-slash-o:before { + content: $fa-var-bell-slash-o; +} +.#{$fa-css-prefix}-trash:before { + content: $fa-var-trash; +} +.#{$fa-css-prefix}-copyright:before { + content: $fa-var-copyright; +} +.#{$fa-css-prefix}-at:before { + content: $fa-var-at; +} +.#{$fa-css-prefix}-eyedropper:before { + content: $fa-var-eyedropper; +} +.#{$fa-css-prefix}-paint-brush:before { + content: $fa-var-paint-brush; +} +.#{$fa-css-prefix}-birthday-cake:before { + content: $fa-var-birthday-cake; +} +.#{$fa-css-prefix}-area-chart:before { + content: $fa-var-area-chart; +} +.#{$fa-css-prefix}-pie-chart:before { + content: $fa-var-pie-chart; +} +.#{$fa-css-prefix}-line-chart:before { + content: $fa-var-line-chart; +} +.#{$fa-css-prefix}-lastfm:before { + content: $fa-var-lastfm; +} +.#{$fa-css-prefix}-lastfm-square:before { + content: $fa-var-lastfm-square; +} +.#{$fa-css-prefix}-toggle-off:before { + content: $fa-var-toggle-off; +} +.#{$fa-css-prefix}-toggle-on:before { + content: $fa-var-toggle-on; +} +.#{$fa-css-prefix}-bicycle:before { + content: $fa-var-bicycle; +} +.#{$fa-css-prefix}-bus:before { + content: $fa-var-bus; +} +.#{$fa-css-prefix}-ioxhost:before { + content: $fa-var-ioxhost; +} +.#{$fa-css-prefix}-angellist:before { + content: $fa-var-angellist; +} +.#{$fa-css-prefix}-cc:before { + content: $fa-var-cc; +} .#{$fa-css-prefix}-shekel:before, .#{$fa-css-prefix}-sheqel:before, -.#{$fa-css-prefix}-ils:before { content: $fa-var-ils; } -.#{$fa-css-prefix}-meanpath:before { content: $fa-var-meanpath; } -.#{$fa-css-prefix}-buysellads:before { content: $fa-var-buysellads; } -.#{$fa-css-prefix}-connectdevelop:before { content: $fa-var-connectdevelop; } -.#{$fa-css-prefix}-dashcube:before { content: $fa-var-dashcube; } -.#{$fa-css-prefix}-forumbee:before { content: $fa-var-forumbee; } -.#{$fa-css-prefix}-leanpub:before { content: $fa-var-leanpub; } -.#{$fa-css-prefix}-sellsy:before { content: $fa-var-sellsy; } -.#{$fa-css-prefix}-shirtsinbulk:before { content: $fa-var-shirtsinbulk; } -.#{$fa-css-prefix}-simplybuilt:before { content: $fa-var-simplybuilt; } -.#{$fa-css-prefix}-skyatlas:before { content: $fa-var-skyatlas; } -.#{$fa-css-prefix}-cart-plus:before { content: $fa-var-cart-plus; } -.#{$fa-css-prefix}-cart-arrow-down:before { content: $fa-var-cart-arrow-down; } -.#{$fa-css-prefix}-diamond:before { content: $fa-var-diamond; } -.#{$fa-css-prefix}-ship:before { content: $fa-var-ship; } -.#{$fa-css-prefix}-user-secret:before { content: $fa-var-user-secret; } -.#{$fa-css-prefix}-motorcycle:before { content: $fa-var-motorcycle; } -.#{$fa-css-prefix}-street-view:before { content: $fa-var-street-view; } -.#{$fa-css-prefix}-heartbeat:before { content: $fa-var-heartbeat; } -.#{$fa-css-prefix}-venus:before { content: $fa-var-venus; } -.#{$fa-css-prefix}-mars:before { content: $fa-var-mars; } -.#{$fa-css-prefix}-mercury:before { content: $fa-var-mercury; } +.#{$fa-css-prefix}-ils:before { + content: $fa-var-ils; +} +.#{$fa-css-prefix}-meanpath:before { + content: $fa-var-meanpath; +} +.#{$fa-css-prefix}-buysellads:before { + content: $fa-var-buysellads; +} +.#{$fa-css-prefix}-connectdevelop:before { + content: $fa-var-connectdevelop; +} +.#{$fa-css-prefix}-dashcube:before { + content: $fa-var-dashcube; +} +.#{$fa-css-prefix}-forumbee:before { + content: $fa-var-forumbee; +} +.#{$fa-css-prefix}-leanpub:before { + content: $fa-var-leanpub; +} +.#{$fa-css-prefix}-sellsy:before { + content: $fa-var-sellsy; +} +.#{$fa-css-prefix}-shirtsinbulk:before { + content: $fa-var-shirtsinbulk; +} +.#{$fa-css-prefix}-simplybuilt:before { + content: $fa-var-simplybuilt; +} +.#{$fa-css-prefix}-skyatlas:before { + content: $fa-var-skyatlas; +} +.#{$fa-css-prefix}-cart-plus:before { + content: $fa-var-cart-plus; +} +.#{$fa-css-prefix}-cart-arrow-down:before { + content: $fa-var-cart-arrow-down; +} +.#{$fa-css-prefix}-diamond:before { + content: $fa-var-diamond; +} +.#{$fa-css-prefix}-ship:before { + content: $fa-var-ship; +} +.#{$fa-css-prefix}-user-secret:before { + content: $fa-var-user-secret; +} +.#{$fa-css-prefix}-motorcycle:before { + content: $fa-var-motorcycle; +} +.#{$fa-css-prefix}-street-view:before { + content: $fa-var-street-view; +} +.#{$fa-css-prefix}-heartbeat:before { + content: $fa-var-heartbeat; +} +.#{$fa-css-prefix}-venus:before { + content: $fa-var-venus; +} +.#{$fa-css-prefix}-mars:before { + content: $fa-var-mars; +} +.#{$fa-css-prefix}-mercury:before { + content: $fa-var-mercury; +} .#{$fa-css-prefix}-intersex:before, -.#{$fa-css-prefix}-transgender:before { content: $fa-var-transgender; } -.#{$fa-css-prefix}-transgender-alt:before { content: $fa-var-transgender-alt; } -.#{$fa-css-prefix}-venus-double:before { content: $fa-var-venus-double; } -.#{$fa-css-prefix}-mars-double:before { content: $fa-var-mars-double; } -.#{$fa-css-prefix}-venus-mars:before { content: $fa-var-venus-mars; } -.#{$fa-css-prefix}-mars-stroke:before { content: $fa-var-mars-stroke; } -.#{$fa-css-prefix}-mars-stroke-v:before { content: $fa-var-mars-stroke-v; } -.#{$fa-css-prefix}-mars-stroke-h:before { content: $fa-var-mars-stroke-h; } -.#{$fa-css-prefix}-neuter:before { content: $fa-var-neuter; } -.#{$fa-css-prefix}-genderless:before { content: $fa-var-genderless; } -.#{$fa-css-prefix}-facebook-official:before { content: $fa-var-facebook-official; } -.#{$fa-css-prefix}-pinterest-p:before { content: $fa-var-pinterest-p; } -.#{$fa-css-prefix}-whatsapp:before { content: $fa-var-whatsapp; } -.#{$fa-css-prefix}-server:before { content: $fa-var-server; } -.#{$fa-css-prefix}-user-plus:before { content: $fa-var-user-plus; } -.#{$fa-css-prefix}-user-times:before { content: $fa-var-user-times; } +.#{$fa-css-prefix}-transgender:before { + content: $fa-var-transgender; +} +.#{$fa-css-prefix}-transgender-alt:before { + content: $fa-var-transgender-alt; +} +.#{$fa-css-prefix}-venus-double:before { + content: $fa-var-venus-double; +} +.#{$fa-css-prefix}-mars-double:before { + content: $fa-var-mars-double; +} +.#{$fa-css-prefix}-venus-mars:before { + content: $fa-var-venus-mars; +} +.#{$fa-css-prefix}-mars-stroke:before { + content: $fa-var-mars-stroke; +} +.#{$fa-css-prefix}-mars-stroke-v:before { + content: $fa-var-mars-stroke-v; +} +.#{$fa-css-prefix}-mars-stroke-h:before { + content: $fa-var-mars-stroke-h; +} +.#{$fa-css-prefix}-neuter:before { + content: $fa-var-neuter; +} +.#{$fa-css-prefix}-genderless:before { + content: $fa-var-genderless; +} +.#{$fa-css-prefix}-facebook-official:before { + content: $fa-var-facebook-official; +} +.#{$fa-css-prefix}-pinterest-p:before { + content: $fa-var-pinterest-p; +} +.#{$fa-css-prefix}-whatsapp:before { + content: $fa-var-whatsapp; +} +.#{$fa-css-prefix}-server:before { + content: $fa-var-server; +} +.#{$fa-css-prefix}-user-plus:before { + content: $fa-var-user-plus; +} +.#{$fa-css-prefix}-user-times:before { + content: $fa-var-user-times; +} .#{$fa-css-prefix}-hotel:before, -.#{$fa-css-prefix}-bed:before { content: $fa-var-bed; } -.#{$fa-css-prefix}-viacoin:before { content: $fa-var-viacoin; } -.#{$fa-css-prefix}-train:before { content: $fa-var-train; } -.#{$fa-css-prefix}-subway:before { content: $fa-var-subway; } -.#{$fa-css-prefix}-medium:before { content: $fa-var-medium; } +.#{$fa-css-prefix}-bed:before { + content: $fa-var-bed; +} +.#{$fa-css-prefix}-viacoin:before { + content: $fa-var-viacoin; +} +.#{$fa-css-prefix}-train:before { + content: $fa-var-train; +} +.#{$fa-css-prefix}-subway:before { + content: $fa-var-subway; +} +.#{$fa-css-prefix}-medium:before { + content: $fa-var-medium; +} .#{$fa-css-prefix}-yc:before, -.#{$fa-css-prefix}-y-combinator:before { content: $fa-var-y-combinator; } -.#{$fa-css-prefix}-optin-monster:before { content: $fa-var-optin-monster; } -.#{$fa-css-prefix}-opencart:before { content: $fa-var-opencart; } -.#{$fa-css-prefix}-expeditedssl:before { content: $fa-var-expeditedssl; } +.#{$fa-css-prefix}-y-combinator:before { + content: $fa-var-y-combinator; +} +.#{$fa-css-prefix}-optin-monster:before { + content: $fa-var-optin-monster; +} +.#{$fa-css-prefix}-opencart:before { + content: $fa-var-opencart; +} +.#{$fa-css-prefix}-expeditedssl:before { + content: $fa-var-expeditedssl; +} .#{$fa-css-prefix}-battery-4:before, .#{$fa-css-prefix}-battery:before, -.#{$fa-css-prefix}-battery-full:before { content: $fa-var-battery-full; } +.#{$fa-css-prefix}-battery-full:before { + content: $fa-var-battery-full; +} .#{$fa-css-prefix}-battery-3:before, -.#{$fa-css-prefix}-battery-three-quarters:before { content: $fa-var-battery-three-quarters; } +.#{$fa-css-prefix}-battery-three-quarters:before { + content: $fa-var-battery-three-quarters; +} .#{$fa-css-prefix}-battery-2:before, -.#{$fa-css-prefix}-battery-half:before { content: $fa-var-battery-half; } +.#{$fa-css-prefix}-battery-half:before { + content: $fa-var-battery-half; +} .#{$fa-css-prefix}-battery-1:before, -.#{$fa-css-prefix}-battery-quarter:before { content: $fa-var-battery-quarter; } +.#{$fa-css-prefix}-battery-quarter:before { + content: $fa-var-battery-quarter; +} .#{$fa-css-prefix}-battery-0:before, -.#{$fa-css-prefix}-battery-empty:before { content: $fa-var-battery-empty; } -.#{$fa-css-prefix}-mouse-pointer:before { content: $fa-var-mouse-pointer; } -.#{$fa-css-prefix}-i-cursor:before { content: $fa-var-i-cursor; } -.#{$fa-css-prefix}-object-group:before { content: $fa-var-object-group; } -.#{$fa-css-prefix}-object-ungroup:before { content: $fa-var-object-ungroup; } -.#{$fa-css-prefix}-sticky-note:before { content: $fa-var-sticky-note; } -.#{$fa-css-prefix}-sticky-note-o:before { content: $fa-var-sticky-note-o; } -.#{$fa-css-prefix}-cc-jcb:before { content: $fa-var-cc-jcb; } -.#{$fa-css-prefix}-cc-diners-club:before { content: $fa-var-cc-diners-club; } -.#{$fa-css-prefix}-clone:before { content: $fa-var-clone; } -.#{$fa-css-prefix}-balance-scale:before { content: $fa-var-balance-scale; } -.#{$fa-css-prefix}-hourglass-o:before { content: $fa-var-hourglass-o; } +.#{$fa-css-prefix}-battery-empty:before { + content: $fa-var-battery-empty; +} +.#{$fa-css-prefix}-mouse-pointer:before { + content: $fa-var-mouse-pointer; +} +.#{$fa-css-prefix}-i-cursor:before { + content: $fa-var-i-cursor; +} +.#{$fa-css-prefix}-object-group:before { + content: $fa-var-object-group; +} +.#{$fa-css-prefix}-object-ungroup:before { + content: $fa-var-object-ungroup; +} +.#{$fa-css-prefix}-sticky-note:before { + content: $fa-var-sticky-note; +} +.#{$fa-css-prefix}-sticky-note-o:before { + content: $fa-var-sticky-note-o; +} +.#{$fa-css-prefix}-cc-jcb:before { + content: $fa-var-cc-jcb; +} +.#{$fa-css-prefix}-cc-diners-club:before { + content: $fa-var-cc-diners-club; +} +.#{$fa-css-prefix}-clone:before { + content: $fa-var-clone; +} +.#{$fa-css-prefix}-balance-scale:before { + content: $fa-var-balance-scale; +} +.#{$fa-css-prefix}-hourglass-o:before { + content: $fa-var-hourglass-o; +} .#{$fa-css-prefix}-hourglass-1:before, -.#{$fa-css-prefix}-hourglass-start:before { content: $fa-var-hourglass-start; } +.#{$fa-css-prefix}-hourglass-start:before { + content: $fa-var-hourglass-start; +} .#{$fa-css-prefix}-hourglass-2:before, -.#{$fa-css-prefix}-hourglass-half:before { content: $fa-var-hourglass-half; } +.#{$fa-css-prefix}-hourglass-half:before { + content: $fa-var-hourglass-half; +} .#{$fa-css-prefix}-hourglass-3:before, -.#{$fa-css-prefix}-hourglass-end:before { content: $fa-var-hourglass-end; } -.#{$fa-css-prefix}-hourglass:before { content: $fa-var-hourglass; } +.#{$fa-css-prefix}-hourglass-end:before { + content: $fa-var-hourglass-end; +} +.#{$fa-css-prefix}-hourglass:before { + content: $fa-var-hourglass; +} .#{$fa-css-prefix}-hand-grab-o:before, -.#{$fa-css-prefix}-hand-rock-o:before { content: $fa-var-hand-rock-o; } +.#{$fa-css-prefix}-hand-rock-o:before { + content: $fa-var-hand-rock-o; +} .#{$fa-css-prefix}-hand-stop-o:before, -.#{$fa-css-prefix}-hand-paper-o:before { content: $fa-var-hand-paper-o; } -.#{$fa-css-prefix}-hand-scissors-o:before { content: $fa-var-hand-scissors-o; } -.#{$fa-css-prefix}-hand-lizard-o:before { content: $fa-var-hand-lizard-o; } -.#{$fa-css-prefix}-hand-spock-o:before { content: $fa-var-hand-spock-o; } -.#{$fa-css-prefix}-hand-pointer-o:before { content: $fa-var-hand-pointer-o; } -.#{$fa-css-prefix}-hand-peace-o:before { content: $fa-var-hand-peace-o; } -.#{$fa-css-prefix}-trademark:before { content: $fa-var-trademark; } -.#{$fa-css-prefix}-registered:before { content: $fa-var-registered; } -.#{$fa-css-prefix}-creative-commons:before { content: $fa-var-creative-commons; } -.#{$fa-css-prefix}-gg:before { content: $fa-var-gg; } -.#{$fa-css-prefix}-gg-circle:before { content: $fa-var-gg-circle; } -.#{$fa-css-prefix}-tripadvisor:before { content: $fa-var-tripadvisor; } -.#{$fa-css-prefix}-odnoklassniki:before { content: $fa-var-odnoklassniki; } -.#{$fa-css-prefix}-odnoklassniki-square:before { content: $fa-var-odnoklassniki-square; } -.#{$fa-css-prefix}-get-pocket:before { content: $fa-var-get-pocket; } -.#{$fa-css-prefix}-wikipedia-w:before { content: $fa-var-wikipedia-w; } -.#{$fa-css-prefix}-safari:before { content: $fa-var-safari; } -.#{$fa-css-prefix}-chrome:before { content: $fa-var-chrome; } -.#{$fa-css-prefix}-firefox:before { content: $fa-var-firefox; } -.#{$fa-css-prefix}-opera:before { content: $fa-var-opera; } -.#{$fa-css-prefix}-internet-explorer:before { content: $fa-var-internet-explorer; } +.#{$fa-css-prefix}-hand-paper-o:before { + content: $fa-var-hand-paper-o; +} +.#{$fa-css-prefix}-hand-scissors-o:before { + content: $fa-var-hand-scissors-o; +} +.#{$fa-css-prefix}-hand-lizard-o:before { + content: $fa-var-hand-lizard-o; +} +.#{$fa-css-prefix}-hand-spock-o:before { + content: $fa-var-hand-spock-o; +} +.#{$fa-css-prefix}-hand-pointer-o:before { + content: $fa-var-hand-pointer-o; +} +.#{$fa-css-prefix}-hand-peace-o:before { + content: $fa-var-hand-peace-o; +} +.#{$fa-css-prefix}-trademark:before { + content: $fa-var-trademark; +} +.#{$fa-css-prefix}-registered:before { + content: $fa-var-registered; +} +.#{$fa-css-prefix}-creative-commons:before { + content: $fa-var-creative-commons; +} +.#{$fa-css-prefix}-gg:before { + content: $fa-var-gg; +} +.#{$fa-css-prefix}-gg-circle:before { + content: $fa-var-gg-circle; +} +.#{$fa-css-prefix}-tripadvisor:before { + content: $fa-var-tripadvisor; +} +.#{$fa-css-prefix}-odnoklassniki:before { + content: $fa-var-odnoklassniki; +} +.#{$fa-css-prefix}-odnoklassniki-square:before { + content: $fa-var-odnoklassniki-square; +} +.#{$fa-css-prefix}-get-pocket:before { + content: $fa-var-get-pocket; +} +.#{$fa-css-prefix}-wikipedia-w:before { + content: $fa-var-wikipedia-w; +} +.#{$fa-css-prefix}-safari:before { + content: $fa-var-safari; +} +.#{$fa-css-prefix}-chrome:before { + content: $fa-var-chrome; +} +.#{$fa-css-prefix}-firefox:before { + content: $fa-var-firefox; +} +.#{$fa-css-prefix}-opera:before { + content: $fa-var-opera; +} +.#{$fa-css-prefix}-internet-explorer:before { + content: $fa-var-internet-explorer; +} .#{$fa-css-prefix}-tv:before, -.#{$fa-css-prefix}-television:before { content: $fa-var-television; } -.#{$fa-css-prefix}-contao:before { content: $fa-var-contao; } -.#{$fa-css-prefix}-500px:before { content: $fa-var-500px; } -.#{$fa-css-prefix}-amazon:before { content: $fa-var-amazon; } -.#{$fa-css-prefix}-calendar-plus-o:before { content: $fa-var-calendar-plus-o; } -.#{$fa-css-prefix}-calendar-minus-o:before { content: $fa-var-calendar-minus-o; } -.#{$fa-css-prefix}-calendar-times-o:before { content: $fa-var-calendar-times-o; } -.#{$fa-css-prefix}-calendar-check-o:before { content: $fa-var-calendar-check-o; } -.#{$fa-css-prefix}-industry:before { content: $fa-var-industry; } -.#{$fa-css-prefix}-map-pin:before { content: $fa-var-map-pin; } -.#{$fa-css-prefix}-map-signs:before { content: $fa-var-map-signs; } -.#{$fa-css-prefix}-map-o:before { content: $fa-var-map-o; } -.#{$fa-css-prefix}-map:before { content: $fa-var-map; } -.#{$fa-css-prefix}-commenting:before { content: $fa-var-commenting; } -.#{$fa-css-prefix}-commenting-o:before { content: $fa-var-commenting-o; } -.#{$fa-css-prefix}-houzz:before { content: $fa-var-houzz; } -.#{$fa-css-prefix}-vimeo:before { content: $fa-var-vimeo; } -.#{$fa-css-prefix}-black-tie:before { content: $fa-var-black-tie; } -.#{$fa-css-prefix}-fonticons:before { content: $fa-var-fonticons; } -.#{$fa-css-prefix}-reddit-alien:before { content: $fa-var-reddit-alien; } -.#{$fa-css-prefix}-edge:before { content: $fa-var-edge; } -.#{$fa-css-prefix}-credit-card-alt:before { content: $fa-var-credit-card-alt; } -.#{$fa-css-prefix}-codiepie:before { content: $fa-var-codiepie; } -.#{$fa-css-prefix}-modx:before { content: $fa-var-modx; } -.#{$fa-css-prefix}-fort-awesome:before { content: $fa-var-fort-awesome; } -.#{$fa-css-prefix}-usb:before { content: $fa-var-usb; } -.#{$fa-css-prefix}-product-hunt:before { content: $fa-var-product-hunt; } -.#{$fa-css-prefix}-mixcloud:before { content: $fa-var-mixcloud; } -.#{$fa-css-prefix}-scribd:before { content: $fa-var-scribd; } -.#{$fa-css-prefix}-pause-circle:before { content: $fa-var-pause-circle; } -.#{$fa-css-prefix}-pause-circle-o:before { content: $fa-var-pause-circle-o; } -.#{$fa-css-prefix}-stop-circle:before { content: $fa-var-stop-circle; } -.#{$fa-css-prefix}-stop-circle-o:before { content: $fa-var-stop-circle-o; } -.#{$fa-css-prefix}-shopping-bag:before { content: $fa-var-shopping-bag; } -.#{$fa-css-prefix}-shopping-basket:before { content: $fa-var-shopping-basket; } -.#{$fa-css-prefix}-hashtag:before { content: $fa-var-hashtag; } -.#{$fa-css-prefix}-bluetooth:before { content: $fa-var-bluetooth; } -.#{$fa-css-prefix}-bluetooth-b:before { content: $fa-var-bluetooth-b; } -.#{$fa-css-prefix}-percent:before { content: $fa-var-percent; } -.#{$fa-css-prefix}-gitlab:before { content: $fa-var-gitlab; } -.#{$fa-css-prefix}-wpbeginner:before { content: $fa-var-wpbeginner; } -.#{$fa-css-prefix}-wpforms:before { content: $fa-var-wpforms; } -.#{$fa-css-prefix}-envira:before { content: $fa-var-envira; } -.#{$fa-css-prefix}-universal-access:before { content: $fa-var-universal-access; } -.#{$fa-css-prefix}-wheelchair-alt:before { content: $fa-var-wheelchair-alt; } -.#{$fa-css-prefix}-question-circle-o:before { content: $fa-var-question-circle-o; } -.#{$fa-css-prefix}-blind:before { content: $fa-var-blind; } -.#{$fa-css-prefix}-audio-description:before { content: $fa-var-audio-description; } -.#{$fa-css-prefix}-volume-control-phone:before { content: $fa-var-volume-control-phone; } -.#{$fa-css-prefix}-braille:before { content: $fa-var-braille; } -.#{$fa-css-prefix}-assistive-listening-systems:before { content: $fa-var-assistive-listening-systems; } +.#{$fa-css-prefix}-television:before { + content: $fa-var-television; +} +.#{$fa-css-prefix}-contao:before { + content: $fa-var-contao; +} +.#{$fa-css-prefix}-500px:before { + content: $fa-var-500px; +} +.#{$fa-css-prefix}-amazon:before { + content: $fa-var-amazon; +} +.#{$fa-css-prefix}-calendar-plus-o:before { + content: $fa-var-calendar-plus-o; +} +.#{$fa-css-prefix}-calendar-minus-o:before { + content: $fa-var-calendar-minus-o; +} +.#{$fa-css-prefix}-calendar-times-o:before { + content: $fa-var-calendar-times-o; +} +.#{$fa-css-prefix}-calendar-check-o:before { + content: $fa-var-calendar-check-o; +} +.#{$fa-css-prefix}-industry:before { + content: $fa-var-industry; +} +.#{$fa-css-prefix}-map-pin:before { + content: $fa-var-map-pin; +} +.#{$fa-css-prefix}-map-signs:before { + content: $fa-var-map-signs; +} +.#{$fa-css-prefix}-map-o:before { + content: $fa-var-map-o; +} +.#{$fa-css-prefix}-map:before { + content: $fa-var-map; +} +.#{$fa-css-prefix}-commenting:before { + content: $fa-var-commenting; +} +.#{$fa-css-prefix}-commenting-o:before { + content: $fa-var-commenting-o; +} +.#{$fa-css-prefix}-houzz:before { + content: $fa-var-houzz; +} +.#{$fa-css-prefix}-vimeo:before { + content: $fa-var-vimeo; +} +.#{$fa-css-prefix}-black-tie:before { + content: $fa-var-black-tie; +} +.#{$fa-css-prefix}-fonticons:before { + content: $fa-var-fonticons; +} +.#{$fa-css-prefix}-reddit-alien:before { + content: $fa-var-reddit-alien; +} +.#{$fa-css-prefix}-edge:before { + content: $fa-var-edge; +} +.#{$fa-css-prefix}-credit-card-alt:before { + content: $fa-var-credit-card-alt; +} +.#{$fa-css-prefix}-codiepie:before { + content: $fa-var-codiepie; +} +.#{$fa-css-prefix}-modx:before { + content: $fa-var-modx; +} +.#{$fa-css-prefix}-fort-awesome:before { + content: $fa-var-fort-awesome; +} +.#{$fa-css-prefix}-usb:before { + content: $fa-var-usb; +} +.#{$fa-css-prefix}-product-hunt:before { + content: $fa-var-product-hunt; +} +.#{$fa-css-prefix}-mixcloud:before { + content: $fa-var-mixcloud; +} +.#{$fa-css-prefix}-scribd:before { + content: $fa-var-scribd; +} +.#{$fa-css-prefix}-pause-circle:before { + content: $fa-var-pause-circle; +} +.#{$fa-css-prefix}-pause-circle-o:before { + content: $fa-var-pause-circle-o; +} +.#{$fa-css-prefix}-stop-circle:before { + content: $fa-var-stop-circle; +} +.#{$fa-css-prefix}-stop-circle-o:before { + content: $fa-var-stop-circle-o; +} +.#{$fa-css-prefix}-shopping-bag:before { + content: $fa-var-shopping-bag; +} +.#{$fa-css-prefix}-shopping-basket:before { + content: $fa-var-shopping-basket; +} +.#{$fa-css-prefix}-hashtag:before { + content: $fa-var-hashtag; +} +.#{$fa-css-prefix}-bluetooth:before { + content: $fa-var-bluetooth; +} +.#{$fa-css-prefix}-bluetooth-b:before { + content: $fa-var-bluetooth-b; +} +.#{$fa-css-prefix}-percent:before { + content: $fa-var-percent; +} +.#{$fa-css-prefix}-gitlab:before { + content: $fa-var-gitlab; +} +.#{$fa-css-prefix}-wpbeginner:before { + content: $fa-var-wpbeginner; +} +.#{$fa-css-prefix}-wpforms:before { + content: $fa-var-wpforms; +} +.#{$fa-css-prefix}-envira:before { + content: $fa-var-envira; +} +.#{$fa-css-prefix}-universal-access:before { + content: $fa-var-universal-access; +} +.#{$fa-css-prefix}-wheelchair-alt:before { + content: $fa-var-wheelchair-alt; +} +.#{$fa-css-prefix}-question-circle-o:before { + content: $fa-var-question-circle-o; +} +.#{$fa-css-prefix}-blind:before { + content: $fa-var-blind; +} +.#{$fa-css-prefix}-audio-description:before { + content: $fa-var-audio-description; +} +.#{$fa-css-prefix}-volume-control-phone:before { + content: $fa-var-volume-control-phone; +} +.#{$fa-css-prefix}-braille:before { + content: $fa-var-braille; +} +.#{$fa-css-prefix}-assistive-listening-systems:before { + content: $fa-var-assistive-listening-systems; +} .#{$fa-css-prefix}-asl-interpreting:before, -.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: $fa-var-american-sign-language-interpreting; } +.#{$fa-css-prefix}-american-sign-language-interpreting:before { + content: $fa-var-american-sign-language-interpreting; +} .#{$fa-css-prefix}-deafness:before, .#{$fa-css-prefix}-hard-of-hearing:before, -.#{$fa-css-prefix}-deaf:before { content: $fa-var-deaf; } -.#{$fa-css-prefix}-glide:before { content: $fa-var-glide; } -.#{$fa-css-prefix}-glide-g:before { content: $fa-var-glide-g; } +.#{$fa-css-prefix}-deaf:before { + content: $fa-var-deaf; +} +.#{$fa-css-prefix}-glide:before { + content: $fa-var-glide; +} +.#{$fa-css-prefix}-glide-g:before { + content: $fa-var-glide-g; +} .#{$fa-css-prefix}-signing:before, -.#{$fa-css-prefix}-sign-language:before { content: $fa-var-sign-language; } -.#{$fa-css-prefix}-low-vision:before { content: $fa-var-low-vision; } -.#{$fa-css-prefix}-viadeo:before { content: $fa-var-viadeo; } -.#{$fa-css-prefix}-viadeo-square:before { content: $fa-var-viadeo-square; } -.#{$fa-css-prefix}-snapchat:before { content: $fa-var-snapchat; } -.#{$fa-css-prefix}-snapchat-ghost:before { content: $fa-var-snapchat-ghost; } -.#{$fa-css-prefix}-snapchat-square:before { content: $fa-var-snapchat-square; } -.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; } -.#{$fa-css-prefix}-first-order:before { content: $fa-var-first-order; } -.#{$fa-css-prefix}-yoast:before { content: $fa-var-yoast; } -.#{$fa-css-prefix}-themeisle:before { content: $fa-var-themeisle; } +.#{$fa-css-prefix}-sign-language:before { + content: $fa-var-sign-language; +} +.#{$fa-css-prefix}-low-vision:before { + content: $fa-var-low-vision; +} +.#{$fa-css-prefix}-viadeo:before { + content: $fa-var-viadeo; +} +.#{$fa-css-prefix}-viadeo-square:before { + content: $fa-var-viadeo-square; +} +.#{$fa-css-prefix}-snapchat:before { + content: $fa-var-snapchat; +} +.#{$fa-css-prefix}-snapchat-ghost:before { + content: $fa-var-snapchat-ghost; +} +.#{$fa-css-prefix}-snapchat-square:before { + content: $fa-var-snapchat-square; +} +.#{$fa-css-prefix}-pied-piper:before { + content: $fa-var-pied-piper; +} +.#{$fa-css-prefix}-first-order:before { + content: $fa-var-first-order; +} +.#{$fa-css-prefix}-yoast:before { + content: $fa-var-yoast; +} +.#{$fa-css-prefix}-themeisle:before { + content: $fa-var-themeisle; +} .#{$fa-css-prefix}-google-plus-circle:before, -.#{$fa-css-prefix}-google-plus-official:before { content: $fa-var-google-plus-official; } +.#{$fa-css-prefix}-google-plus-official:before { + content: $fa-var-google-plus-official; +} .#{$fa-css-prefix}-fa:before, -.#{$fa-css-prefix}-font-awesome:before { content: $fa-var-font-awesome; } -.#{$fa-css-prefix}-handshake-o:before { content: $fa-var-handshake-o; } -.#{$fa-css-prefix}-envelope-open:before { content: $fa-var-envelope-open; } -.#{$fa-css-prefix}-envelope-open-o:before { content: $fa-var-envelope-open-o; } -.#{$fa-css-prefix}-linode:before { content: $fa-var-linode; } -.#{$fa-css-prefix}-address-book:before { content: $fa-var-address-book; } -.#{$fa-css-prefix}-address-book-o:before { content: $fa-var-address-book-o; } +.#{$fa-css-prefix}-font-awesome:before { + content: $fa-var-font-awesome; +} +.#{$fa-css-prefix}-handshake-o:before { + content: $fa-var-handshake-o; +} +.#{$fa-css-prefix}-envelope-open:before { + content: $fa-var-envelope-open; +} +.#{$fa-css-prefix}-envelope-open-o:before { + content: $fa-var-envelope-open-o; +} +.#{$fa-css-prefix}-linode:before { + content: $fa-var-linode; +} +.#{$fa-css-prefix}-address-book:before { + content: $fa-var-address-book; +} +.#{$fa-css-prefix}-address-book-o:before { + content: $fa-var-address-book-o; +} .#{$fa-css-prefix}-vcard:before, -.#{$fa-css-prefix}-address-card:before { content: $fa-var-address-card; } +.#{$fa-css-prefix}-address-card:before { + content: $fa-var-address-card; +} .#{$fa-css-prefix}-vcard-o:before, -.#{$fa-css-prefix}-address-card-o:before { content: $fa-var-address-card-o; } -.#{$fa-css-prefix}-user-circle:before { content: $fa-var-user-circle; } -.#{$fa-css-prefix}-user-circle-o:before { content: $fa-var-user-circle-o; } -.#{$fa-css-prefix}-user-o:before { content: $fa-var-user-o; } -.#{$fa-css-prefix}-id-badge:before { content: $fa-var-id-badge; } +.#{$fa-css-prefix}-address-card-o:before { + content: $fa-var-address-card-o; +} +.#{$fa-css-prefix}-user-circle:before { + content: $fa-var-user-circle; +} +.#{$fa-css-prefix}-user-circle-o:before { + content: $fa-var-user-circle-o; +} +.#{$fa-css-prefix}-user-o:before { + content: $fa-var-user-o; +} +.#{$fa-css-prefix}-id-badge:before { + content: $fa-var-id-badge; +} .#{$fa-css-prefix}-drivers-license:before, -.#{$fa-css-prefix}-id-card:before { content: $fa-var-id-card; } +.#{$fa-css-prefix}-id-card:before { + content: $fa-var-id-card; +} .#{$fa-css-prefix}-drivers-license-o:before, -.#{$fa-css-prefix}-id-card-o:before { content: $fa-var-id-card-o; } -.#{$fa-css-prefix}-quora:before { content: $fa-var-quora; } -.#{$fa-css-prefix}-free-code-camp:before { content: $fa-var-free-code-camp; } -.#{$fa-css-prefix}-telegram:before { content: $fa-var-telegram; } +.#{$fa-css-prefix}-id-card-o:before { + content: $fa-var-id-card-o; +} +.#{$fa-css-prefix}-quora:before { + content: $fa-var-quora; +} +.#{$fa-css-prefix}-free-code-camp:before { + content: $fa-var-free-code-camp; +} +.#{$fa-css-prefix}-telegram:before { + content: $fa-var-telegram; +} .#{$fa-css-prefix}-thermometer-4:before, .#{$fa-css-prefix}-thermometer:before, -.#{$fa-css-prefix}-thermometer-full:before { content: $fa-var-thermometer-full; } +.#{$fa-css-prefix}-thermometer-full:before { + content: $fa-var-thermometer-full; +} .#{$fa-css-prefix}-thermometer-3:before, -.#{$fa-css-prefix}-thermometer-three-quarters:before { content: $fa-var-thermometer-three-quarters; } +.#{$fa-css-prefix}-thermometer-three-quarters:before { + content: $fa-var-thermometer-three-quarters; +} .#{$fa-css-prefix}-thermometer-2:before, -.#{$fa-css-prefix}-thermometer-half:before { content: $fa-var-thermometer-half; } +.#{$fa-css-prefix}-thermometer-half:before { + content: $fa-var-thermometer-half; +} .#{$fa-css-prefix}-thermometer-1:before, -.#{$fa-css-prefix}-thermometer-quarter:before { content: $fa-var-thermometer-quarter; } +.#{$fa-css-prefix}-thermometer-quarter:before { + content: $fa-var-thermometer-quarter; +} .#{$fa-css-prefix}-thermometer-0:before, -.#{$fa-css-prefix}-thermometer-empty:before { content: $fa-var-thermometer-empty; } -.#{$fa-css-prefix}-shower:before { content: $fa-var-shower; } +.#{$fa-css-prefix}-thermometer-empty:before { + content: $fa-var-thermometer-empty; +} +.#{$fa-css-prefix}-shower:before { + content: $fa-var-shower; +} .#{$fa-css-prefix}-bathtub:before, .#{$fa-css-prefix}-s15:before, -.#{$fa-css-prefix}-bath:before { content: $fa-var-bath; } -.#{$fa-css-prefix}-podcast:before { content: $fa-var-podcast; } -.#{$fa-css-prefix}-window-maximize:before { content: $fa-var-window-maximize; } -.#{$fa-css-prefix}-window-minimize:before { content: $fa-var-window-minimize; } -.#{$fa-css-prefix}-window-restore:before { content: $fa-var-window-restore; } +.#{$fa-css-prefix}-bath:before { + content: $fa-var-bath; +} +.#{$fa-css-prefix}-podcast:before { + content: $fa-var-podcast; +} +.#{$fa-css-prefix}-window-maximize:before { + content: $fa-var-window-maximize; +} +.#{$fa-css-prefix}-window-minimize:before { + content: $fa-var-window-minimize; +} +.#{$fa-css-prefix}-window-restore:before { + content: $fa-var-window-restore; +} .#{$fa-css-prefix}-times-rectangle:before, -.#{$fa-css-prefix}-window-close:before { content: $fa-var-window-close; } +.#{$fa-css-prefix}-window-close:before { + content: $fa-var-window-close; +} .#{$fa-css-prefix}-times-rectangle-o:before, -.#{$fa-css-prefix}-window-close-o:before { content: $fa-var-window-close-o; } -.#{$fa-css-prefix}-bandcamp:before { content: $fa-var-bandcamp; } -.#{$fa-css-prefix}-grav:before { content: $fa-var-grav; } -.#{$fa-css-prefix}-etsy:before { content: $fa-var-etsy; } -.#{$fa-css-prefix}-imdb:before { content: $fa-var-imdb; } -.#{$fa-css-prefix}-ravelry:before { content: $fa-var-ravelry; } -.#{$fa-css-prefix}-eercast:before { content: $fa-var-eercast; } -.#{$fa-css-prefix}-microchip:before { content: $fa-var-microchip; } -.#{$fa-css-prefix}-snowflake-o:before { content: $fa-var-snowflake-o; } -.#{$fa-css-prefix}-superpowers:before { content: $fa-var-superpowers; } -.#{$fa-css-prefix}-wpexplorer:before { content: $fa-var-wpexplorer; } -.#{$fa-css-prefix}-meetup:before { content: $fa-var-meetup; } +.#{$fa-css-prefix}-window-close-o:before { + content: $fa-var-window-close-o; +} +.#{$fa-css-prefix}-bandcamp:before { + content: $fa-var-bandcamp; +} +.#{$fa-css-prefix}-grav:before { + content: $fa-var-grav; +} +.#{$fa-css-prefix}-etsy:before { + content: $fa-var-etsy; +} +.#{$fa-css-prefix}-imdb:before { + content: $fa-var-imdb; +} +.#{$fa-css-prefix}-ravelry:before { + content: $fa-var-ravelry; +} +.#{$fa-css-prefix}-eercast:before { + content: $fa-var-eercast; +} +.#{$fa-css-prefix}-microchip:before { + content: $fa-var-microchip; +} +.#{$fa-css-prefix}-snowflake-o:before { + content: $fa-var-snowflake-o; +} +.#{$fa-css-prefix}-superpowers:before { + content: $fa-var-superpowers; +} +.#{$fa-css-prefix}-wpexplorer:before { + content: $fa-var-wpexplorer; +} +.#{$fa-css-prefix}-meetup:before { + content: $fa-var-meetup; +} diff --git a/public/sass/base/font-awesome/_larger.scss b/public/sass/base/font-awesome/_larger.scss index 41e9a8184aa..5d7bebcda14 100644 --- a/public/sass/base/font-awesome/_larger.scss +++ b/public/sass/base/font-awesome/_larger.scss @@ -7,7 +7,15 @@ line-height: (3em / 4); vertical-align: -15%; } -.#{$fa-css-prefix}-2x { font-size: 2em; } -.#{$fa-css-prefix}-3x { font-size: 3em; } -.#{$fa-css-prefix}-4x { font-size: 4em; } -.#{$fa-css-prefix}-5x { font-size: 5em; } +.#{$fa-css-prefix}-2x { + font-size: 2em !important; +} +.#{$fa-css-prefix}-3x { + font-size: 3em; +} +.#{$fa-css-prefix}-4x { + font-size: 4em; +} +.#{$fa-css-prefix}-5x { + font-size: 5em; +} diff --git a/public/sass/base/font-awesome/_list.scss b/public/sass/base/font-awesome/_list.scss index 7d1e4d54d6c..9e30bee7447 100644 --- a/public/sass/base/font-awesome/_list.scss +++ b/public/sass/base/font-awesome/_list.scss @@ -5,7 +5,9 @@ padding-left: 0; margin-left: $fa-li-width; list-style-type: none; - > li { position: relative; } + > li { + position: relative; + } } .#{$fa-css-prefix}-li { position: absolute; diff --git a/public/sass/base/font-awesome/_mixins.scss b/public/sass/base/font-awesome/_mixins.scss index c3bbd5745d3..19771009107 100644 --- a/public/sass/base/font-awesome/_mixins.scss +++ b/public/sass/base/font-awesome/_mixins.scss @@ -3,29 +3,28 @@ @mixin fa-icon() { display: inline-block; - font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration + font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} + FontAwesome; // shortening font declaration font-size: inherit; // can't have font-size inherit on line above, so need to override text-rendering: auto; // optimizelegibility throws things off #1094 -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - } @mixin fa-icon-rotate($degrees, $rotation) { -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; -webkit-transform: rotate($degrees); - -ms-transform: rotate($degrees); - transform: rotate($degrees); + -ms-transform: rotate($degrees); + transform: rotate($degrees); } @mixin fa-icon-flip($horiz, $vert, $rotation) { -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; -webkit-transform: scale($horiz, $vert); - -ms-transform: scale($horiz, $vert); - transform: scale($horiz, $vert); + -ms-transform: scale($horiz, $vert); + transform: scale($horiz, $vert); } - // Only display content to screen readers. A la Bootstrap 4. // // See: http://a11yproject.com/posts/how-to-hide-content/ @@ -37,7 +36,7 @@ padding: 0; margin: -1px; overflow: hidden; - clip: rect(0,0,0,0); + clip: rect(0, 0, 0, 0); border: 0; } diff --git a/public/sass/base/font-awesome/_path.scss b/public/sass/base/font-awesome/_path.scss index bb457c23a8e..0316afa161d 100644 --- a/public/sass/base/font-awesome/_path.scss +++ b/public/sass/base/font-awesome/_path.scss @@ -2,14 +2,19 @@ * -------------------------- */ @font-face { - font-family: 'FontAwesome'; - src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); - src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), - url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), - url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), - url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), - url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); -// src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts + font-family: "FontAwesome"; + src: url("#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}"); + src: url("#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}") + format("embedded-opentype"), + url("#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}") + format("woff2"), + url("#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}") + format("woff"), + url("#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}") + format("truetype"), + url("#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular") + format("svg"); + // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts font-weight: normal; font-style: normal; } diff --git a/public/sass/base/font-awesome/_rotated-flipped.scss b/public/sass/base/font-awesome/_rotated-flipped.scss index a3558fd09ca..07c11a8b299 100644 --- a/public/sass/base/font-awesome/_rotated-flipped.scss +++ b/public/sass/base/font-awesome/_rotated-flipped.scss @@ -1,12 +1,22 @@ // Rotated & Flipped Icons // ------------------------- -.#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } -.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } -.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } +.#{$fa-css-prefix}-rotate-90 { + @include fa-icon-rotate(90deg, 1); +} +.#{$fa-css-prefix}-rotate-180 { + @include fa-icon-rotate(180deg, 2); +} +.#{$fa-css-prefix}-rotate-270 { + @include fa-icon-rotate(270deg, 3); +} -.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } -.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } +.#{$fa-css-prefix}-flip-horizontal { + @include fa-icon-flip(-1, 1, 0); +} +.#{$fa-css-prefix}-flip-vertical { + @include fa-icon-flip(1, -1, 2); +} // Hook for IE8-9 // ------------------------- diff --git a/public/sass/base/font-awesome/_screen-reader.scss b/public/sass/base/font-awesome/_screen-reader.scss index 637426f0da6..e3bff899ed9 100644 --- a/public/sass/base/font-awesome/_screen-reader.scss +++ b/public/sass/base/font-awesome/_screen-reader.scss @@ -1,5 +1,9 @@ // Screen Readers // ------------------------- -.sr-only { @include sr-only(); } -.sr-only-focusable { @include sr-only-focusable(); } +.sr-only { + @include sr-only(); +} +.sr-only-focusable { + @include sr-only-focusable(); +} diff --git a/public/sass/base/font-awesome/_stacked.scss b/public/sass/base/font-awesome/_stacked.scss index aef7403660c..33fbe76979c 100644 --- a/public/sass/base/font-awesome/_stacked.scss +++ b/public/sass/base/font-awesome/_stacked.scss @@ -9,12 +9,19 @@ line-height: 2em; vertical-align: middle; } -.#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { +.#{$fa-css-prefix}-stack-1x, +.#{$fa-css-prefix}-stack-2x { position: absolute; left: 0; width: 100%; text-align: center; } -.#{$fa-css-prefix}-stack-1x { line-height: inherit; } -.#{$fa-css-prefix}-stack-2x { font-size: 2em; } -.#{$fa-css-prefix}-inverse { color: $fa-inverse; } +.#{$fa-css-prefix}-stack-1x { + line-height: inherit; +} +.#{$fa-css-prefix}-stack-2x { + font-size: 2em; +} +.#{$fa-css-prefix}-inverse { + color: $fa-inverse; +} diff --git a/public/sass/base/font-awesome/_variables.scss b/public/sass/base/font-awesome/_variables.scss index 498fc4a087c..a4bd3cd87b9 100644 --- a/public/sass/base/font-awesome/_variables.scss +++ b/public/sass/base/font-awesome/_variables.scss @@ -1,15 +1,15 @@ // Variables // -------------------------- -$fa-font-path: "../fonts" !default; -$fa-font-size-base: 14px !default; +$fa-font-path: "../fonts" !default; +$fa-font-size-base: 14px !default; $fa-line-height-base: 1 !default; //$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts" !default; // for referencing Bootstrap CDN font files directly -$fa-css-prefix: fa !default; -$fa-version: "4.7.0" !default; -$fa-border-color: #eee !default; -$fa-inverse: #fff !default; -$fa-li-width: (30em / 14) !default; +$fa-css-prefix: fa !default; +$fa-version: "4.7.0" !default; +$fa-border-color: #eee !default; +$fa-inverse: #fff !default; +$fa-li-width: (30em / 14) !default; $fa-var-500px: "\f26e"; $fa-var-address-book: "\f2b9"; @@ -797,4 +797,3 @@ $fa-var-yoast: "\f2b1"; $fa-var-youtube: "\f167"; $fa-var-youtube-play: "\f16a"; $fa-var-youtube-square: "\f166"; - diff --git a/public/sass/components/_alerts.scss b/public/sass/components/_alerts.scss index afc83a1024d..3420dcfdfaf 100644 --- a/public/sass/components/_alerts.scss +++ b/public/sass/components/_alerts.scss @@ -2,18 +2,17 @@ // Alerts // -------------------------------------------------- - // Base styles // ------------------------- .alert { padding: 1.25rem 2rem 1.25rem 1.5rem; margin-bottom: $line-height-base; - text-shadow: 0 2px 0 rgba(255,255,255,.5); + text-shadow: 0 2px 0 rgba(255, 255, 255, 0.5); background: $alert-error-bg; position: relative; color: $white; - text-shadow: 0 1px 0 rgba(0,0,0,.2); + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); border-radius: 2px; display: flex; flex-direction: row; @@ -57,7 +56,7 @@ .fa { align-self: flex-end; font-size: 1.5rem; - color: rgba(255,255,255,.75) + color: rgba(255, 255, 255, 0.75); } } diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index 4be896d8ec0..4c9b197c3d0 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -14,7 +14,7 @@ text-align: center; vertical-align: middle; cursor: pointer; - border: $input-btn-border-width solid transparent; + border: none; @include button-size($btn-padding-y, $btn-padding-x, $font-size-base, $btn-border-radius); @@ -44,17 +44,28 @@ &[disabled], &:disabled { cursor: $cursor-disabled; - opacity: .65; + opacity: 0.65; @include box-shadow(none); } } // Button Sizes // -------------------------------------------------- +// XLarge +.btn-xlarge { + @include button-size($btn-padding-y-xl, $btn-padding-x-xl, $font-size-lg, $btn-border-radius); + font-weight: normal; + padding-bottom: $btn-padding-y-xl - 3; + .gicon { + font-size: 31px; + margin-right: 1rem; + } +} // Large .btn-large { @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $font-size-lg, $btn-border-radius); + font-weight: normal; } .btn-small { @@ -81,6 +92,7 @@ .btn-warning { @include buttonBackground($btn-warning-bg, $btn-warning-bg-hl); } + // Danger and error appear as red .btn-danger { @include buttonBackground($btn-danger-bg, $btn-danger-bg-hl); @@ -95,7 +107,7 @@ } // Inverse appears as dark gray .btn-inverse { - @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color); + @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow); //background: $card-background; box-shadow: $card-shadow; //border: 1px solid $tight-form-func-highlight-bg; @@ -124,11 +136,67 @@ @include box-shadow(none); cursor: default; - &:hover, &:active, &:active:hover, &:focus { + &:hover, + &:active, + &:active:hover, + &:focus { color: $gray-1; background-color: transparent; - border-color: $gray-1; + border-color: $gray-1; } } +// Extra padding +.btn-p-x-2 { + padding-left: 20px; + padding-right: 20px; +} +// External services +// Usage: +// + +$btn-service-icon-width: 35px; +.btn-service { + position: relative; +} + +@each $service, $data in $external-services { + $serviceBgColor: map-get($data, bgColor); + $serviceBorderColor: map-get($data, borderColor); + + .btn-service--#{$service} { + background-color: $serviceBgColor; + border: 1px solid $serviceBorderColor; + + .btn-service-icon { + font-size: 24px; // Override + border-right: 1px solid $serviceBorderColor; + } + } +} + +.btn-service-icon { + position: absolute; + left: 0; + height: 100%; + top: 0; + padding-left: 0.5rem; + padding-right: 0.5rem; + width: $btn-service-icon-width; + text-align: center; + + &::before { + position: relative; + top: 4px; + } +} + +.btn-service--grafanacom { + .btn-service-icon { + background-image: url(/public/img/grafana_mask_icon_white.svg); + background-repeat: no-repeat; + background-position: 50%; + background-size: 60%; + } +} diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss index 6dba94494b1..11a8abb7640 100644 --- a/public/sass/components/_cards.scss +++ b/public/sass/components/_cards.scss @@ -1,10 +1,8 @@ .layout-selector { @include clearfix(); + margin-left: $spacer; text-align: right; - margin: 0 $spacer*1.5 $spacer; - position: relative; - top: -0.5rem; button { background: $input-label-bg; @@ -27,7 +25,7 @@ &:nth-child(2) { border-radius: 3px 0 0 3px; - border-right: 1px solid $panel-border; + border-right: $panel-border; } &:nth-child(1) { @@ -97,7 +95,7 @@ color: $text-color-weak; text-transform: uppercase; font-size: $font-size-sm; - font-weight: bold; + font-weight: $font-weight-semi-bold; } .card-item-notice { @@ -127,7 +125,6 @@ } .card-list-layout-grid { - .card-item-type { display: inline-block; } @@ -144,7 +141,7 @@ .card-item-wrapper { width: 100%; - padding: 0 1.5rem 1.5rem 0rem; + padding: 0 1rem 1rem 0rem; } .card-item-wrapper--clickable { @@ -158,6 +155,11 @@ img { width: 6rem; } + .fa, + .icon-gf, + .gicon { + font-size: 3.5rem; + } } .card-item-name { @@ -186,7 +188,6 @@ } .card-list-layout-list { - .card-item-wrapper { padding: 0; width: 100%; @@ -197,9 +198,8 @@ } .card-item { - border-bottom: .2rem solid $page-bg; - border-radius: 0; - box-shadow: none; + border-bottom: 3px solid $page-bg; + border-radius: 2px; } .card-item-header { diff --git a/public/sass/components/_code_editor.scss b/public/sass/components/_code_editor.scss index c9a6e94cf9c..79203e7898c 100644 --- a/public/sass/components/_code_editor.scss +++ b/public/sass/components/_code_editor.scss @@ -1,5 +1,5 @@ .gf-code-editor { - min-height: 2.60rem; + min-height: 2.6rem; min-width: 20rem; flex-grow: 1; margin-right: 0.25rem; @@ -7,7 +7,7 @@ &.ace_editor { @include font-family-monospace(); font-size: 1rem; - min-height: 2.60rem; + min-height: 2.6rem; @include border-radius($input-border-radius-sm); border: $input-btn-border-width solid $input-border-color; @@ -26,7 +26,9 @@ width: 550px !important; .ace_scroller { - .ace_selected, .ace_active-line, .ace_line-hover { + .ace_selected, + .ace_active-line, + .ace_line-hover { color: $dropdownLinkColorHover; background-color: $dropdownLinkBackgroundHover !important; } diff --git a/public/sass/components/_color_picker.scss b/public/sass/components/_color_picker.scss index 22c96398160..c0643342307 100644 --- a/public/sass/components/_color_picker.scss +++ b/public/sass/components/_color_picker.scss @@ -1,4 +1,3 @@ - .sp-replacer { background: inherit; border: none; @@ -6,7 +5,8 @@ padding: 0; } -.sp-replacer:hover, .sp-replacer.sp-active { +.sp-replacer:hover, +.sp-replacer.sp-active { border-color: inherit; color: inherit; } @@ -18,7 +18,8 @@ padding: 0; } -.sp-palette-container, .sp-picker-container { +.sp-palette-container, +.sp-picker-container { border: none; } diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss new file mode 100644 index 00000000000..b44f1fa55ce --- /dev/null +++ b/public/sass/components/_dashboard_grid.scss @@ -0,0 +1,62 @@ +@import "~react-grid-layout/css/styles.css"; +@import "~react-resizable/css/styles.css"; + +.panel-in-fullscreen { + .react-grid-layout { + height: 100% !important; + } + + .react-grid-item { + display: none !important; + transition-property: none !important; + } + + .panel--fullscreen { + display: block !important; + position: unset !important; + width: 100% !important; + height: 100% !important; + transform: translate(0px, 0px) !important; + } +} + +@include media-breakpoint-down(sm) { + .react-grid-layout { + height: 100% !important; + } + + .react-grid-item { + display: none; + transition-property: none !important; + } + + .panel { + display: block !important; + position: unset !important; + width: 100% !important; + transform: translate(0px, 0px) !important; + margin-bottom: 1rem; + } +} + +.theme-dark { + .react-grid-item > .react-resizable-handle { + background-image: url("../img/resize-handle-white.svg"); + } +} + +// Hack for preventing panel menu overlapping. +.react-grid-item.resizing.panel, +.react-grid-item.panel.dropdown-menu-open, +.react-grid-item.react-draggable-dragging.panel { + z-index: $zindex-dropdown; +} + +// Disable animation on initial rendering and enable it when component has been mounted. +.react-grid-item.cssTransforms.panel { + transition-property: none; +} + +.animated .react-grid-item.cssTransforms.panel { + transition-property: transform; +} diff --git a/public/sass/components/_dashboard_list.scss b/public/sass/components/_dashboard_list.scss new file mode 100644 index 00000000000..51603862db7 --- /dev/null +++ b/public/sass/components/_dashboard_list.scss @@ -0,0 +1,32 @@ +.dashboard-list { + .search-results-container { + padding: 5px 0 0 0; + } +} + +.search-results { + margin-top: 2rem; +} + +.search-results-filter-row { + height: 35px; + display: flex; + justify-content: space-between; + + .gf-form-button-row { + padding-top: 0; + + button:last-child { + margin-right: 0; + } + } +} + +.search-results-filter-row__filters { + display: flex; +} + +.search-results-filter-row__filters-item { + width: 150px; + margin-right: 0; +} diff --git a/public/sass/components/_dashboard_settings.scss b/public/sass/components/_dashboard_settings.scss new file mode 100644 index 00000000000..af09ca3658a --- /dev/null +++ b/public/sass/components/_dashboard_settings.scss @@ -0,0 +1,81 @@ +.dashboard-settings { + opacity: 0; + height: 100%; + display: flex; + flex-direction: row; +} + +.dashboard-page--settings-opening { + .dashboard-container { + display: none; + } +} + +.dashboard-page--settings-open { + .dashboard-settings { + opacity: 1; + transition: opacity 300ms ease-in-out; + } +} + +.dashboard-settings__content { + flex-grow: 1; + min-width: 0; + height: 100%; + padding: 30px; +} + +.dashboard-settings__aside { + padding: 30px 0 0 30px; + background: $panel-bg; + display: flex; + flex-direction: column; +} + +.dashboard-settings__aside-header { + color: $text-muted; + font-size: $font-size-h3; + padding-right: 60px; + white-space: nowrap; + margin-bottom: $spacer; + + i { + font-size: 25px; + position: relative; + top: 1px; + padding-right: 5px; + } +} + +.dashboard-settings__header { + font-size: $font-size-h3; + margin-bottom: $spacer*2; +} + +.dashboard-settings__nav-item { + padding: 7px 12px; + color: $text-color; + font-size: $font-size-md; + @include left-brand-border; + + &.active { + @include left-brand-border-gradient(); + background: $page-bg; + } + + i { + padding-right: 5px; + } +} + +.dashboard-settings__aside-actions { + display: flex; + flex-direction: column; + height: 100%; + flex-grow: 1; + margin: $spacer*3 $spacer*2 0 0; + + button { + margin-bottom: 10px; + } +} diff --git a/public/sass/components/_drop.scss b/public/sass/components/_drop.scss index c1441bd31cb..6568414ed88 100644 --- a/public/sass/components/_drop.scss +++ b/public/sass/components/_drop.scss @@ -3,7 +3,7 @@ $color: inherit; $color: $text-color; $useDropShadow: false; $attachmentOffset: 0%; -$easing: cubic-bezier(0, 0, 0.265, 1.00); +$easing: cubic-bezier(0, 0, 0.265, 1); @include drop-theme("error", $popover-error-bg, $popover-color); @include drop-theme("popover", $popover-bg, $popover-color, $popover-border-color); @@ -22,9 +22,10 @@ $easing: cubic-bezier(0, 0, 0.265, 1.00); &.drop-open { display: block; } - } +} -.drop-element, .drop-element * { +.drop-element, +.drop-element * { box-sizing: border-box; } diff --git a/public/sass/components/_dropdown.scss b/public/sass/components/_dropdown.scss index 6031c05db3f..1bdda9a0b6e 100644 --- a/public/sass/components/_dropdown.scss +++ b/public/sass/components/_dropdown.scss @@ -2,7 +2,6 @@ // Dropdown menus // -------------------------------------------------- - // Use the .menu class on any
  • element within the topbar or ul.tabs and you'll get some superfancy dropdowns .dropup, .dropdown { @@ -18,7 +17,7 @@ position: relative; top: -3px; width: 250px; - font-size: 80%; + font-size: $font-size-sm; margin-left: 22px; color: $gray-2; white-space: normal; @@ -31,9 +30,9 @@ width: 0; height: 0; vertical-align: top; - border-top: 4px solid $black; + border-top: 4px solid $text-color-weak; border-right: 4px solid transparent; - border-left: 4px solid transparent; + border-left: 4px solid transparent; content: ""; } @@ -58,6 +57,7 @@ background-color: $dropdownBackground; border: 1px solid #ccc; // Fallback for IE7-8 border: 1px solid $dropdownBorder; + text-align: left; // Aligns the dropdown menu to right &.pull-right { @@ -85,11 +85,56 @@ white-space: nowrap; i { - padding-right: 5px; + display: inline-block; + margin-right: 10px; color: $link-color-disabled; + position: relative; + top: 3px; + } + + .gicon { + opacity: 0.9; } } } + + &--navbar { + top: 100%; + min-width: 100%; + } + + &--menu, + &--navbar, + &--sidemenu { + background: $menu-dropdown-bg; + box-shadow: $menu-dropdown-shadow; + margin-top: 0px; + border: none; + + > li > a { + display: flex; + padding: 5px 10px; + border-left: 2px solid transparent; + + &:hover { + @include left-brand-border-gradient(); + color: $link-hover-color; + background: $menu-dropdown-hover-bg !important; + } + } + } + + &--sidemenu { + li.sidemenu-org-switcher { + > a { + padding: 8px 10px 8px 15px; + } + } + } +} + +.dropdown-item-text { + flex-grow: 1; } // Hover/Focus state @@ -178,7 +223,7 @@ // Different positioning for bottom up menu .dropdown-menu { top: auto; - bottom: 100%; + bottom: 0; margin-bottom: 1px; } } @@ -192,7 +237,7 @@ .dropdown-submenu > .dropdown-menu { top: 0; left: 100%; - margin-top: -6px; + margin-top: 0px; margin-left: -1px; @include border-radius(0 6px 6px 6px); } @@ -219,9 +264,9 @@ border-color: transparent; border-style: solid; border-width: 5px 0 5px 5px; - border-left-color: darken($dropdownBackground, 20%); + border-left-color: $text-color-weak; margin-top: 5px; - margin-right: -10px; + margin-right: -4px; } .dropdown-submenu:hover > a::after { border-left-color: $dropdownLinkColorHover; @@ -252,20 +297,15 @@ // Typeahead // --------- .typeahead { - z-index: 1051; + z-index: $zindex-typeahead; margin-top: 2px; // give it some space to breathe } -.dropdown-menu-item-with-shortcut { - a { - min-width: 12rem; - } -} - .dropdown-menu-item-shortcut { display: block; - float: right; + margin-left: $spacer; color: $text-muted; + min-width: 47px; &::before { font-family: FontAwesome; @@ -275,3 +315,22 @@ content: "\f11c"; } } + +.dropdown-menu.dropdown-menu--new { + li a { + padding: $spacer/2 $spacer; + border-left: 2px solid $side-menu-bg; + background: $side-menu-bg; + + i { + display: inline-block; + padding-right: 21px; + } + + &:hover { + @include left-brand-border-gradient(); + color: $link-hover-color; + background: $input-label-bg; + } + } +} diff --git a/public/sass/components/_empty_list_cta.scss b/public/sass/components/_empty_list_cta.scss new file mode 100644 index 00000000000..dd50f3a3b8b --- /dev/null +++ b/public/sass/components/_empty_list_cta.scss @@ -0,0 +1,24 @@ +.empty-list-cta { + background-color: $search-filter-box-bg; + text-align: center; + padding: $spacer*2; + border-radius: $border-radius; + + .grafana-info-box { + max-width: 700px; + margin: 0 auto; + } +} + +.empty-list-cta__title { + padding-bottom: $spacer*3; + font-style: italic; +} + +.empty-list-cta__button { + margin-bottom: $spacer*3; +} + +.empty-list-cta__pro-tip-link { + margin-left: 5px; +} diff --git a/public/sass/components/_filter-controls.scss b/public/sass/components/_filter-controls.scss index 287bebe3757..f95e54b6abe 100644 --- a/public/sass/components/_filter-controls.scss +++ b/public/sass/components/_filter-controls.scss @@ -2,8 +2,6 @@ // FILTER CONTROLS // ========================================================================== - - // Filters // -------------------------------------------------------------------------- @@ -16,8 +14,6 @@ margin-top: 20px; } - - // Actions // -------------------------------------------------------------------------- diff --git a/public/sass/components/_filter-list.scss b/public/sass/components/_filter-list.scss index 045f4115dbc..7713aa05ac2 100644 --- a/public/sass/components/_filter-list.scss +++ b/public/sass/components/_filter-list.scss @@ -2,8 +2,6 @@ // FILTER LIST // ========================================================================== - - // List // -------------------------------------------------------------------------- @@ -23,8 +21,6 @@ } } - - // Card // -------------------------------------------------------------------------- @@ -57,7 +53,6 @@ padding: 5px 50px 5px 5px; } - .filter-list-card-status { color: #777; font-size: 12px; @@ -72,17 +67,17 @@ text-transform: uppercase; &.online { - background-image: url('/img/online.svg'); + background-image: url("/img/online.svg"); color: $online; } &.warn { - background-image: url('/img/warn-tiny.svg'); + background-image: url("/img/warn-tiny.svg"); color: $warn; } &.critical { - background-image: url('/img/critical.svg'); + background-image: url("/img/critical.svg"); color: $critical; } } diff --git a/public/sass/components/_filter-table.scss b/public/sass/components/_filter-table.scss index 65ca43cc6a9..f8b3763ebca 100644 --- a/public/sass/components/_filter-table.scss +++ b/public/sass/components/_filter-table.scss @@ -2,8 +2,6 @@ // FILTER TABLE // ========================================================================== - - // Table // -------------------------------------------------------------------------- @@ -13,44 +11,77 @@ .filter-table { width: 100%; - border-collapse: collapse; - // table-layout: fixed; -} + border-collapse: separate; -.filter-table tr { - background: $grafanaListBackground; - border-bottom: 3px solid $page-bg; -} + tbody { + tr:nth-child(odd) { + background: $table-bg-odd; + } + } -.filter-table th { - width: auto; - padding: $table-cell-padding; - text-align: left; -} + th { + width: auto; + padding: $table-cell-padding; + text-align: left; + line-height: 30px; + height: 30px; + white-space: nowrap; + } -.filter-table td { - padding: $table-cell-padding; + td { + padding: $table-cell-padding; + line-height: 30px; + height: 30px; + white-space: nowrap; - &.filter-table__switch-cell { + &.filter-table__switch-cell { + padding: 0; + border-right: 3px solid $page-bg; + } + } + + .link-td { padding: 0; - border-right: 3px solid $page-bg; + line-height: 30px; + height: 30px; + white-space: nowrap; + + &.filter-table__switch-cell { + padding: 0; + border-right: 3px solid $page-bg; + } + + a { + display: block; + padding: $table-cell-padding; + } + } + + .ellipsis { + display: block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .expanded { + border-color: $panel-bg; + } + + .expanded > td { + padding-bottom: 0; + } + + .filter-table__avatar { + width: 25px; + height: 25px; + border-radius: 50%; + } + + &--hover { + tbody tr:hover { + background: $dark-3; + } } } - -.filter-table .ellipsis { - display: block; - width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.filter-table .expanded { - border-color: $panel-bg; -} - -.filter-table .expanded > td { - padding-bottom: 0; -} - - diff --git a/public/sass/components/_footer.scss b/public/sass/components/_footer.scss index 79c0e0178df..8b7d64e47fe 100644 --- a/public/sass/components/_footer.scss +++ b/public/sass/components/_footer.scss @@ -1,12 +1,13 @@ .page-dashboard .footer { - display: none; + display: none; } .footer { color: $footer-link-color; padding: 5rem 0 1rem 0; - font-size: $font-size-xs; - width: 98%; /* was causing horiz scrollbars - need to examine */ + font-size: $font-size-sm; + position: relative; + width: 98%; /* was causing horiz scrollbars - need to examine */ a { color: $footer-link-color; @@ -24,7 +25,7 @@ display: inline-block; padding-right: 2px; &::after { - content: ' | '; + content: " | "; padding-left: 2px; } } @@ -32,7 +33,14 @@ li:last-child { &::after { padding-left: 0; - content: ''; + content: ""; } } } + +.login-page { + .footer { + position: absolute; + bottom: $spacer; + } +} diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index f11ddc3adcf..6058e10dab3 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -1,4 +1,5 @@ -$gf-form-margin: 0.25rem; +$gf-form-margin: 3px; +$input-border: 1px solid $input-border-color; .gf-form { margin-bottom: $gf-form-margin; @@ -7,7 +8,6 @@ $gf-form-margin: 0.25rem; align-items: center; text-align: left; position: relative; - font-size: $font-size-sm; &--offset-1 { margin-left: $spacer; @@ -20,12 +20,45 @@ $gf-form-margin: 0.25rem; &--flex-end { justify-content: flex-end; } + + &--alt { + flex-direction: column; + align-items: flex-start; + + .gf-form-label { + padding: 4px 0; + } + } +} + +.gf-form--has-input-icon { + position: relative; + + .gf-form-input-icon { + position: absolute; + top: 50%; + margin-top: -9px; + font-size: $font-size-lg; + left: 10px; + color: $input-color-placeholder; + } + + > input { + padding-left: 35px; + + &:focus + .gf-form-input-icon { + color: $text-muted; + } + } } .gf-form-disabled { color: $text-color-weak; - .query-keyword, + .gf-form-select-wrapper::after { + color: $text-color-weak; + } + a, .gf-form-input { color: $text-color-weak; @@ -41,45 +74,58 @@ $gf-form-margin: 0.25rem; flex-direction: row; flex-wrap: wrap; align-content: flex-start; + + .gf-form + .gf-form { + margin-right: $gf-form-margin; + } } .gf-form-button-row { padding-top: $spacer * 1.5; - a, button { + a, + button { margin-right: $spacer; } } .gf-form-label { padding: $input-padding-y $input-padding-x; - margin-right: $gf-form-margin; flex-shrink: 0; + font-weight: $font-weight-semi-bold; + font-size: $font-size-sm; background-color: $input-label-bg; display: block; - font-size: $font-size-sm; - - border: $input-btn-border-width solid transparent; - @include border-radius($label-border-radius-sm); + border: $input-btn-border-width solid $input-label-border-color; + border-right: none; + border-radius: $label-border-radius; &--grow { flex-grow: 1; - min-height: 2.60rem; + min-height: 2.6rem; } &--error { color: $critical; } + + &:disabled { + color: $text-color-weak; + } +} + +.gf-form-label + .gf-form-label { + margin-right: $gf-form-margin; } .gf-form-pre { display: block; flex-grow: 1; - font-size: $font-size-sm; margin: 0; margin-right: $gf-form-margin; border: $input-btn-border-width solid transparent; + border-left: none; @include border-radius($label-border-radius-sm); } @@ -105,14 +151,14 @@ $gf-form-margin: 0.25rem; width: 100%; padding: $input-padding-y $input-padding-x; margin-right: $gf-form-margin; - font-size: $font-size-base; + font-size: $font-size-md; line-height: $input-line-height; color: $input-color; background-color: $input-bg; background-image: none; background-clip: padding-box; - border: $input-btn-border-width solid $input-border-color; - @include border-radius($input-border-radius-sm); + border: $input-border; + border-radius: $input-border-radius; @include box-shadow($input-box-shadow); white-space: nowrap; overflow: hidden; @@ -156,19 +202,22 @@ $gf-form-margin: 0.25rem; cursor: $cursor-disabled; } - &.gf-size-auto { width: auto; } + &.gf-size-auto { + width: auto; + } &--dropdown { padding-right: $input-padding-x*2; &::after { position: absolute; - top: 35%; - right: $input-padding-x; + top: 36%; + right: 11px; + font-size: 11px; background-color: transparent; - color: $input-color; + color: $text-color; font: normal normal normal $font-size-sm/1 FontAwesome; - content: '\f0d7'; + content: "\f0d7"; pointer-events: none; } } @@ -178,6 +227,10 @@ $gf-form-margin: 0.25rem; padding-bottom: 4px; font-size: $font-size-sm; } + + &--plaintext { + white-space: unset; + } } .gf-form-hint { @@ -195,12 +248,30 @@ $gf-form-margin: 0.25rem; position: relative; background-color: $input-bg; + .gf-form-select-icon { + position: absolute; + z-index: 1; + left: $input-padding-x; + top: 50%; + margin-top: -7px; + + & + .gf-form-input { + position: relative; + z-index: 2; + padding-left: $input-padding-x*3; + background-color: transparent; + + option { + // Firefox + color: $black; + } + } + } + select.gf-form-input { - text-indent: .01px; - text-overflow: ''; - padding-right: $input-padding-x*2; - -webkit-appearance: none; - -moz-appearance: menulist-text; // was set to "window" and caused odd display on windos and linux. + text-indent: 0.01px; + text-overflow: ""; + padding-right: $input-padding-x*3; appearance: none; &:-moz-focusring { @@ -216,13 +287,14 @@ $gf-form-margin: 0.25rem; &::after { position: absolute; - top: 35%; - right: $input-padding-x/2; + top: 36%; + right: 11px; background-color: transparent; - color: $input-color; + color: $text-color; font: normal normal normal $font-size-sm/1 FontAwesome; - content: '\f0d7'; + content: "\f0d7"; pointer-events: none; + font-size: 11px; } &--has-help-icon { @@ -241,8 +313,6 @@ $gf-form-margin: 0.25rem; margin-right: $gf-form-margin; line-height: $input-line-height; font-size: $font-size-sm; - box-shadow: none; - border: $input-btn-border-width solid transparent; flex-shrink: 0; flex-grow: 0; @@ -262,6 +332,8 @@ $gf-form-margin: 0.25rem; position: relative; background-color: $input-bg; padding-right: $input-padding-x; + border: $input-border; + border-radius: $input-border-radius; &::after { position: absolute; @@ -270,9 +342,13 @@ $gf-form-margin: 0.25rem; background-color: transparent; color: $input-color; font: normal normal normal $font-size-sm/1 FontAwesome; - content: '\f0d7'; + content: "\f0d7"; pointer-events: none; } + + .gf-form-input { + border: none; + } } .gf-form-help-icon { @@ -296,7 +372,7 @@ $gf-form-margin: 0.25rem; } &--header { - margin-bottom: $gf-form-margin + margin-bottom: $gf-form-margin; } } diff --git a/public/sass/components/_icon-picker.scss b/public/sass/components/_icon-picker.scss deleted file mode 100644 index 796f3f95db5..00000000000 --- a/public/sass/components/_icon-picker.scss +++ /dev/null @@ -1,26 +0,0 @@ -.gf-icon-picker { - width: 400px; - height: 450px; - - .icon-filter { - padding-bottom: 10px; - margin: auto; - width: 50%; - } - - .icon-container { - max-height: 350px; - overflow: auto; - - .gf-event-icon { - margin: 0.4rem; - height: 1.5rem; - } - } -} - -.gf-icon-picker-button { - .gf-event-icon { - height: 1.2rem; - } -} diff --git a/public/sass/components/_infobox.scss b/public/sass/components/_infobox.scss index 0d3cbcd52b8..9a6a2e78a4f 100644 --- a/public/sass/components/_infobox.scss +++ b/public/sass/components/_infobox.scss @@ -1,13 +1,3 @@ -// .grafana-info-box::before { -// content: "\f05a"; -// font-family:'FontAwesome'; -// position: absolute; -// top: -13px; -// left: -8px; -// font-size: 20px; -// color: $text-color; -// } - .grafana-info-box { position: relative; background: $info-box-background; @@ -17,6 +7,10 @@ margin-bottom: $spacer; margin-right: $gf-form-margin; flex-grow: 1; + color: $info-box-color; + h5 { + color: $info-box-color; + } h5 { margin-bottom: $spacer; diff --git a/public/sass/components/_json_explorer.scss b/public/sass/components/_json_explorer.scss index 37c610b2002..2b1be8bd4f5 100644 --- a/public/sass/components/_json_explorer.scss +++ b/public/sass/components/_json_explorer.scss @@ -1,8 +1,9 @@ - .json-formatter-row { font-family: monospace; - &, a, a:hover { + &, + a, + a:hover { color: $json-explorer-default-color; text-decoration: none; } @@ -16,9 +17,15 @@ opacity: 0.5; margin-left: 1rem; - &::after { display: none; } - &.json-formatter-object::after { content: "No properties"; } - &.json-formatter-array::after { content: "[]"; } + &::after { + display: none; + } + &.json-formatter-object::after { + content: "No properties"; + } + &.json-formatter-array::after { + content: "[]"; + } } } @@ -27,19 +34,33 @@ white-space: normal; word-wrap: break-word; } - .json-formatter-number { color: $json-explorer-number-color; } - .json-formatter-boolean { color: $json-explorer-boolean-color; } - .json-formatter-null { color: $json-explorer-null-color; } - .json-formatter-undefined { color: $json-explorer-undefined-color; } - .json-formatter-function { color: $json-explorer-function-color; } - .json-formatter-date { background-color: fade($json-explorer-default-color, 5%); } + .json-formatter-number { + color: $json-explorer-number-color; + } + .json-formatter-boolean { + color: $json-explorer-boolean-color; + } + .json-formatter-null { + color: $json-explorer-null-color; + } + .json-formatter-undefined { + color: $json-explorer-undefined-color; + } + .json-formatter-function { + color: $json-explorer-function-color; + } + .json-formatter-date { + background-color: fade($json-explorer-default-color, 5%); + } .json-formatter-url { text-decoration: underline; color: $json-explorer-url-color; cursor: pointer; } - .json-formatter-bracket { color: $json-explorer-bracket-color; } + .json-formatter-bracket { + color: $json-explorer-bracket-color; + } .json-formatter-key { color: $json-explorer-key-color; cursor: pointer; @@ -51,7 +72,9 @@ cursor: pointer; } - .json-formatter-array-comma { margin-right: 4px; } + .json-formatter-array-comma { + margin-right: 4px; + } .json-formatter-toggler { line-height: 1.2rem; @@ -71,7 +94,7 @@ // Inline preview on hover (optional) > a > .json-formatter-preview-text { opacity: 0; - transition: opacity .15s ease-in; + transition: opacity 0.15s ease-in; font-style: italic; } @@ -81,7 +104,7 @@ // Open state &.json-formatter-open { - > .json-formatter-toggler-link .json-formatter-toggler::after{ + > .json-formatter-toggler-link .json-formatter-toggler::after { transform: rotate(90deg); } > .json-formatter-children::after { @@ -95,4 +118,3 @@ } } } - diff --git a/public/sass/components/_jsontree.scss b/public/sass/components/_jsontree.scss index 011566f8731..665deda0f12 100644 --- a/public/sass/components/_jsontree.scss +++ b/public/sass/components/_jsontree.scss @@ -8,7 +8,8 @@ json-tree { &::before { pointer-events: none; } - &::before, & > .json-tree-key { + &::before, + & > .json-tree-key { cursor: pointer; } } @@ -23,7 +24,8 @@ json-tree { ul { padding-left: $spacer; } - li, ul { + li, + ul { list-style: none; } li { @@ -33,22 +35,23 @@ json-tree { color: $variable; padding: 5px 10px 5px 15px; &::after { - content: ':'; + content: ":"; } } json-node.expandable { &::before { - content: '\25b6'; + content: "\25b6"; position: absolute; left: 0px; font-size: 8px; - transition: transform .1s ease; + transition: transform 0.1s ease; } &.expanded::before { transform: rotate(90deg); } } - .json-tree-leaf-value, .json-tree-branch-preview { + .json-tree-leaf-value, + .json-tree-branch-preview { word-break: break-all; } .json-tree-branch-preview { @@ -56,6 +59,6 @@ json-tree { font-style: italic; max-width: 40%; height: 1.5em; - opacity: .7; + opacity: 0.7; } } diff --git a/public/sass/components/_modals.scss b/public/sass/components/_modals.scss index 42953f1d6a1..df435462549 100644 --- a/public/sass/components/_modals.scss +++ b/public/sass/components/_modals.scss @@ -10,7 +10,7 @@ bottom: 0; left: 0; z-index: $zindex-modal-backdrop; - background-color: $black; + background-color: $modal-backdrop-bg; } .modal-backdrop, @@ -21,10 +21,9 @@ // Base modal .modal { position: fixed; - overflow: hidden; z-index: $zindex-modal; width: 100%; - background-color: $panel-bg; + background: $page-bg; @include box-shadow(0 3px 7px rgba(0,0,0,0.3)); @include background-clip(padding-box); outline: none; @@ -38,32 +37,32 @@ } .modal-header { - background-color: $body-bg; - @include brand-bottom-border(); - @include clearfix(); - - .gf-tabs-link.active { - background-color: $panel-bg; - } + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; + @include clearfix(); } .modal-header-title { - font-style: italic; font-size: $font-size-h3; float: left; padding-top: $spacer * 0.75; margin: 0 $spacer*3 0 $spacer*1.5; + + .gicon { + position: relative; + top: -2px; + } } .modal-header-close { float: right; - padding: 0.75rem $spacer; + padding: 9px $spacer; } // Body (where all modal content resides) .modal-body { position: relative; - overflow-y: auto; } .modal-content { @@ -78,7 +77,7 @@ // Footer (for actions) .modal-footer { padding: 14px 15px 15px; - border-top: 1px solid $panel-bg; + border-top: 1px solid $panel-bg; background-color: $panel-bg; text-align: right; // right align buttons @include clearfix(); // clear it in case folks use .pull-* classes on buttons @@ -140,7 +139,8 @@ .share-modal-big-icon { margin-bottom: 10px; margin-right: 2rem; - .fa, .icon-gf { + .fa, + .icon-gf { font-size: 50px; } } @@ -174,5 +174,3 @@ text-overflow: ellipsis; } } - - diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 238e6111f87..7cf03319f1b 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -1,198 +1,168 @@ - .navbar { - display: block; - overflow: visible; position: relative; - z-index: 110; -} - -.navbar-inner { - min-height: $navbarHeight; + padding-left: 40px; + z-index: $zindex-navbar-fixed; + height: $navbarHeight; padding-right: $spacer; - background-color: $navbarBackground; + display: flex; + flex-grow: 1; + border-bottom: 1px solid transparent; + transition-duration: 350ms; + transition-timing-function: ease-in-out; + transition-property: box-shadow, border-bottom; +} + +@mixin navbar-alt-look() { + background: $page-header-bg; + box-shadow: $search-shadow; border-bottom: $navbarBorder; - @include clearfix(); } -.navbar .nav { - position: relative; - left: 0; - float: left; -} - -.navbar .nav.pull-right { - float: right; // redeclare due to specificity - margin-right: 0; // remove margin on float right nav -} - -.navbar .nav > li { - float: left; -} - -// Links -.navbar .nav > li > a { - float: none; - padding: 17px 13px 13px; - color: $navbarLinkColor; - text-decoration: none; - - .fa { font-size: 115%; } -} - -// Hover/focus -.navbar .nav > li > a:focus, -.navbar .nav > li > a:hover { - color: $navbarLinkColorHover; - text-decoration: none; -} - -// Active nav items -.navbar .nav > .active > a, -.navbar .nav > .active > a:hover, -.navbar .nav > .active > a:focus { - color: $navbarLinkColorActive; - text-decoration: none; - background-color: $navbarLinkBackgroundActive; -} - -.navbar-brand-btn { - display: block; - position: relative; - float: left; - margin: 0; - border-right: 1px solid $tight-form-border; - background-color: $navbarButtonBackground; - padding: 0.4rem 1.0rem 0.4rem 1rem; - min-height: $navbarHeight; - - .fa-caret-down { - font-size: 70%; +.dashboard-page--settings-open { + .navbar { + @include navbar-alt-look(); } - .fa-chevron-left{ + .navbar-button--add-panel, + .navbar-button--star, + .navbar-button--share, + .navbar-button--settings, + .navbar-page-btn .fa-caret-down, + .gf-timepicker-nav { display: none; } - &:hover { - background: $navbarButtonBackgroundHighlight; + .navbar-buttons--close { + display: flex; + } +} + +.panel-in-fullscreen { + .navbar { + @include navbar-alt-look(); } - img { - width: 30px; - position: relative; - top: -1px; - } - - .navbar-brand-btn-background { - display: inline-block; - border: 1px solid $body-bg; - padding: 4px; - border-radius: 50%; - background: $iconContainerBackground; - width: 40px; - height: 40px; - } - - .icon-gf-grafana_wordmark { - font-size: 21px; - position: relative; - top: 6px; - padding-left: 5px; + .navbar-button--add-panel, + .navbar-button--star, + .navbar-button--save, + .navbar-button--settings, + .navbar-page-btn .fa-caret-down { display: none; } + + .navbar-buttons--close { + display: flex; + } } .navbar-page-btn { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; - float: left; display: block; margin: 0; - font-size: 1.4rem; - border-right: 1px solid $tight-form-border; color: darken($link-color, 5%); - background-color: $navbarButtonBackground; font-size: $font-size-lg; - padding: 1rem 1rem 0.75rem 1rem; + padding-left: 1rem; min-height: $navbarHeight; - - &:hover, &.active { - background: $navbarButtonBackgroundHighlight; - } + line-height: $navbarHeight; .fa-caret-down { font-size: 60%; padding-left: 0.2rem; } - .icon-gf { - position: relative; - top: 2px; - font-size: 20px; - line-height: 8px; - } - - > img { - max-width: 27px; - max-height: 27px; - } - &--search { padding: 1rem 1.5rem 0.75rem 1.5rem; } + + .gicon { + position: relative; + top: -1px; + font-size: 19px; + line-height: 8px; + opacity: 0.75; + margin-right: 8px; + // icon hidden on smaller screens + display: none; + } } -.navbar-page-btn-wrapper { +.navbar-buttons { + height: $navbarHeight; + display: flex; + align-items: center; + justify-content: flex-end; + margin-right: $spacer; + + &--close { + display: none; + margin-right: 0; + } +} + +.navbar__spacer { + flex-grow: 1; +} + +.navbar-button { + @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow); + display: inline-block; - position: relative; -} + font-weight: $btn-font-weight; + padding: 6px 11px; + line-height: 16px; + color: $text-muted; + border: 1px solid $navbar-button-border; + margin-right: 3px; + white-space: nowrap; -.dropdown-menu.dropdown-menu--navbar { - top: 100%; - min-width: 100%; - margin-top: 0px; + .gicon { + font-size: 16px; + } - li a { - padding: $spacer/2 $spacer; - border-left: 2px solid $side-menu-bg; - background: $side-menu-bg; + .fa { + font-size: 16px; + } - i { - display: inline-block; - padding-right: 21px; - } + &--add-panel { + padding: 3px 10px; - &:hover { - @include left-brand-border-gradient(); - color: $link-hover-color; - background: $input-label-bg; + .gicon { + font-size: 22px; } } -} -.sidemenu-pinned { - .navbar-brand-btn { - width: $side-menu-width; + &--tight { + padding: 7px 4px; - .icon-gf-grafana_wordmark { - display: inline-block; - } - - .fa-caret-down { - display: none; - } - - &:hover .fa-chevron-left { - display: inline-block; - color: $text-color-weak; + .fa { + font-size: 14px; position: relative; - left: 1.3rem; + top: 2px; } } + + &--primary { + @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); + } } -.navbar-section-wrapper { - position: relative; - float: left; +@include media-breakpoint-up(sm) { + .navbar { + padding-left: 50px; + } + + .sidemenu-open { + .navbar { + padding-left: 15px; + margin-left: 0; + } + } + + .navbar-page-btn { + .gicon { + display: inline-block; + } + } } diff --git a/public/sass/components/_navs.scss b/public/sass/components/_navs.scss index abbb4ba5042..796e84fc124 100644 --- a/public/sass/components/_navs.scss +++ b/public/sass/components/_navs.scss @@ -2,7 +2,6 @@ // Navs // -------------------------------------------------- - // BASE CLASS // ---------- @@ -55,7 +54,7 @@ // Actual tabs (as links) .nav-tabs > li > a { - padding: 0.40rem 1rem 0.35rem 1rem; + padding: 0.4rem 1rem 0.35rem 1rem; margin-right: $spacer/2; line-height: $line-height-base; border: 1px solid transparent; diff --git a/public/sass/components/_old_stuff.scss b/public/sass/components/_old_stuff.scss index ccc56377748..80f2b96dc0d 100644 --- a/public/sass/components/_old_stuff.scss +++ b/public/sass/components/_old_stuff.scss @@ -1,4 +1,3 @@ - .editor-row { vertical-align: top; } diff --git a/public/sass/components/_page_header.scss b/public/sass/components/_page_header.scss new file mode 100644 index 00000000000..947aad11326 --- /dev/null +++ b/public/sass/components/_page_header.scss @@ -0,0 +1,172 @@ +.page-header-canvas { + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; +} + +.page-header { + padding: 2.5rem 0 0 0; + + .btn { + float: right; + margin-left: 1rem; + + // better align icons + .fa { + position: relative; + top: 1px; + } + } +} + +.page-header__inner { + flex-grow: 1; + display: flex; + margin-bottom: 2.5rem; +} + +.page-header__title { + font-size: $font-size-h2; + margin-bottom: 1px; + padding-top: $spacer; +} + +.page-header__img { + position: relative; + top: 10px; + height: 50px; +} + +.page-header__icon { + font-size: 50px; + width: 50px; + height: 50px; + position: relative; + + &.fa { + top: 10px; + } + + &.gicon { + top: 10px; + } + + &.icon-gf { + top: 3px; + } +} + +.page-header__logo { + margin: 0 $spacer; +} + +.page-header__sub-title { + color: $text-muted; +} + +.page-header-stamps-type { + color: $link-color-disabled; + text-transform: uppercase; +} + +.page-header__select-nav { + margin-bottom: 10px; + max-width: 100%; + + @include media-breakpoint-up(lg) { + display: none; + } +} + +.page-header__tabs { + display: none; + @include media-breakpoint-up(lg) { + display: block; + } +} + +.page-breadcrumbs { + display: flex; + padding: 10px 0; + line-height: 0.5; +} + +.breadcrumb { + display: inline-block; + box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.35); + overflow: hidden; + border-radius: 5px; + counter-reset: flag; +} + +.breadcrumb-item { + @include gradientBar($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color); + + text-decoration: none; + outline: none; + display: block; + float: left; + font-size: 12px; + line-height: 30px; + padding: 0 7px 0 37px; + position: relative; + box-shadow: $card-shadow; + + &:first-child { + padding-left: 10px; + border-radius: 5px 0 0 5px; /*to match with the parent's radius*/ + font-size: 18px; + } + + &:first-child::before { + left: 14px; + } + + &:last-child { + border-radius: 0 5px 5px 0; /*this was to prevent glitches on hover*/ + padding-right: 20px; + } + + &.active, + &:hover { + background: #333; + background: linear-gradient(#333, #000); + } + + &.active::after, + &:hover::after { + background: #333; + background: linear-gradient(135deg, #333, #000); + } + + &::after { + content: ""; + position: absolute; + top: 0; + right: -14px; // half of square's length + + // same dimension as the line-height of .breadcrumb-item + width: 30px; + height: 30px; + + transform: scale(0.707) rotate(45deg); + // we need to prevent the arrows from getting buried under the next link + z-index: 1; + + // background same as links but the gradient will be rotated to compensate with the transform applied + 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); + + // 5px - for rounded arrows and + // 50px - to prevent hover glitches on the border created using shadows*/ + border-radius: 0 5px 0 50px; + } + + // we dont need an arrow after the last link + &:last-child::after { + content: none; + } +} diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss new file mode 100644 index 00000000000..548d677ef47 --- /dev/null +++ b/public/sass/components/_panel_add_panel.scss @@ -0,0 +1,71 @@ +.add-panel { + height: 100%; +} + +.add-panel__header { + padding: 5px 15px; + display: flex; + align-items: center; + + i { + font-size: 30px; + margin-right: $spacer; + } +} + +.add-panel__title { + font-size: $font-size-md; + margin-right: $spacer/2; +} + +.add-panel__sub-title { + font-style: italic; + color: $text-muted; + position: relative; + top: 1px; +} + +.add-panel__items { + padding: 3px 8px; + display: flex; + flex-direction: row; + flex-wrap: wrap; + overflow: auto; + height: calc(100% - 43px); + align-content: flex-start; + justify-content: space-around; + position: relative; +} + +.add-panel__item { + background: $card-background; + box-shadow: $card-shadow; + + border-radius: 3px; + padding: $spacer/3 $spacer; + width: 31%; + height: 60px; + text-align: center; + margin: $gf-form-margin; + cursor: pointer; + + &.active, + &:hover { + background: $card-background-hover; + } +} + +.add-panel__item-name { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-size: $font-size-sm; +} + +.add-panel__item-img { + height: calc(100% - 15px); +} + +.add-panel__item-icon { + padding: 2px; +} diff --git a/public/sass/components/_panel_alertlist.scss b/public/sass/components/_panel_alertlist.scss index f124c0c4b8c..c5d76c2f3b1 100644 --- a/public/sass/components/_panel_alertlist.scss +++ b/public/sass/components/_panel_alertlist.scss @@ -9,4 +9,3 @@ width: 100%; height: calc(100% - 30px); } - diff --git a/public/sass/components/_panel_dashlist.scss b/public/sass/components/_panel_dashlist.scss index 40b78166b1e..09ad4208099 100644 --- a/public/sass/components/_panel_dashlist.scss +++ b/public/sass/components/_panel_dashlist.scss @@ -8,23 +8,17 @@ } .dashlist-link { - display: block; - margin: 5px; - padding: 7px; - background-color: $tight-form-bg; + @include list-item(); + .fa { padding-top: 3px; } + .dashlist-star { float: right; } + .fa-star { color: $orange; } - - &:hover { - background-color: $tight-form-func-bg; - } } - - diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index d5a231a220c..1fb3eda1834 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -1,19 +1,17 @@ - // Colours -$progress-color-dark: $panel-bg !default; -$progress-color: $panel-bg !default; -$progress-color-light: $panel-bg !default; +$progress-color-dark: $panel-bg !default; +$progress-color: $panel-bg !default; +$progress-color-light: $panel-bg !default; $progress-color-grey-light: $body-bg !default; -$progress-color-shadow: $panel-border !default; -$progress-color-grey: $iconContainerBackground !default; -$progress-color-grey-dark: $iconContainerBackground !default; +$progress-color-shadow: $panel-border !default; +$progress-color-grey: $iconContainerBackground !default; +$progress-color-grey-dark: $iconContainerBackground !default; // Sizing -$marker-size: 60px !default; -$marker-size-half: ($marker-size / 2); -$path-height: 2px !default; -$path-position: $marker-size-half - ($path-height / 2); - +$marker-size: 60px !default; +$marker-size-half: ($marker-size / 2); +$path-height: 2px !default; +$path-position: $marker-size-half - ($path-height / 2); .dashlist-cta-close-btn { color: $text-color-weak; @@ -35,7 +33,7 @@ $path-position: $marker-size-half - ($path-height / 2); // Container element .progress-tracker { display: flex; - margin: 20px auto; + margin: 0 auto; padding: 0; list-style: none; } @@ -52,9 +50,9 @@ $path-position: $marker-size-half - ($path-height / 2); // For a flexbox bug in firefox that wont allow the text overflow on the text min-width: $marker-size; - &::after { + &::after { right: -50%; - content: ''; + content: ""; display: block; position: absolute; z-index: 1; @@ -96,7 +94,7 @@ $path-position: $marker-size-half - ($path-height / 2); -webkit-text-fill-color: transparent; background: $brand-gradient; -webkit-background-clip: text; - text-decoration:none; + text-decoration: none; } } } @@ -154,17 +152,18 @@ $path-position: $marker-size-half - ($path-height / 2); .progress-marker { color: $text-color-weak; - text-decoration:none; + text-decoration: none; font-size: 35px; vertical-align: sub; } a.progress-link { &:hover { - .progress-marker, .progress-text { + .progress-marker, + .progress-text { color: $link-hover-color; } - &:hover .progress-marker.completed { + &:hover .progress-marker.completed { color: $online; } } diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index c92e5a0d3d0..d03c8e0efb3 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -1,10 +1,31 @@ -.graph-canvas-wrapper { - position: relative; - cursor: crosshair; +.graph-panel { + display: flex; + flex-direction: column; + height: 100%; + + &--legend-right { + flex-direction: row; + + .graph-legend { + flex: 0 1 10px; + max-height: 100%; + } + + .graph-legend-series { + display: block; + padding-left: 0px; + } + + .graph-legend-table .graph-legend-series { + display: table-row; + } + } } -.histogram-chart { +.graph-panel__chart { position: relative; + cursor: crosshair; + flex-grow: 1; } .datapoints-warning { @@ -22,11 +43,12 @@ } .graph-legend { - @include clearfix(); + flex: 0 1 auto; + max-height: 30%; margin: 0 $spacer; text-align: center; - width: calc(100% - $spacer); padding-top: 6px; + position: relative; .popover-content { padding: 0; @@ -48,19 +70,19 @@ font-size: 85%; text-align: left; &.current::before { - content: "Current: " + content: "Current: "; } &.max::before { - content: "Max: " + content: "Max: "; } &.min::before { - content: "Min: " + content: "Min: "; } &.total::before { - content: "Total: " + content: "Total: "; } &.avg::before { - content: "Avg: " + content: "Avg: "; } } @@ -89,7 +111,9 @@ display: block; overflow-y: auto; overflow-x: hidden; + height: 100%; padding-bottom: 1px; + padding-right: 5px; } .graph-legend-series { @@ -100,15 +124,17 @@ float: none; .graph-legend-alias::after { - content: '(right-y)'; + content: "(right-y)"; padding: 0 5px; color: $text-color-weak; } } } - - td, .graph-legend-alias, .graph-legend-icon, .graph-legend-value { + td, + .graph-legend-alias, + .graph-legend-icon, + .graph-legend-value { float: none; display: table-cell; white-space: nowrap; @@ -125,7 +151,7 @@ } } - .graph-legend-value { + .graph-legend-value { padding-left: 15px; } @@ -139,13 +165,17 @@ } .graph-legend-series:nth-child(odd) { - background-color: $grafanaListAccent; + background: $table-bg-accent; } .graph-legend-value { - &.current, &.max, &.min, &.total, &.avg { + &.current, + &.max, + &.min, + &.total, + &.avg { &::before { - content: ''; + content: ""; } } } @@ -160,40 +190,6 @@ } } -.graph-legend-rightside { - - &.graph-wrapper { - display: table; - width: 100%; - } - - .graph-canvas-wrapper { - display: table-cell; - width: 100%; - position: relative; - } - - .graph-legend-wrapper { - display: table-cell; - vertical-align: top; - position: relative; - left: 4px; - } - - .graph-legend { - margin: 0 0 0 1rem; - } - - .graph-legend-series { - display: block; - padding-left: 0px; - } - - .graph-legend-table .graph-legend-series { - display: table-row; - } -} - .graph-legend-series-hidden { .graph-legend-value, .graph-legend-alias { @@ -202,7 +198,7 @@ } .graph-legend-popover { - width: 200px; + width: 210px; label { display: inline-block; } @@ -261,7 +257,7 @@ max-width: 650px; text-overflow: ellipsis; overflow: hidden; - } + } .graph-tooltip-value { display: table-cell; @@ -272,7 +268,6 @@ } .graph-annotation { - .label-tag { margin-right: 4px; margin-top: 8px; @@ -405,7 +400,7 @@ } } - &--T0{ + &--T0 { right: -104px; width: 129px; @@ -428,10 +423,10 @@ position: relative; &--critical { - background-color: rgba(237, 46, 24, 0.60); + background-color: rgba(237, 46, 24, 0.6); } &--warning { - background-color: rgba(247, 149, 32, 0.60); + background-color: rgba(247, 149, 32, 0.6); } } } diff --git a/public/sass/components/_panel_heatmap.scss b/public/sass/components/_panel_heatmap.scss index 134186f25a2..c0bdee7ec90 100644 --- a/public/sass/components/_panel_heatmap.scss +++ b/public/sass/components/_panel_heatmap.scss @@ -42,7 +42,7 @@ .heatmap-crosshair { line { - stroke: darken($red,15%); + stroke: darken($red, 15%); stroke-width: 1; } } diff --git a/public/sass/components/_panel_pluginlist.scss b/public/sass/components/_panel_pluginlist.scss index 605e1afdb6a..a51748a984d 100644 --- a/public/sass/components/_panel_pluginlist.scss +++ b/public/sass/components/_panel_pluginlist.scss @@ -8,14 +8,7 @@ } .pluginlist-link { - display: block; - margin: 5px; - padding: 7px; - background-color: $tight-form-bg; - - &:hover { - background-color: $tight-form-func-bg; - } + @include list-item(); } .pluginlist-icon { @@ -25,7 +18,7 @@ } .pluginlist-image { - width: 20px; + width: 17px; } .pluginlist-title { @@ -43,14 +36,14 @@ } .pluginlist-message--update { - &:hover { + &:hover { border-bottom: 1px solid $text-color; } } -.pluginlist-message--enable{ +.pluginlist-message--enable { color: $external-link-color; - &:hover { + &:hover { border-bottom: 1px solid $external-link-color; } } diff --git a/public/sass/components/_panel_singlestat.scss b/public/sass/components/_panel_singlestat.scss index 48d0a29f4b8..33a956a0244 100644 --- a/public/sass/components/_panel_singlestat.scss +++ b/public/sass/components/_panel_singlestat.scss @@ -2,9 +2,11 @@ position: relative; display: table; width: 100%; + height: 100%; } .singlestat-panel-value-container { + line-height: 1; display: table-cell; vertical-align: middle; text-align: center; @@ -18,37 +20,6 @@ padding-right: 20px; } -.singlestat-panel-table { - width: 100%; - td { - padding: 5px 10px; - white-space: nowrap; - text-align: right; - border-bottom: 1px solid $grafanaListBorderBottom; - } - - th { - text-align: right; - padding: 5px 10px; - font-weight: bold; - color: $blue - } - - td:first-child { - text-align: left; - } - - tr:nth-child(odd) td { - background-color: $grafanaListAccent; - } - - tr:last-child td { - border: none; - } -} - #flotGagueValue0 { font-weight: bold; //please dont hurt me for this! } - - diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index 77652bc634d..f120fcc8b35 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -27,9 +27,11 @@ margin-left: 0; margin-bottom: 0; } + ul > li { display: inline; // Remove list-style and block-level defaults } + ul > li > a { float: left; // Collapse white-space padding: 4px 12px; @@ -109,10 +111,10 @@ } .table-panel-header-bg { - background: $grafanaListAccent; + background: $list-item-bg; border-top: 2px solid $body-bg; border-bottom: 2px solid $body-bg; - height: 2.0em; + height: 2em; position: absolute; top: 0; right: 0; diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss index 2442505b5b6..aa9a1da679b 100644 --- a/public/sass/components/_query_editor.scss +++ b/public/sass/components/_query_editor.scss @@ -1,6 +1,12 @@ .query-keyword { - font-weight: bold; - color: $blue; + font-weight: $font-weight-semi-bold; + color: $query-blue; +} + +.gf-form-disabled { + .query-keyword { + color: darken($query-blue, 20%); + } } .query-segment-operator { @@ -27,6 +33,10 @@ .gf-form-label { margin-right: 2px; } + + .gf-form + .gf-form { + margin-right: 0; + } } .gf-form-query-content { @@ -53,7 +63,7 @@ } .gf-form-query-letter-cell-letter { font-weight: bold; - color: $blue; + color: $query-blue; } .gf-form-query-letter-cell-ds { color: $text-color-weak; diff --git a/public/sass/components/_query_part.scss b/public/sass/components/_query_part.scss index 1e2fb9622c2..340bd3c27c3 100644 --- a/public/sass/components/_query_part.scss +++ b/public/sass/components/_query_part.scss @@ -1,6 +1,5 @@ - .query-part { - background-color: $input-bg !important; + background-color: lighten($input-label-bg, 5%); &.show-function-controls { padding-top: 5px; @@ -8,4 +7,3 @@ text-align: center; } } - diff --git a/public/sass/components/_row.scss b/public/sass/components/_row.scss index 0cdc3428f3e..3c1465a30bc 100644 --- a/public/sass/components/_row.scss +++ b/public/sass/components/_row.scss @@ -1,167 +1,78 @@ - -.dash-row { - display: block; +.dashboard-row { display: flex; - flex-direction: column; - position: relative; + align-items: center; - &--collapse { - .dash-row-header { - background: $panel-bg; - border: $panel-border; - margin-bottom: $panel-margin*2; - } - } -} + height: 100%; -.dash-row-header { - position: relative; - display: flex; - flex-direction: row; - margin-right: $panel-margin; - margin-left: 0; + &--collapsed { + background: $panel-bg; - .h1 { font-size: 2.7rem; font-style: normal; line-height: 4rem; font-weight: 300; } - .h2 { font-size: 2.4rem; line-height: 3.5rem; font-weight: 300; } - .h3 { font-size: 2.0rem; line-height: 3rem; font-weight: 300;} - .h4 { font-size: 1.7rem; font-weight: 300;} - .h5 { font-size: 1.4rem; font-weight: 300;} - .h6 { font-size: 1rem; font-weight: 300; } -} - -.dash-row-header-title { - padding: 0.6rem; - flex-grow: 1; - - .dash-row-collapse-toggle { - padding: 0 2px; - font-size: $font-size-xs; - color: $text-muted; - position: relative; - left: -3px; - top: -1px; - } - - &:hover { - .dash-row-collapse-toggle { - color: $link-color; - } - } -} - -.panels-wrapper { - flex-grow: 1; - position: relative; -} - -.dash-row-dropview { - position: relative; - background: $panel-bg; - border: $panel-border; - margin: 0 $panel-margin $panel-margin*2 $panel-margin; - padding: $panel-margin*2; - display: flex; - flex-direction: row; -} - -.dash-row-dropview-close { - position: absolute; - right: -12px; - top: -10px; - width: 20px; - height: 20px; -} - -.add-panel-panels { - display: flex; - flex-direction: row; - flex-wrap: wrap; -} - -.add-panel-item { - background: $input-label-bg; - border: $panel-border; - padding: $spacer/3 $spacer; - min-width: 9rem; - max-width: 9rem; - text-align: center; - margin: $gf-form-margin; - cursor: pointer; - - &.active, - &:hover { - box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 1px rgba(82,168,236,5.8) - } -} - -.add-panel-item-name { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - font-size: $font-size-sm; -} - -.add-panel-item-img { - width: 2rem; -} - -$dash-row-menu-animation-speed: 0.20s; - -.dash-row-menu-container { - position: absolute; - top: 0px; - width: 138px; - min-height: 100%; - transform: translate(-131px, 0); - transition: 0.1s ease-out 0.4s; - z-index: 99; - display: flex; - flex-direction: row; - - &:hover { - transform: translate(-$panel-margin, 0); - transition: $dash-row-menu-animation-speed ease-in 0.05s; - z-index: 101; - - .dash-row-menu-grip { - opacity: 0; - transition: $dash-row-menu-animation-speed ease-in 0.05s; - } - } -} - -.dash-row-menu { - list-style: none; - flex-grow: 1; - box-shadow: $search-shadow; - background: $side-menu-bg; - - li a { - display: block; - white-space: nowrap; - color: $text-muted; - font-size: $font-size-sm; - padding: $spacer/2 $spacer; - border-left: 2px solid $side-menu-bg; - i { + .dashboard-row__panel_count { display: inline-block; - padding-right: $spacer/2; } + .dashboard-row__drag, + .dashboard-row__actions { + visibility: visible; + opacity: 1; + } + } + + &:hover { + .dashboard-row__actions { + visibility: visible; + opacity: 1; + } + } +} + +.dashboard-row__title { + flex-grow: 0; + font-size: 1.15rem; + font-weight: $font-weight-semi-bold; + color: $text-color; + + .fa { + color: $text-muted; + font-size: $font-size-xs; + padding: 0 0.5rem; + } +} + +.dashboard-row__actions { + color: $text-muted; + visibility: hidden; + opacity: 0; + flex-grow: 1; + transition: 200ms opacity ease-in 200ms; + + a { + color: $text-color-weak; + padding-left: $spacer; + &:hover { - @include left-brand-border-gradient(); - color: $link-color; - background: $input-label-bg; + color: $link-hover-color; } } } -.dash-row-menu-grip { - text-align: center; - font-size: 130%; - color: $text-color-faint; - opacity: 1; - transition: $dash-row-menu-animation-speed ease-out 0.5s; - width: 1rem; +.dashboard-row__panel_count { + padding-left: $spacer; + color: $text-color-weak; + font-style: italic; + font-size: $font-size-sm; + font-weight: normal; + display: none; } +.dashboard-row__drag { + cursor: move; + width: 1rem; + height: 100%; + background: url("../img/grab_dark.svg") no-repeat 50% 50%; + background-size: 8px; + visibility: hidden; + position: absolute; + top: 0; + right: 0; +} diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index 85227625a8d..50d6428b41e 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -1,4 +1,106 @@ -// +/* + * Container style + */ +.ps { + overflow: hidden !important; + overflow-anchor: none; + -ms-overflow-style: none; + touch-action: auto; + -ms-touch-action: auto; +} + +/* + * Scrollbar rail styles + */ +.ps__rail-x { + display: none; + opacity: 0; + transition: background-color 0.2s linear, opacity 0.2s linear; + -webkit-transition: background-color 0.2s linear, opacity 0.2s linear; + height: 15px; + /* there must be 'bottom' or 'top' for ps__rail-x */ + bottom: 0px; + /* please don't change 'position' */ + position: absolute; +} + +.ps__rail-y { + display: none; + opacity: 0; + transition: background-color 0.2s linear, opacity 0.2s linear; + -webkit-transition: background-color 0.2s linear, opacity 0.2s linear; + width: 15px; + /* there must be 'right' or 'left' for ps__rail-y */ + right: 0; + /* please don't change 'position' */ + position: absolute; +} + +.ps--active-x > .ps__rail-x, +.ps--active-y > .ps__rail-y { + display: block; + background-color: transparent; +} + +.ps--focus > .ps__rail-x, +.ps--focus > .ps__rail-y, +.ps--scrolling-x > .ps__rail-x, +.ps--scrolling-y > .ps__rail-y { + opacity: 0.6; +} + +.ps__rail-x:hover, +.ps__rail-y:hover, +.ps__rail-x:focus, +.ps__rail-y:focus { + background-color: transparent; + opacity: 0.9; +} + +/* + * Scrollbar thumb styles + */ +.ps__thumb-x { + background-color: #aaa; + border-radius: 6px; + height: 6px; + /* there must be 'bottom' for ps__thumb-x */ + bottom: 2px; + /* please don't change 'position' */ + position: absolute; +} + +.ps__thumb-y { + @include gradient-vertical($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + width: 6px; + /* there must be 'right' for ps__thumb-y */ + right: 2px; + /* please don't change 'position' */ + position: absolute; +} + +/* MS supports */ +@supports (-ms-overflow-style: none) { + .ps { + overflow: auto !important; + } +} + +@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + .ps { + overflow: auto !important; + } +} + +.ps__rail-x:hover, +.ps__rail-y:hover, +.ps__rail-x:focus, +.ps__rail-y:focus { + background-color: transparent; + opacity: 0.9; +} + // Srollbars // @@ -12,20 +114,46 @@ } ::-webkit-scrollbar-button:start:decrement, -::-webkit-scrollbar-button:end:increment { display: none; } -::-webkit-scrollbar-button:horizontal:decrement { display: none; } -::-webkit-scrollbar-button:horizontal:increment { display: none; } -::-webkit-scrollbar-button:vertical:decrement { display: none; } -::-webkit-scrollbar-button:vertical:increment { display: none; } -::-webkit-scrollbar-button:horizontal:decrement:active { background-image: none; } -::-webkit-scrollbar-button:horizontal:increment:active { background-image: none; } -::-webkit-scrollbar-button:vertical:decrement:active { background-image: none; } -::-webkit-scrollbar-button:vertical:increment:active {background-image: none; } -::-webkit-scrollbar-track-piece { background-color: transparent; } +::-webkit-scrollbar-button:end:increment { + display: none; +} +::-webkit-scrollbar-button:horizontal:decrement { + display: none; +} +::-webkit-scrollbar-button:horizontal:increment { + display: none; +} +::-webkit-scrollbar-button:vertical:decrement { + display: none; +} +::-webkit-scrollbar-button:vertical:increment { + display: none; +} +::-webkit-scrollbar-button:horizontal:decrement:active { + background-image: none; +} +::-webkit-scrollbar-button:horizontal:increment:active { + background-image: none; +} +::-webkit-scrollbar-button:vertical:decrement:active { + background-image: none; +} +::-webkit-scrollbar-button:vertical:increment:active { + background-image: none; +} +::-webkit-scrollbar-track-piece { + background-color: transparent; +} ::-webkit-scrollbar-thumb:vertical { height: 50px; - background: -webkit-gradient(linear, left top, right top, color-stop(0%, $scrollbarBackground), color-stop(100%, $scrollbarBackground2)); + background: -webkit-gradient( + linear, + left top, + right top, + color-stop(0%, $scrollbarBackground), + color-stop(100%, $scrollbarBackground2) + ); border: 1px solid $scrollbarBorder; border-top: 1px solid $scrollbarBorder; border-left: 1px solid $scrollbarBorder; @@ -33,9 +161,14 @@ ::-webkit-scrollbar-thumb:horizontal { width: 50px; - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, $scrollbarBackground), color-stop(100%, $scrollbarBackground2)); + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(0%, $scrollbarBackground), + color-stop(100%, $scrollbarBackground2) + ); border: 1px solid $scrollbarBorder; border-top: 1px solid $scrollbarBorder; border-left: 1px solid $scrollbarBorder; } - diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index e35bf4e6b6e..f2db2140f71 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -1,34 +1,30 @@ .search-backdrop { position: fixed; right: 0; - bottom: 0; left: 0; + bottom: 0; top: $navbarHeight; z-index: $zindex-modal-backdrop; background-color: $black; - @include opacity(70); + @include opacity(75); } .search-container { - left: 0; + left: $side-menu-width; top: 0; right: 0; bottom: 0; z-index: ($zindex-modal-backdrop + 10); position: fixed; - - .label-tag { - margin-left: 6px; - font-size: 11px; - padding: 2px 6px; - } } // Search .search-field-wrapper { width: 100%; display: flex; - background-color: $navbarButtonBackground; + background-color: $navbarBackground; + box-shadow: $navbarShadow; + position: relative; input { max-width: 653px; @@ -48,12 +44,6 @@ flex-grow: 1; } -.search-switches { - flex-grow: 1; - padding: 1rem 1rem 0.75rem 1rem; - white-space: nowrap; -} - .search-field-icon { font-size: $font-size-lg; padding: 1rem 1rem 0.75rem 1.5rem; @@ -61,123 +51,203 @@ .search-dropdown { display: flex; - flex-direction: column; - max-width: 1100px; - visibility: none; - opacity: 0; - height: 100%; + flex-direction: row; + height: calc(100% - #{$navbarHeight}); +} - &--fade-in { - visibility: visible; - opacity: 1; - transition: opacity 0.3s; +.search-dropdown__col_1 { + background: $page-bg; + max-width: 700px; + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.search-dropdown__col_2 { + flex-grow: 1; + height: 100%; + padding-top: 16px; +} + +.search-filter-box { + background: $search-filter-box-bg; + border-radius: 2px; + padding: $spacer*1.5; + max-width: 340px; + margin-bottom: $spacer * 1.5; + margin-left: $spacer * 1.5; +} + +.search-filter-box__header { + border-bottom: 1px solid $dark-5; + margin-bottom: $spacer * 1.5; +} + +.search-filter-box-link { + display: block; + margin-bottom: 16px; + + &:last-child { + margin-bottom: 0; + } + + i, + img { + font-size: 20px; + margin-right: 5px; } } .search-results-container { - height: 90%; - overflow: auto; + height: 100%; display: block; - line-height: 28px; padding: $spacer; - background: $panel-bg; + position: relative; flex-grow: 10; + margin-bottom: 1rem; + + .label-tag { + margin-left: 6px; + font-size: 11px; + padding: 2px 6px; + } .selected { .search-result-tag { - opacity: 0.70; + opacity: 0.7; color: white; } } +} - .fa-star, .fa-star-o { - padding-left: 13px; +.search-section { + background: $panel-bg; + border: $panel-border; + padding: 0px 4px 4px 4px; + margin-bottom: 3px; + border-radius: 5px; +} + +.search-section__header { + font-size: $font-size-h6; + padding: 7px 4px 3px 0; + color: $text-color-weak; + display: flex; + flex-grow: 1; + + &:hover, + &.selected { + color: $text-color; } - .fa-star { - color: $orange; - } - - .search-result-link { - color: $grafanaListMainLinkColor; - .fa { - padding-right: 10px; + &:hover { + .search-section__header__link { + opacity: 1; } } - - .search-item { - word-wrap: break-word; - display: block; - padding: 3px 10px; - white-space: nowrap; - background-color: $tight-form-bg; - margin-bottom: 4px; - @include left-brand-border(); - - .search-result-icon::before { - content: "\f009"; - } - - &.search-item-dash-home .search-result-icon::before { - content: "\f015"; - } - - &:hover, - &.selected { - background-color: $tight-form-func-bg; - @include left-brand-border-gradient(); - } - } - - .search-result-tags { - float: right; - } - - .search-result-actions { - float: right; - padding-left: 20px; - } } +.search-section__header__icon { + padding: 5px 0px; + width: 43px; + text-align: center; +} + +.search-section__header__toggle { + padding: 5px; +} + +.search-section__header__text { + flex-grow: 1; + line-height: 24px; +} + +.search-section__header__link { + padding: 2px 10px 0; + color: $text-muted; + opacity: 0; + transition: opacity 150ms ease-in-out; +} + +.search-item { + @include list-item(); + + display: flex; + flex-grow: 1; + height: 37px; + white-space: nowrap; + padding: 0px; + + &:hover, + &.selected { + background: $list-item-hover-bg; + } +} + +.search-item__body { + flex: 1 1 auto; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + padding: 0 10px; +} + +.search-item__body-title { + color: $list-item-link-color; +} + +.search-item__icon { + padding: 5px; + flex: 0 0 auto; + font-size: 19px; + padding: 5px 2px 5px 10px; +} + +.search-item__tags { + padding: 10px; +} + +.search-item__actions { + flex: 0 0 auto; + padding: 0 10px 0 0; +} + +.search-item__actions__item { + display: inline-block; + opacity: 0; + width: 0; + transition: all 0.2s ease-in-out; + .fa-star, + .fa-star-o { + color: $orange; + line-height: 37px; + } +} + +.search-item:hover { + .search-item__actions__item { + width: 15px; + opacity: 1; + } +} .search-button-row { - padding: $spacer*2; - display: flex; - flex-direction: row; - align-items: flex-start; - justify-content: space-around; - height: 30%; - - button, a { - margin-bottom: $spacer; - } - - .search-button-row-explore-link { - color: $gray-3; - font-size: $font-size-sm; - position: relative; - top: 1.0rem; - &:hover { - color: $link-hover-color; - } - img { - vertical-align: text-bottom; - } - } + text-align: center; + padding: $spacer*2 $spacer; + background: $panel-bg; } -@include media-breakpoint-up(lg) { - .search-dropdown { - flex-direction: row; - } - .search-button-row { - flex-direction: column; - justify-content: flex-start; - } -} - -@include media-breakpoint-up(md) { +@include media-breakpoint-down(xs) { .search-container { - left: 78px; + left: 0; + } + + .search-dropdown__col_2 { + display: none; + } + + .search-item__tags { + display: none; } } diff --git a/public/sass/components/_settings_permissions.scss b/public/sass/components/_settings_permissions.scss new file mode 100644 index 00000000000..5864c58679e --- /dev/null +++ b/public/sass/components/_settings_permissions.scss @@ -0,0 +1,32 @@ +.permissionlist { + .permissionlist__section { + margin-bottom: $spacer*2; + } + + .permissionlist__section-header { + margin-bottom: $spacer; + display: flex; + } + + .permissionlist__section-header h6 { + margin: auto 5px; + color: $text-color-weak; + } + + .permissionlist__section-header__add-button { + margin-left: auto; + width: 105px; + } + + .permissionlist__item { + background-color: $tight-form-bg; + + &:hover { + background-color: $tight-form-func-bg; + } + } + + .permissionlist__item-buttons { + margin-left: auto; + } +} diff --git a/public/sass/components/_shortcuts.scss b/public/sass/components/_shortcuts.scss index 1dedb062183..b5f61872585 100644 --- a/public/sass/components/_shortcuts.scss +++ b/public/sass/components/_shortcuts.scss @@ -1,4 +1,3 @@ - .shortcut-category { float: left; font-size: $font-size-sm; @@ -11,7 +10,6 @@ .shortcut-table-category-header { font-weight: normal; font-size: $font-size-h6; - font-style: italic; text-align: left; } @@ -45,4 +43,3 @@ color: $btn-inverse-text-color; box-shadow: inset 0 -1px 0 $btn-inverse-bg-hl; } - diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index 0e5402403e5..5f21bbc8cd0 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -1,229 +1,272 @@ -.sidemenu-canvas { - position: relative; -} - -.sidemenu-wrapper { - position: absolute; - top: 52px; - left: 0; +.sidemenu { + position: fixed; + display: flex; + flex-flow: column; + flex-direction: column; width: $side-menu-width; - background-color: rgba($side-menu-bg,$side-menu-opacity); - z-index: 1000; - opacity: 0; - visibility: hidden; - + z-index: $zindex-sidemenu; a:focus { text-decoration: none; } -} -.sidemenu-open { - .sidemenu-wrapper { - visibility: visible; - //transform: translate3d(0, 0, 0); - opacity: 1; - transition: opacity 0.3s; + .sidemenu__logo_small_breakpoint { + display: none; + } + + .sidemenu__close { + display: none; } } -.sidemenu-pinned { - .sidemenu-wrapper { - min-height: calc(100% - 54px); - } - .dashboard-container { - padding-left: $side-menu-width + 0.5rem; - } - .page-container { - margin-left: $side-menu-width; - } - .search-container { - left: $side-menu-width; +// body class that hides sidemenu +.sidemenu-hidden { + .sidemenu { + display: none; } } -.sidemenu { - list-style: none; - margin: 0; - padding: 0; +@include media-breakpoint-up(sm) { + .sidemenu-open { + .sidemenu { + background: $side-menu-bg; + position: initial; + height: auto; + box-shadow: $side-menu-shadow; + position: relative; + z-index: $zindex-sidemenu; + } + .sidemenu__top, + .sidemenu__bottom { + display: block; + } + } +} - > li { - position: relative; - @include left-brand-border(); +.sidemenu__top { + padding-top: 3rem; + flex-grow: 1; + display: none; +} +.sidemenu__bottom { + padding-bottom: $spacer; + display: none; +} + +.sidemenu-item { + position: relative; + @include left-brand-border(); + + @include media-breakpoint-up(sm) { &.active, &:hover { background-color: $side-menu-item-hover-bg; @include left-brand-border-gradient(); .dropdown-menu { + margin: 0; display: block; opacity: 0; top: 0px; // important to overlap it otherwise it can be hidden // again by the mouse getting outside the hover space - left: $side-menu-width - 0.2rem; - background-color: rgba($side-menu-bg,$side-menu-opacity); - @include animation('dropdown-anim 150ms ease-in-out 100ms forwards'); - z-index: -9999; + left: $side-menu-width - 2px; + @include animation("dropdown-anim 150ms ease-in-out 100ms forwards"); + z-index: $zindex-sidemenu; } } } } +.dropup.sidemenu-item:hover .dropdown-menu { + top: auto !important; +} + +.sidemenu-link { + color: $link-color; + line-height: 42px; + padding: 0px 10px 0px 10px; + display: block; + position: relative; + font-size: 16px; + border: 1px solid transparent; + text-align: center; + + img { + border-radius: 50%; + width: 28px; + height: 28px; + box-shadow: 0 0 14px 2px rgba(255, 255, 255, 0.05); + } +} + @include keyframes(dropdown-anim) { 0% { opacity: 0; - transform: translate3d(-5%,0,0); + //transform: translate3d(-5%,0,0); } 100% { opacity: 1; - transform: translate3d(0,0,0); + //transform: translate3d(0,0,0); } } -.sidemenu-main-link { - font-size: 16px; -} - -.sidemenu-item-text { - width: 110px; - display: inline-block; - vertical-align: middle; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - .icon-circle { - width: 40px; - height: 40px; + width: 35px; + height: 35px; display: inline-block; - i { - color: $link-color; - opacity: .7; + + .fa, + .icon-gf, + .gicon { + color: $side-menu-link-color; position: relative; - left: 7px; + opacity: 0.7; + font-size: 130%; + } + + .fa { + top: 2px; + position: relative; + } + + .icon-gf { top: 5px; - font-size: 150%; } + img { - left: 7px; position: relative; } } -.sidemenu-item { - color: $link-color; - line-height: 47px; - padding: 0px 10px 0px 10px; +.side-menu-header { + padding: 10px 10px 10px 20px; + white-space: nowrap; + background-color: $side-menu-item-hover-bg; + font-size: 17px; + color: #ebedf2; +} + +li.sidemenu-org-switcher { + border-bottom: 1px solid $dropdownDividerBottom; +} + +.sidemenu-org-switcher__org-name { + font-size: $font-size-base; +} + +.sidemenu-org-switcher__org-current { + font-size: $font-size-xs; + color: $text-color-weak; + position: relative; + top: -2px; +} + +.sidemenu-org-switcher__switch { + font-size: $font-size-sm; + padding-left: 1.5rem; + display: flex; + align-items: center; + > i.fa.fa-random { + margin-right: 4px; + top: 1px; + } +} + +.sidemenu__logo { display: block; - border-left: 1px solid transparent; + padding: 0.4rem 1rem 0.4rem 0.65rem; + min-height: $navbarHeight; + position: relative; - - .sidemenu-item-text { - padding-left: 11px; + &:hover { + background: $navbarButtonBackgroundHighlight; } img { - border-radius: 50%; - width: 28px; - height: 28px; - box-shadow: 0 0 14px 2px rgba(255,255,255, 0.05); + width: 30px; + position: relative; + top: 5px; + left: 4px; } } -.sidemenu-section-tagline { - font-style: italic; - line-height: 10px; -} +@include media-breakpoint-down(xs) { + .sidemenu-open--xs { + .sidemenu { + width: 100%; + background: $side-menu-bg; + position: initial; + height: auto; + box-shadow: $side-menu-shadow; + position: relative; + z-index: $zindex-sidemenu; + } -.sidemenu-section-text-wrapper { - padding-top: 4px; -} + .sidemenu__close { + display: block; + font-size: $font-size-md; + position: relative; + top: -3px; + } -.sidemenu-org-section .dropdown-menu-title { - margin: 0 10px 0 6px; - padding: 7px 0 7px; - overflow: hidden; - color: $dropdownTitle; -} + .sidemenu__top, + .sidemenu__bottom { + display: block; + } + } -.sidemenu-org-section .dropdown-menu-title > span { - display: inline-block; - position: relative; + .sidemenu { + .sidemenu__logo { + display: none; + } - &::after { - display: block; - position: absolute; - top: 50%; - right: 0; - left: 100%; - width: 200px; - height: 1px; - margin-left: 5px; - background: $dropdownDivider; - content: ''; + .sidemenu__logo_small_breakpoint { + padding: 16px 10px 26px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: baseline; + + .fa-bars { + font-size: 25px; + } + } + + .sidemenu__top { + padding-top: 0rem; + } + + .side-menu-header { + padding-left: 10px; + } + + .sidemenu-link { + text-align: left; + } + + .sidemenu-icon { + display: none; + } + + .dropdown-menu--sidemenu { + display: block; + position: unset; + width: 100%; + float: none; + margin-top: 0.5rem; + margin-bottom: 0.5rem; + + > li > a { + padding-left: 15px; + } + } + + .sidemenu__bottom { + .dropdown-menu--sidemenu { + display: flex; + flex-direction: column-reverse; + } + } } } - -.sidemenu-org { - padding: 17px 10px 15px 14px; - box-sizing: border-box; - cursor: pointer; - display: table; - position: relative; - width: 100%; -} - -.sidemenu .fa-caret-right { - position: absolute; - top: 38%; - right: 6px; - font-size: 14px; - color: $text-color-faint; -} - -.sidemenu-org-avatar, -.sidemenu-org-details { - display: table-cell; - vertical-align: top; -} - -.sidemenu-org-avatar { - width: 40px; - height: 40px; - background-color: $gray-2; - border-radius: 50%; - text-align: center; - - >img { - position: absolute; - width: 40px; - height: 40px; - border-radius: 50%; - left: 14px; - } -} - -.sidemenu-org-avatar--missing { - color: $gray-4; - text-shadow: 0 1px 0 $dark-1; - line-height: 40px; - font-size: $font-size-lg; -} - -.sidemenu-org-details { - padding-left: 10px; - color: $link-color; -} - -.sidemenu-org-name { - display: block; - font-size: 13px; - color: $link-color-disabled; -} - -.sidemenu-org-user { - display: block; -} - diff --git a/public/sass/components/_submenu.scss b/public/sass/components/_submenu.scss index 878058bd1c4..0bf7f5b0a1c 100644 --- a/public/sass/components/_submenu.scss +++ b/public/sass/components/_submenu.scss @@ -5,10 +5,11 @@ align-content: flex-start; align-items: flex-start; - margin: 0 $panel-margin ($panel-margin*2) $panel-margin; + margin: 0 0 $panel-margin 0; } -.annotation-disabled, .annotation-disabled a { +.annotation-disabled, +.annotation-disabled a { color: $link-color-disabled; } @@ -38,7 +39,11 @@ .variable-value-link { padding-right: 10px; - padding: 8px 7px; + padding: $input-padding-y $input-padding-x; + background-color: $input-bg; + border: 1px solid $input-border-color; + border-radius: $input-border-radius; + color: $input-color; box-sizing: content-box; display: inline-block; color: $text-color; @@ -48,18 +53,9 @@ } } -.variable-link-wrapper { +.variable-link-wrapper { display: inline-block; position: relative; - - .hidden-input { - padding: 8px 7px; - border: none; - margin: 0px; - background: transparent; - border-radius: 0; - border-right: 1px solid $tight-form-border; - } } .variable-value-dropdown { @@ -71,28 +67,28 @@ overflow-y: auto; overflow-x: hidden; background-color: $dropdownBackground; - box-shadow: 0 0 25px 0 rgba(0,0,0,0.4); + box-shadow: 0 0 25px 0 rgba(0, 0, 0, 0.4); z-index: 1000; font-size: $font-size-base; border-radius: 3px 3px 0 0; border: 1px solid $tight-form-func-bg; &.multi { - .selected { - .variable-option-icon{ - background: url($checkboxImageUrl) 0px -18px no-repeat; - } - } - } + .selected { + .variable-option-icon { + background: url($checkboxImageUrl) 0px -18px no-repeat; + } + } + } - &.single { - .variable-option-icon { - display: none; - } - .selected { - background-color: $tight-form-func-highlight-bg; - } - } + &.single { + .variable-option-icon { + display: none; + } + .selected { + background-color: $tight-form-func-highlight-bg; + } + } } .variable-options-wrapper { @@ -140,7 +136,8 @@ } .variable-option { - &:hover, &.highlighted { + &:hover, + &.highlighted { background-color: $blue-dark; } } diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index f09e83f8908..c7eb1914103 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -1,7 +1,3 @@ -$switch-border-radius: 1rem; -$switch-width: 3.5rem; -$switch-height: 1.5rem; - /* ============================================================ SWITCH 3 - YES NO ============================================================ */ @@ -10,7 +6,7 @@ $switch-height: 1.5rem; position: relative; max-width: 4.5rem; flex-grow: 1; - min-width: 4.0rem; + min-width: 4rem; margin-right: $gf-form-margin; input { @@ -27,11 +23,15 @@ $switch-height: 1.5rem; outline: none; user-select: none; width: 100%; - height: 2.65rem; - background-color: $page-bg; + height: 37px; + background: $input-bg; + border: 1px solid $input-border-color; + border-left: none; + border-radius: $input-border-radius; } - input + label::before, input + label::after { + input + label::before, + input + label::after { @include buttonBackground($input-bg, $input-bg); display: block; @@ -61,11 +61,10 @@ $switch-height: 1.5rem; color: lighten($orange, 10%); text-shadow: $text-shadow-strong; } - } input + label::before { - font-family: 'FontAwesome'; + font-family: "FontAwesome"; content: "\f096"; // square-o color: $text-color-weak; transition: transform 0.4s; @@ -78,7 +77,7 @@ $switch-height: 1.5rem; color: $orange; text-shadow: $text-shadow-strong; - font-family: 'FontAwesome'; + font-family: "FontAwesome"; transition: transform 0.4s; transform: rotateY(180deg); backface-visibility: hidden; @@ -102,7 +101,55 @@ $switch-height: 1.5rem; } } -gf-form-switch[disabled] { +.gf-form-switch--transparent { + input + label { + background: transparent; + border: none; + } + + input + label::before, + input + label::after { + background: transparent; + border: none; + } + + &:hover { + input + label::before { + background: transparent; + } + + input + label::after { + background: transparent; + } + } +} + +.gf-form-switch--search-result__section { + min-width: 3.05rem; + margin-right: -0.3rem; + + input + label { + height: 1.7rem; + } +} + +.gf-form-switch--search-result__item { + min-width: 2.7rem; + + input + label { + height: 2.7rem; + } +} + +.gf-form-switch--search-result-filter-row__checkbox { + min-width: 3.75rem; + + input + label { + height: 2.5rem; + } +} + +gf-form-switch[disabled] { .gf-form-label, .gf-form-switch input + label { cursor: default; diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index 6cc04f259d6..dfd760753fe 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -1,42 +1,43 @@ .tabbed-view { - background-color: $page-bg; - background-image: $page-gradient; - margin: (-$panel-margin*2) (-$panel-margin); - margin-bottom: $spacer*2; padding: $spacer*3; + margin-bottom: $dashboard-padding; &.tabbed-view--panel-edit { padding: 0; .tabbed-view-header { - background-color: $body-bg; - padding: 1.5em 1rem 0 1rem; - } - .gf-tabs-link.active { - background-color: $panel-bg; + padding: 0px 25px; + background: none; } } } .tabbed-view-header { + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; @include clearfix(); - @include brand-bottom-border(); } .tabbed-view-title { float: left; - font-style: italic; padding-top: 0.5rem; margin: 0 $spacer*3 0 $spacer*1; } +.tabbed-view-panel-title { + float: left; + padding-top: 9px; + margin: 0 2rem 0 0; +} + .tabbed-view-close-btn { float: right; padding: 0; margin: 0; background-color: transparent; border: none; - padding: ($tabs-padding-top + $tabs-top-margin) $spacer $tabs-padding-bottom; + padding: $tabs-padding; color: $text-color; i { font-size: 120%; @@ -48,7 +49,11 @@ .tabbed-view-body { padding: $spacer*2 $spacer; - min-height: 250px; + + &--small { + min-height: 0px; + padding-bottom: 0px; + } } .section-heading { diff --git a/public/sass/components/_tables_lists.scss b/public/sass/components/_tables_lists.scss index 6787ef2869e..43be08eac33 100644 --- a/public/sass/components/_tables_lists.scss +++ b/public/sass/components/_tables_lists.scss @@ -8,7 +8,7 @@ } tr td { - background-color: $grafanaListBackground; + background-color: $list-item-bg; padding: 5px 10px; white-space: nowrap; border-bottom: 4px solid $panel-bg; @@ -37,7 +37,7 @@ display: block; padding: 1px 10px; line-height: 34px; - background-color: $tight-form-bg; + background-color: $list-item-bg; margin-bottom: 4px; cursor: pointer; } diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index 32e395c4d73..197d5892652 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -1,41 +1,8 @@ -.nav-tabs-alt { - & > li > a { - color: darken($link-color, 20%); - } - - li > a:hover { - border-bottom: none; - } - - li.active > a, - li.active > a:focus, - li.active > a:hover { - @include border-radius(3px); - border: 1px solid $divider-border-color; - background-color: transparent; - border-bottom: 1px solid $page-bg; - color: $link-color; - } - - li.disabled > a { - color: $text-color; - } - - .open .dropdown-toggle { - background-color: #060606; - border-color: transparent; - } - - .tab-content { - padding: 10px; - background-color: $panel-bg; - } -} - .gf-tabs { @include clearfix(); float: left; - margin: $tabs-top-margin 0 0 0; + position: relative; + top: 1px; } .gf-tabs-item { @@ -44,13 +11,23 @@ } .gf-tabs-link { - padding: $tabs-padding-top $spacer $tabs-padding-bottom $spacer; + padding: $tabs-padding; margin-right: $spacer/2; - border: 1px solid transparent; position: relative; display: block; + border: solid transparent; + border-width: 0 1px 1px; + border-radius: 3px 3px 0 0; + color: $text-color; - @include border-radius(2px 2px 0 0); + i { + margin-right: 5px; + } + + .gicon { + position: relative; + top: -2px; + } &:hover, &:focus { @@ -60,20 +37,25 @@ &.active, &.active:hover, &.active:focus { - @include border-radius(3px); - border-color: rgba(216, 131, 40, 0.77); - border-bottom: 2px solid $panel-bg; + border-color: $orange $tab-border-color transparent; + background: $page-bg; color: $link-color; - position: relative; - top: 1px; + overflow: hidden; + + &::before { + display: block; + content: " "; + position: absolute; + left: 0; + right: 0; + height: 2px; + top: 0; + background-image: linear-gradient( + to right, + #ffd500 0%, + #ff4400 99%, + #ff4400 100% + ); + } } } - -.form-tabs-wrapper { - @include brand-bottom-border(); - @include clearfix(); -} - -.form-tabs-content { - padding: $spacer*2 $spacer; -} diff --git a/public/sass/components/_tags.scss b/public/sass/components/_tags.scss index c00328c49c4..ef9a82bc630 100644 --- a/public/sass/components/_tags.scss +++ b/public/sass/components/_tags.scss @@ -3,17 +3,16 @@ .badge { display: inline-block; padding: 2px 4px; - font-size: $font-size-base * .846; + font-size: $font-size-base * 0.846; font-weight: bold; line-height: 14px; // ensure proper line-height if floated color: $white; vertical-align: baseline; white-space: nowrap; - text-shadow: 0 -1px 0 rgba(0,0,0,.25); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: $gray-1; } - // Labels & Badges .label-tag { background-color: $purple; @@ -25,7 +24,7 @@ padding: 2px 6px; border-width: 1px; border-style: solid; - box-shadow: 0 0 1px rgba($white,.2); + box-shadow: 0 0 1px rgba($white, 0.2); .icon-tag { position: relative; top: 1px; @@ -34,8 +33,6 @@ } .label-tag:hover { - opacity: 0.85; + opacity: 0.85; background-color: darken($purple, 10%); } - - diff --git a/public/sass/components/_tagsinput.scss b/public/sass/components/_tagsinput.scss index 0753f045a61..d25a8fc33ba 100644 --- a/public/sass/components/_tagsinput.scss +++ b/public/sass/components/_tagsinput.scss @@ -9,7 +9,6 @@ input { display: inline-block; border: none; - border-right: 1px solid $tight-form-border; margin: 0px; border-radius: 0; padding: 8px 6px; @@ -23,16 +22,17 @@ color: white; [data-role="remove"] { - margin-left:8px; - cursor:pointer; - &::after{ + margin-left: 8px; + cursor: pointer; + &::after { content: "x"; - padding:0px 2px; + padding: 0px 2px; } &:hover { - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), + 0 1px 2px rgba(0, 0, 0, 0.05); &:active { - box-shadow: inset 0 3px 5px rgba(0,0,0,0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); } } } diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index a5f311cdb46..2d7a12c3d01 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -1,29 +1,30 @@ -.nav.gf-timepicker-nav { - margin-right: 0; -} - .timepicker-timestring { font-weight: normal; } +.gf-timepicker-nav { + flex-wrap: nowrap; + display: flex; +} + .gf-timepicker-nav-btn { - overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; + overflow: hidden; } .gf-timepicker-dropdown { - margin: -8px -10px 10px 5px; + position: absolute; + top: $navbarHeight; + right: 0; padding: 10px 20px; - float: right; - background-color: $panel-bg; - @include box-shadow($navbarDropdownShadow); + background-color: $page-bg; + border-radius: 0 0 0 4px; + box-shadow: $search-shadow; } .gf-timepicker-absolute-section { width: 290px; float: left; - border-right: 1px solid $divider-border-color; padding: 0 10px; select { width: 183px; @@ -41,7 +42,7 @@ font-size: 75%; padding: 3px; border-radius: 2px; - font-weight: bold; + font-weight: 500; margin-left: 4px; } @@ -59,7 +60,7 @@ li.active { border-bottom: 1px solid $blue; margin: 3px 0; - font-weight: bold; + font-weight: 500; } } } @@ -89,7 +90,7 @@ } .input-datetime-format { - color: $link-color-disabled + color: $link-color-disabled; } .fa { @@ -117,15 +118,3 @@ @extend .fa; @extend .fa-chevron-left; } - -.gf-timepicker-time-control { - font-size: $font-size-sm; - a { - padding: 18px 7px 13px !important; - } -} - -.dashnav-move-timeframe { - position: relative; - top: 1px; -} diff --git a/public/sass/components/_tooltip.scss b/public/sass/components/_tooltip.scss index f710dc68931..b5799e49301 100644 --- a/public/sass/components/_tooltip.scss +++ b/public/sass/components/_tooltip.scss @@ -11,11 +11,25 @@ font-size: 11px; line-height: 1.4; @include opacity(0); - &.in { @include opacity(100); } - &.top { margin-top: -3px; padding: 5px 0; } - &.right { margin-left: 3px; padding: 0 5px; } - &.bottom { margin-top: 3px; padding: 5px 0; } - &.left { margin-left: -3px; padding: 0 5px; } + &.in { + @include opacity(100); + } + &.top { + margin-top: -3px; + padding: 5px 0; + } + &.right { + margin-left: 3px; + padding: 0 5px; + } + &.bottom { + margin-top: 3px; + padding: 5px 0; + } + &.left { + margin-left: -3px; + padding: 0 5px; + } } // Wrapper for the tooltip content @@ -70,13 +84,13 @@ } .grafana-tooltip { - position : absolute; + position: absolute; top: -1000; left: 0; color: $tooltipColor; padding: 10px; font-size: 11pt; - font-weight : 200; + font-weight: 200; background-color: $tooltipBackground; border-radius: 5px; z-index: 9999; @@ -92,4 +106,3 @@ .grafana-tip { padding-left: 5px; } - diff --git a/public/sass/components/_typeahead.scss b/public/sass/components/_typeahead.scss index efa4f3459ed..84b6dc552d2 100644 --- a/public/sass/components/_typeahead.scss +++ b/public/sass/components/_typeahead.scss @@ -1,4 +1,3 @@ - // typeahead max height .typeahead { max-height: 300px; @@ -8,4 +7,3 @@ .typeahead strong { color: $yellow; } - diff --git a/public/sass/components/_view_states.scss b/public/sass/components/_view_states.scss index a1ed89f9888..45fa3a631ee 100644 --- a/public/sass/components/_view_states.scss +++ b/public/sass/components/_view_states.scss @@ -1,20 +1,4 @@ -@mixin hide-controls() { - .add-row-panel-hint, - .dash-row-menu-container { - display: none; - } - .resize-panel-handle, - .panel-drop-zone { - visibility: hidden; - } -} - -.hide-controls { - @include hide-controls(); -} - .page-kiosk-mode { - @include hide-controls(); dashnav { display: none; } @@ -22,54 +6,32 @@ .playlist-active, .user-activity-low { - .add-row-panel-hint, + .react-resizable-handle .add-row-panel-hint, .dash-row-menu-container, - .resize-panel-handle, - .panel-drop-zone - .dashnav-refresh-action, - .dashnav-zoom-out, - .dashnav-action-icons, + .navbar-button--refresh, + .navbar-buttons--zoom, + .navbar-buttons--actions, + .panel-menu-container, .panel-info-corner--info, - .panel-info-corner--links, - .dashnav-move-timeframe { + .panel-info-corner--links { opacity: 0; - transition: all 1.5s ease-in-out 1s; } - // navbar buttons - .navbar-brand-btn, - .navbar-inner { - border-color: transparent; + .navbar { + box-shadow: none; background: transparent; - transition: all 1.5s ease-in-out 1s; - .fa { - opacity: 0; - transition: all 1.5s ease-in-out 1s; - } } .navbar-page-btn { border-color: transparent; background: transparent; - transform: translate3d(-50px, 0, 0); - transition: all 1.5s ease-in-out 1s; - .icon-gf { + transform: translate3d(-40px, 0, 0); + i { opacity: 0; - transition: all 1.5s ease-in-out 1s; } } .gf-timepicker-nav-btn { transform: translate3d(40px, 0, 0); - transition: transform 1.5s ease-in-out 1s; - } -} - -.playlist-active { - .dash-playlist-actions { - .fa { - opacity: 1; - color: $text-color-faint !important; - } } } diff --git a/public/sass/components/edit_sidemenu.scss b/public/sass/components/edit_sidemenu.scss index e84ae2c1914..87d90f18162 100644 --- a/public/sass/components/edit_sidemenu.scss +++ b/public/sass/components/edit_sidemenu.scss @@ -1,4 +1,3 @@ - .edit-tab-with-sidemenu { display: flex; flex-direction: row; @@ -29,7 +28,6 @@ } } - @include media-breakpoint-down(sm) { .edit-tab-with-sidemenu { flex-direction: column; diff --git a/public/sass/grafana.dark.scss b/public/sass/grafana.dark.scss index 858d6ace336..53193d213e6 100644 --- a/public/sass/grafana.dark.scss +++ b/public/sass/grafana.dark.scss @@ -1,4 +1,3 @@ @import "variables"; @import "variables.dark"; @import "grafana"; - diff --git a/public/sass/icons.json b/public/sass/icons.json index 4c87042fe02..7e1bc712bf4 100644 --- a/public/sass/icons.json +++ b/public/sass/icons.json @@ -1,52 +1,52 @@ [ - "grafana_wordmark", - "worldping", - "raintank_wordmark", - "raintank_r-icn", - "check-alt", - "check", - "collector", - "dashboard", - "panel", - "datasources", - "endpoint-tiny", - "endpoint", - "page", - "filter", - "status", - "monitoring", - "monitoring-tiny", - "jump-to-dashboard", - "warning", - "nodata", - "critical", - "crit", - "online", - "event-error", - "event", - "sadface", - "private-collector", - "alert-disabled", - "refresh", - "save", - "share", - "star", - "search", - "remove", - "video", - "bulk_action", - "grabber", - "users", - "globe", - "snapshot", - "play-grafana-icon", - "grafana-icon", - "email", - "stopwatch", - "skull", - "probe", - "apps", - "scale", - "pending", - "verified" -] \ No newline at end of file + "grafana_wordmark", + "worldping", + "raintank_wordmark", + "raintank_r-icn", + "check-alt", + "check", + "collector", + "dashboard", + "panel", + "datasources", + "endpoint-tiny", + "endpoint", + "page", + "filter", + "status", + "monitoring", + "monitoring-tiny", + "jump-to-dashboard", + "warning", + "nodata", + "critical", + "crit", + "online", + "event-error", + "event", + "sadface", + "private-collector", + "alert-disabled", + "refresh", + "save", + "share", + "star", + "search", + "remove", + "video", + "bulk_action", + "grabber", + "users", + "globe", + "snapshot", + "play-grafana-icon", + "grafana-icon", + "email", + "stopwatch", + "skull", + "probe", + "apps", + "scale", + "pending", + "verified" +] diff --git a/public/sass/layout/_lists.scss b/public/sass/layout/_lists.scss index 3333e8d937b..63201f5fd98 100644 --- a/public/sass/layout/_lists.scss +++ b/public/sass/layout/_lists.scss @@ -1,4 +1,3 @@ - .ui-list { margin: 0; padding: 0; diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index 5702a1aa260..63ca67c086c 100644 --- a/public/sass/layout/_page.scss +++ b/public/sass/layout/_page.scss @@ -1,22 +1,77 @@ .grafana-app { - display: block; + display: flex; + align-items: stretch; + position: absolute; + width: 100%; height: 100%; + top: 0; + left: 0; } .main-view { - // height: 100%; REMOVED FOR FOOTER TRW + position: relative; + flex-grow: 1; + background: $page-gradient; } .page-container { - background-color: $page-bg; - padding: ($spacer * 2) ($spacer * 4); - max-width: 1060px; - min-height: calc(100% - 54px); - padding-bottom: $spacer * 5; - background-image: linear-gradient(60deg, transparent 70%, darken($page-bg, 4%) 98%) + margin-left: auto; + margin-right: auto; + padding-left: $spacer*2; + padding-right: $spacer*2; + max-width: 980px; + @include clearfix(); +} + +.scroll-canvas { + position: absolute; + width: 100%; + overflow: auto; + height: 100%; + + &--dashboard { + height: calc(100% - 56px); + } } .page-body { + padding-top: $spacer*2; + min-height: 500px; +} + +.page-heading { + font-size: 1.25rem; + margin-top: 0; + margin-bottom: $spacer * 0.7; +} + +.page-action-bar { + margin-bottom: $spacer * 2; + display: flex; + align-items: flex-start; + + > a, + > button { + margin-left: $spacer; + } +} + +.page-action-bar--narrow { + margin-bottom: 0; +} + +.page-action-bar__spacer { + width: $spacer * 2; + flex-grow: 1; +} + +.sidebar-content { + width: calc( + 100% - #{$page-sidebar-width + $page-sidebar-margin} + ); // sidebar width + margin +} + +.sidebar-container { @include media-breakpoint-up(md) { display: flex; flex-direction: row; @@ -24,10 +79,6 @@ } } -.page-content-with-sidebar { - width: calc(100% - #{$page-sidebar-width + $page-sidebar-margin}); // sidebar width + margin -} - .page-sidebar { @include media-breakpoint-up(md) { width: $page-sidebar-width; @@ -35,42 +86,8 @@ } } -.page-header { - padding: $spacer 0 0 0; - margin-bottom: 2rem; - @include brand-bottom-border(); - @include clearfix(); - - h1 { - flex-grow: 1; - display: inline-block; - margin-bottom: $spacer*1.5; - } - - button, a { - float: right; - margin-left: $spacer; - } -} - -.page-heading { - font-size: 1.25rem; - margin-top: 0; - margin-bottom: $spacer * 0.7; -} - -.admin-page { - max-width: 800px; - margin-left: 10px; - h2 { - margin-left: 15px; - margin-bottom: 0px; - font-size: $font-size-lg; - color: $text-color; - i { - padding-right: 6px; - } - } +.page-sub-heading { + margin-bottom: $spacer; } .page-sidebar { diff --git a/public/sass/mixins/_animations.scss b/public/sass/mixins/_animations.scss index 853dd34048e..18d9b6db456 100644 --- a/public/sass/mixins/_animations.scss +++ b/public/sass/mixins/_animations.scss @@ -1,19 +1,19 @@ @mixin keyframes($animation-name) { - @-webkit-keyframes #{$animation-name} { - @content; - } - @-moz-keyframes #{$animation-name} { - @content; - } - @-ms-keyframes #{$animation-name} { - @content; - } - @-o-keyframes #{$animation-name} { - @content; - } - @keyframes #{$animation-name} { - @content; - } + @-webkit-keyframes #{$animation-name} { + @content; + } + @-moz-keyframes #{$animation-name} { + @content; + } + @-ms-keyframes #{$animation-name} { + @content; + } + @-o-keyframes #{$animation-name} { + @content; + } + @keyframes #{$animation-name} { + @content; + } } @mixin animation($str) { diff --git a/public/sass/mixins/_buttons.scss b/public/sass/mixins/_buttons.scss index f77adc859c3..8561a03d02b 100644 --- a/public/sass/mixins/_buttons.scss +++ b/public/sass/mixins/_buttons.scss @@ -1,19 +1,22 @@ - // Button backgrounds // ------------------ -@mixin buttonBackground($startColor, $endColor, $text-color: #fff, $textShadow: 0 -1px 0 rgba(0,0,0,.25)) { +@mixin buttonBackground($startColor, $endColor, $text-color: #fff, $textShadow: 0px 1px 0 rgba(0,0,0,0.1)) { // gradientBar will set the background to a pleasing blend of these, to support IE<=9 @include gradientBar($startColor, $endColor, $text-color, $textShadow); // in these cases the gradient won't cover the background, so we override - &:hover, &:focus, &:active, &.active, &.disabled, &[disabled] { + &:hover, + &:focus, + &:active, + &.active, + &.disabled, + &[disabled] { color: $text-color; background-image: none; background-color: $startColor; } } - // Button sizes @mixin button-size($padding-y, $padding-x, $font-size, $border-radius) { padding: $padding-y $padding-x; @@ -32,14 +35,14 @@ @include hover { color: #fff; background-color: $color; - border-color: $color; + border-color: $color; } &:focus, &.focus { color: #fff; background-color: $color; - border-color: $color; + border-color: $color; } &:active, @@ -47,14 +50,14 @@ .open > &.dropdown-toggle { color: #fff; background-color: $color; - border-color: $color; + border-color: $color; &:hover, &:focus, &.focus { color: #fff; background-color: darken($color, 17%); - border-color: darken($color, 25%); + border-color: darken($color, 25%); } } diff --git a/public/sass/mixins/_drop_element.scss b/public/sass/mixins/_drop_element.scss index f1bb69efd98..e7e53382e0e 100644 --- a/public/sass/mixins/_drop_element.scss +++ b/public/sass/mixins/_drop_element.scss @@ -1,4 +1,3 @@ - @mixin drop-theme($themeName, $theme-bg, $theme-color, $border-color: $theme-bg) { .drop-element.drop-#{$themeName} { max-width: 100%; @@ -11,7 +10,6 @@ background: $theme-bg; color: $theme-color; padding: 0.65rem; - font-size: $font-size-sm; word-wrap: break-word; max-width: 20rem; border: 1px solid $border-color; @@ -90,7 +88,8 @@ left: $popover-arrow-size * 2; } - &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-middle .drop-content { + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-middle + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -100,7 +99,8 @@ } } - &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-middle .drop-content { + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-middle + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -110,7 +110,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-middle .drop-content { + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-middle + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -120,7 +121,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-middle .drop-content { + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-middle + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -131,7 +133,8 @@ } // Top and bottom corners - &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-bottom .drop-content { + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-bottom + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -141,7 +144,8 @@ } } - &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-bottom .drop-content { + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-bottom + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -151,7 +155,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-top .drop-content { + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-top + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -161,7 +166,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-top .drop-content { + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-top + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -172,7 +178,8 @@ } // Side corners - &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-left .drop-content { + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-left + .drop-content { margin-right: $popover-arrow-size; &:before { @@ -182,7 +189,8 @@ } } - &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-right .drop-content { + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-right + .drop-content { margin-left: $popover-arrow-size; &:before { @@ -192,7 +200,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-left .drop-content { + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-left + .drop-content { margin-right: $popover-arrow-size; &:before { @@ -202,7 +211,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-right .drop-content { + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-right + .drop-content { margin-left: $popover-arrow-size; &:before { @@ -237,44 +247,55 @@ } } // Centers and middles - &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-center .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-center + .#{$themePrefix}-content { transform-origin: 50% calc(100% + #{$attachmentOffset}); } - &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-center .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-center + .#{$themePrefix}-content { transform-origin: 50% (-$attachmentOffset); } - &.#{$themePrefix}-element-attached-right.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-right.#{$themePrefix}-element-attached-middle + .#{$themePrefix}-content { transform-origin: calc(100% + #{$attachmentOffset}) 50%; } - &.#{$themePrefix}-element-attached-left.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-left.#{$themePrefix}-element-attached-middle + .#{$themePrefix}-content { transform-origin: -($attachmentOffset 50%); } // Top and bottom corners - &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-bottom .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-bottom + .#{$themePrefix}-content { transform-origin: 0 (-$attachmentOffset); } - &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-bottom .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-bottom + .#{$themePrefix}-content { transform-origin: 100% (-$attachmentOffset); } - &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-top .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-top + .#{$themePrefix}-content { transform-origin: 0 calc(100% + #{$attachmentOffset}); } - &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-top .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-top + .#{$themePrefix}-content { transform-origin: 100% calc(100% + #{$attachmentOffset}); } // Side corners - &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-left .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-left + .#{$themePrefix}-content { transform-origin: calc(100% + #{$attachmentOffset}) 0; } - &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-right .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-right + .#{$themePrefix}-content { transform-origin: (-$attachmentOffset) 0; } - &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-left .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-right.#{$themePrefix}-target-attached-left + .#{$themePrefix}-content { transform-origin: calc(100% + #{$attachmentOffset}) 100%; } - &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-right .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-left.#{$themePrefix}-target-attached-right + .#{$themePrefix}-content { transform-origin: (-$attachmentOffset) 100%; } } } - diff --git a/public/sass/mixins/_forms.scss b/public/sass/mixins/_forms.scss index 5138cdf0a99..ce488f0f636 100644 --- a/public/sass/mixins/_forms.scss +++ b/public/sass/mixins/_forms.scss @@ -41,7 +41,8 @@ &:focus { border-color: $input-border-focus; outline: none; - $shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px $input-box-shadow-focus; + $shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 5px $input-box-shadow-focus; @include box-shadow($shadow); } } diff --git a/public/sass/mixins/_grid-framework.scss b/public/sass/mixins/_grid-framework.scss index 15d1e6bdf86..c14671f6628 100644 --- a/public/sass/mixins/_grid-framework.scss +++ b/public/sass/mixins/_grid-framework.scss @@ -16,7 +16,7 @@ max-width: 100%; min-height: 1px; padding-right: ($grid-gutter-width / 2); - padding-left: ($grid-gutter-width / 2); + padding-left: ($grid-gutter-width / 2); } } @@ -29,16 +29,17 @@ @each $modifier in (pull, push) { @for $i from 0 through $columns { .#{$modifier}-#{$breakpoint}-#{$i} { - @include make-col-modifier($modifier, $i, $columns) + @include make-col-modifier($modifier, $i, $columns); } } } // `$columns - 1` because offsetting by the width of an entire row isn't possible @for $i from 0 through ($columns - 1) { - @if $breakpoint-counter != 1 or $i != 0 { // Avoid emitting useless .col-xs-offset-0 + @if $breakpoint-counter != 1 or $i != 0 { + // Avoid emitting useless .col-xs-offset-0 .offset-#{$breakpoint}-#{$i} { - @include make-col-modifier(offset, $i, $columns) + @include make-col-modifier(offset, $i, $columns); } } } diff --git a/public/sass/mixins/_grid.scss b/public/sass/mixins/_grid.scss index 6e67f92cd39..abfc28ab61c 100644 --- a/public/sass/mixins/_grid.scss +++ b/public/sass/mixins/_grid.scss @@ -5,14 +5,13 @@ @mixin make-container($gutter: $grid-gutter-width) { margin-left: auto; margin-right: auto; - padding-left: ($gutter / 2); + padding-left: ($gutter / 2); padding-right: ($gutter / 2); @if not $enable-flex { @include clearfix(); } } - // For each breakpoint, define the maximum width of the container in a media query @mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) { @each $breakpoint, $container-max-width in $max-widths { @@ -29,7 +28,7 @@ } @else { @include clearfix(); } - margin-left: ($gutter / -2); + margin-left: ($gutter / -2); margin-right: ($gutter / -2); } @@ -37,7 +36,7 @@ position: relative; min-height: 1px; padding-right: ($grid-gutter-width / 2); - padding-left: ($grid-gutter-width / 2); + padding-left: ($grid-gutter-width / 2); @if $enable-flex { flex: 0 0 percentage($size / $columns); diff --git a/public/sass/mixins/_hover.scss b/public/sass/mixins/_hover.scss index 3a11254e847..2f2c0c1e43f 100644 --- a/public/sass/mixins/_hover.scss +++ b/public/sass/mixins/_hover.scss @@ -3,23 +3,29 @@ // See Media Queries Level 4: http://drafts.csswg.org/mediaqueries/#hover // Currently shimmed by https://github.com/twbs/mq4-hover-shim @media (hover: hover) { - &:hover { @content } + &:hover { + @content; + } + } + } @else { + &:hover { + @content; } - } - @else { - &:hover { @content } } } @mixin hover-focus { @if $enable-hover-media-query { - &:focus { @content } - @include hover { @content } - } - @else { + &:focus { + @content; + } + @include hover { + @content; + } + } @else { &:focus, &:hover { - @content + @content; } } } @@ -28,15 +34,16 @@ @if $enable-hover-media-query { &, &:focus { - @content + @content; } - @include hover { @content } - } - @else { + @include hover { + @content; + } + } @else { &, &:focus, &:hover { - @content + @content; } } } @@ -45,15 +52,16 @@ @if $enable-hover-media-query { &:focus, &:active { - @content + @content; } - @include hover { @content } - } - @else { + @include hover { + @content; + } + } @else { &:focus, &:active, &:hover { - @content + @content; } } } diff --git a/public/sass/mixins/_mixins.scss b/public/sass/mixins/_mixins.scss index 78f9d3fca08..f3be6af56ba 100644 --- a/public/sass/mixins/_mixins.scss +++ b/public/sass/mixins/_mixins.scss @@ -1,4 +1,3 @@ - @mixin clearfix() { &::after { content: ""; @@ -73,7 +72,6 @@ border: 0; } - // FONTS // -------------------------------------------------- @@ -110,7 +108,6 @@ @include font-shorthand($size, $weight, $lineHeight); } - // FORMS // -------------------------------------------------- @@ -122,7 +119,6 @@ @include box-sizing(border-box); // Makes inputs behave like true block-level elements } - // CSS3 PROPERTIES // -------------------------------------------------- @@ -146,13 +142,13 @@ } @mixin border-bottom-right-radius($radius) { -webkit-border-bottom-right-radius: $radius; - -moz-border-radius-bottomright: $radius; - border-bottom-right-radius: $radius; + -moz-border-radius-bottomright: $radius; + border-bottom-right-radius: $radius; } @mixin border-bottom-left-radius($radius) { -webkit-border-bottom-left-radius: $radius; - -moz-border-radius-bottomleft: $radius; - border-bottom-left-radius: $radius; + -moz-border-radius-bottomleft: $radius; + border-bottom-left-radius: $radius; } // Single Side Border Radius @@ -241,21 +237,21 @@ // CSS3 Content Columns @mixin content-columns($columnCount, $columnGap: $gridGutterWidth) { -webkit-column-count: $columnCount; - -moz-column-count: $columnCount; - column-count: $columnCount; + -moz-column-count: $columnCount; + column-count: $columnCount; -webkit-column-gap: $columnGap; - -moz-column-gap: $columnGap; - column-gap: $columnGap; + -moz-column-gap: $columnGap; + column-gap: $columnGap; } // Optional hyphenation @mixin hyphens($mode: auto) { word-wrap: break-word; -webkit-hyphens: $mode; - -moz-hyphens: $mode; - -ms-hyphens: $mode; - -o-hyphens: $mode; - hyphens: $mode; + -moz-hyphens: $mode; + -ms-hyphens: $mode; + -o-hyphens: $mode; + hyphens: $mode; } // Opacity @@ -269,16 +265,26 @@ // Add an alphatransparency value to any background or border color (via Elyse Holladay) #translucent { @mixin background($color: $white, $alpha: 1) { - background-color: hsla(hue($color), saturation($color), lightness($color), $alpha); + background-color: hsla( + hue($color), + saturation($color), + lightness($color), + $alpha + ); } @mixin border($color: $white, $alpha: 1) { - border-color: hsla(hue($color), saturation($color), lightness($color), $alpha); + border-color: hsla( + hue($color), + saturation($color), + lightness($color), + $alpha + ); @include background-clip(padding-box); } } // Gradient Bar Colors for buttons and alerts -@mixin gradientBar($primaryColor, $secondaryColor, $text-color: #fff, $textShadow: 0 -1px 0 rgba(0,0,0,.25)) { +@mixin gradientBar($primaryColor, $secondaryColor, $text-color: #fff, $textShadow: 0 -1px 0 rgba(0,0,0,0.25)) { color: $text-color; text-shadow: $textShadow; @include gradient-vertical($primaryColor, $secondaryColor); @@ -288,37 +294,66 @@ // Gradients @mixin gradient-horizontal($startColor: #555, $endColor: #333) { background-color: $endColor; - background-image: linear-gradient(to right, $startColor, $endColor); // Standard, IE10 + background-image: linear-gradient( + to right, + $startColor, + $endColor + ); // Standard, IE10 background-repeat: repeat-x; } @mixin gradient-vertical($startColor: #555, $endColor: #333) { background-color: mix($startColor, $endColor, 60%); - background-image: linear-gradient(to bottom, $startColor, $endColor); // Standard, IE10 + background-image: linear-gradient( + to bottom, + $startColor, + $endColor + ); // Standard, IE10 background-repeat: repeat-x; } @mixin gradient-directional($startColor: #555, $endColor: #333, $deg: 45deg) { background-color: $endColor; background-repeat: repeat-x; - background-image: linear-gradient($deg, $startColor, $endColor); // Standard, IE10 + background-image: linear-gradient( + $deg, + $startColor, + $endColor + ); // Standard, IE10 } @mixin gradient-horizontal-three-colors($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) { background-color: mix($midColor, $endColor, 80%); - background-image: linear-gradient(to right, $startColor, $midColor $colorStop, $endColor); + background-image: linear-gradient( + to right, + $startColor, + $midColor $colorStop, + $endColor + ); background-repeat: no-repeat; } @mixin gradient-vertical-three-colors($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) { background-color: mix($midColor, $endColor, 80%); - background-image: linear-gradient($startColor, $midColor $colorStop, $endColor); + background-image: linear-gradient( + $startColor, + $midColor $colorStop, + $endColor + ); background-repeat: no-repeat; } @mixin gradient-radial($innerColor: #555, $outerColor: #333) { background-color: $outerColor; - background-image: -webkit-gradient(radial, center center, 0, center center, 460, from($innerColor), to($outerColor)); + background-image: -webkit-gradient( + radial, + center center, + 0, + center center, + 460, + from($innerColor), + to($outerColor) + ); background-image: -webkit-radial-gradient(circle, $innerColor, $outerColor); background-image: -moz-radial-gradient(circle, $innerColor, $outerColor); background-image: -o-radial-gradient(circle, $innerColor, $outerColor); @@ -327,16 +362,29 @@ @mixin striped($color: #555, $angle: 45deg) { background-color: $color; - background-image: linear-gradient($angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); + background-image: linear-gradient( + $angle, + rgba(255, 255, 255, 0.15) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.15) 75%, + transparent 75%, + transparent + ); } -@mixin left-brand-border() { - border-left: 2px solid transparent; +@mixin left-brand-border($color: transparent) { + border-left: 2px solid $color; } @mixin left-brand-border-gradient() { border: none; - border-image: linear-gradient(rgba(255,213,0,1) 0%, rgba(255,68,0,1) 99%, rgba(255,68,0,1) 100%); + border-image: linear-gradient( + rgba(255, 213, 0, 1) 0%, + rgba(255, 68, 0, 1) 99%, + rgba(255, 68, 0, 1) 100% + ); border-image-slice: 1; border-style: solid; border-top: 0; @@ -355,4 +403,16 @@ border-bottom-width: 1px; } +@mixin list-item() { + display: block; + margin: 3px; + padding: 7px; + background: $list-item-bg; + box-shadow: $list-item-shadow; + color: $list-item-link-color; + &:hover { + background: $list-item-hover-bg; + color: $list-item-link-color; + } +} diff --git a/public/sass/pages/_admin.scss b/public/sass/pages/_admin.scss index b2be062849d..32f47212131 100644 --- a/public/sass/pages/_admin.scss +++ b/public/sass/pages/_admin.scss @@ -1,4 +1,3 @@ - .admin-settings-section { color: $variable; font-weight: bold; diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index 6db3470f3ea..44c4fabbd5b 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -41,7 +41,8 @@ display: flex; justify-content: center; align-items: center; - .icon-gf, .fa { + .icon-gf, + .fa { font-size: 200%; position: relative; top: 2px; @@ -102,7 +103,7 @@ .panel-alert-state { &--alerting { - box-shadow: 0 0 10px rgba($critical,0.5); + box-shadow: 0 0 10px rgba($critical, 0.5); position: relative; .panel-alert-icon:before { @@ -112,18 +113,18 @@ } &--alerting::after { - content: ''; + content: ""; position: absolute; top: 0; z-index: -1; width: 100%; height: 100%; - box-shadow: 0 0 10px rgba($critical,1); + box-shadow: 0 0 10px rgba($critical, 1); opacity: 0; - animation: alerting-panel 1.6s cubic-bezier(1,.1,.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; diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 7aacbce4f6b..af9a02caa2a 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -1,47 +1,40 @@ .dashboard-container { padding: $dashboard-padding; width: 100%; -} - -.page-dashboard { - .main-view { - background-image: none; - } + min-height: 100%; + // background: $dashboard-gradient; } .template-variable { color: $variable; } - - div.flot-text { color: $text-color !important; } .panel { - display: inline-block; - float: left; + height: 100%; &--solo { - .resize-panel-handle { - display: none; - } .panel-container { border: none; + z-index: $zindex-sidemenu + 1; } } } -.panel-margin { - margin: 0 0.4rem 0.8rem 0.4rem; +.panel-height-helper { display: block; + height: 100%; } .panel-container { background-color: $panel-bg; - position: relative; border: $panel-border; + position: relative; + border-radius: 3px; + height: 100%; &.panel-transparent { background-color: transparent; @@ -50,36 +43,82 @@ div.flot-text { } .panel-content { - padding: 0px 10px 5px 10px; + padding: $panel-padding; + height: calc(100% - 27px); + position: relative; + overflow: hidden; } .panel-title-container { min-height: 9px; - padding-top: 4px; - cursor: pointer; + cursor: move; word-wrap: break-word; + display: block; } .panel-title { border: 0px; font-weight: $font-weight-semi-bold; position: relative; - cursor: pointer; width: 100%; display: block; + padding-bottom: 2px; +} + +.panel-title-text { + cursor: pointer; + font-weight: $font-weight-semi-bold; + + &:hover { + color: $link-hover-color; + } +} + +.panel-menu-container { + width: 1px; + height: 19px; + display: inline-block; +} + +.panel-menu-toggle { + color: $text-color-weak; + cursor: pointer; + padding: 3px 5px; + visibility: hidden; + opacity: 0; + position: absolute; + width: 16px; + height: 16px; + left: 1px; + top: 4px; + + &:hover { + color: $link-hover-color; + } } .panel-loading { - position:absolute; + position: absolute; top: -3px; right: 0px; z-index: 800; + font-size: $font-size-sm; + color: $text-color-weak; } .panel-header { text-align: center; + + &:hover { + transition: background-color 0.1s ease-in-out; + background-color: $panel-header-hover-bg; + } } +.panel-menu { + top: 25px; + left: -100px; +} .panel-info-corner-inner { width: 0; @@ -106,14 +145,14 @@ div.flot-text { width: 27px; height: 27px; top: 0; - z-index: 10; + z-index: 1; .fa { position: relative; top: -4px; left: -6px; font-size: 75%; - z-index: 1000; + z-index: 1; } &--info { @@ -145,73 +184,11 @@ div.flot-text { } } -.panel-full-edit { - margin-top: 20px; - margin-bottom: 20px; -} - -.panel-menu { - z-index: 500; - position: absolute; - background: $tight-form-func-bg; - border: $panel-menu-border; - - .panel-menu-row { - white-space: nowrap; - border-bottom: $panel-menu-border; - &:last-child { - border-bottom: none; - } - } - - .panel-menu-link, .panel-menu-icon { - padding: 5px 10px; - } - - .panel-menu-link { - display: inline-block; - border-right: $panel-menu-border; - &:last-child { - border: none; - } - } - - .dropdown-menu { - text-align: left; - } -} - -.panel-highlight { - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 5px rgba(82,168,236,10.8) -} - -// .panel-hover-highlight { -// box-shadow: inset 0 1px 1px rgba(0,0,0,0.025), 0 0 1px rgba(82,168,236,0.5) -// } - -.on-drag-hover { - .panel-container { - box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 5px rgba(82,168,236,10.8) - } -} - -.panel-drop-zone { - display: none; - .panel-container { - border: $panel-border; - display: flex; - justify-content: center; - flex-direction: column; - text-align: center; - color: $text-color-faint; - font-weight: bold; - background: $panel-drop-zone-bg; - } -} - -.panel-in-fullscreen { - .panel-drop-zone { - display: none !important; +.panel-hover-highlight { + .panel-menu-toggle { + visibility: visible; + transition: opacity 0.1s ease-in 0.2s; + opacity: 1; } } @@ -222,45 +199,24 @@ div.flot-text { color: $blue; font-size: 85%; position: absolute; - top: 0; + top: 4px; right: 0; } -.resize-panel-handle { - cursor: nwse-resize; - position: absolute; - font-size: 10px; - bottom: 0; - right: 0; - width: 15px; - height: 15px; - display: block; - color: $text-color-faint; - - &:before { - left: initial; - right: -1px; - bottom: 0px; - position: absolute; - } -} - .dashboard-header { font-family: $headings-font-family; font-size: $font-size-h3; text-align: center; + overflow: hidden; + position: relative; + top: -10px; span { display: inline-block; @include brand-bottom-border(); - padding: 0.5rem .5rem .2rem .5rem; + padding: 0.5rem 0.5rem 0.2rem 0.5rem; } } -.dash-edit-view { - opacity: 0; - - &--open{ - opacity: 1; - transition: opacity 200ms ease-in-out; - } +.panel-full-edit { + margin: $dashboard-padding (-$dashboard-padding) 0 (-$dashboard-padding); } diff --git a/public/sass/pages/_errorpage.scss b/public/sass/pages/_errorpage.scss index e18306ea05a..51aba412f86 100644 --- a/public/sass/pages/_errorpage.scss +++ b/public/sass/pages/_errorpage.scss @@ -1,60 +1,106 @@ - // // Layout // +.error-container { + display: flex; + flex-direction: row; +} + .error-row { - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; } .error-column { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } -.error-space-between {justify-content: space-between;} +.error-space-between { + justify-content: space-between; +} .graph-box { - width: 62%; - padding: 2rem 1rem; + width: 62%; + padding: 2rem 1rem; } .info-box { - width: 38%; - padding: 2rem 1rem 6rem; + width: 38%; + padding: 2rem 1rem 2rem; } -.graph-percentage {padding: 0 0 1.5rem;} +.graph-percentage { + padding: 0 0 1.5rem; +} -.image-box {padding: .5rem} +.image-box { + padding: 0.5rem; +} -.left-margin{padding: 0 0 0 5rem;} +.left-margin { + padding: 0 0 0 5rem; +} -.current-box {justify-content: flex-end;} +.current-box { + justify-content: flex-end; +} // // Text // .current-text { - color: $blue; - font-weight: bold; - line-height: 1rem; + color: $blue; + font-weight: bold; + line-height: 1rem; } -.error-link {color: $orange;} +.error-link { + color: $orange; +} .error-minus { - color: #7eb26d; - padding: 0 .5rem; - line-height: 1.5rem; + color: #7eb26d; + padding: 0 0.5rem; + line-height: 1.5rem; } .graph-percentage p { - text-align: right; - margin: 0; - line-height: 1rem; + text-align: right; + margin: 0; + line-height: 1rem; } -.graph-text {margin: 0;} +.graph-text { + margin: 0; +} + +@include media-breakpoint-down(sm) { + .graph-box { + width: 50%; + } + + .info-box { + width: 50%; + } +} + +@include media-breakpoint-down(xs) { + .error-container { + flex-direction: column; + } + + .graph-box { + width: 100%; + } + + .info-box { + width: 100%; + } + + .error-full-width { + width: 100%; + } +} diff --git a/public/sass/pages/_history.scss b/public/sass/pages/_history.scss index 707a3a40545..ea845b78445 100644 --- a/public/sass/pages/_history.scss +++ b/public/sass/pages/_history.scss @@ -29,10 +29,13 @@ white-space: nowrap; position: relative; - &:before, &:after { + &:before, + &:after { } - &:after { left: -40px; } + &:after { + left: -40px; + } } .diff-line-number { @@ -44,7 +47,9 @@ width: 30px; } -.diff-line-number-hide { visibility: hidden; } +.diff-line-number-hide { + visibility: hidden; +} .diff-line-icon { color: $diff-json-icon; @@ -58,28 +63,49 @@ .diff-json-new, .diff-json-old, .diff-json-deleted, -.diff-json-added, { +.diff-json-added { color: $diff-json-changed-fg; - & .diff-line-number { color: $diff-json-changed-num; } + & .diff-line-number { + color: $diff-json-changed-num; + } } -.diff-json-new { background-color: $diff-json-new; } -.diff-json-old { background-color: $diff-json-old; } -.diff-json-added { background-color: $diff-json-added; } -.diff-json-deleted { background-color: $diff-json-deleted; } +.diff-json-new { + background-color: $diff-json-new; +} +.diff-json-old { + background-color: $diff-json-old; +} +.diff-json-added { + background-color: $diff-json-added; +} +.diff-json-deleted { + background-color: $diff-json-deleted; +} .diff-value { user-select: all; } // Basic -.diff-circle { margin-right: .5em; } -.diff-circle-changed { color: #f59433; } -.diff-circle-added { color: #29D761; } -.diff-circle-deleted { color: #fd474a; } +.diff-circle { + margin-right: 0.5em; +} +.diff-circle-changed { + color: #f59433; +} +.diff-circle-added { + color: #29d761; +} +.diff-circle-deleted { + color: #fd474a; +} -.diff-item-added, .diff-item-deleted { list-style: none; } +.diff-item-added, +.diff-item-deleted { + list-style: none; +} .diff-group { background: $diff-group-bg; @@ -88,7 +114,9 @@ padding: 10px 15px; margin: 1rem 0; - & .diff-group { padding: 0 5px; } + & .diff-group { + padding: 0 5px; + } } .diff-group-name { @@ -100,20 +128,24 @@ } .diff-summary-key { - padding-left: .25em; + padding-left: 0.25em; } .diff-list { padding-left: 40px; - & .diff-list { padding-left: 0; } + & .diff-list { + padding-left: 0; + } } .diff-item { color: $gray-2; line-height: 2.5; - & > div { display: inline; } + & > div { + display: inline; + } } .diff-item-changeset { @@ -125,7 +157,7 @@ border-radius: 3px; color: $diff-label-fg; display: inline; - font-size: .95rem; + font-size: 0.95rem; margin: 0 5px; padding: 3px 8px; } @@ -160,7 +192,7 @@ .diff-change-group { width: 100%; - color: rgba(223,224,225, .6); + color: rgba(223, 224, 225, 0.6); margin-bottom: 14px; } diff --git a/public/sass/pages/_login.scss b/public/sass/pages/_login.scss index 2b3426b45b5..8622eec4e99 100644 --- a/public/sass/pages/_login.scss +++ b/public/sass/pages/_login.scss @@ -1,42 +1,113 @@ -.login-container { - background-position: left; - background-size: 60%; +$login-border: #8daac5; + +.login { + background-position: center; + min-height: 85vh; background-repeat: no-repeat; min-width: 100%; margin-left: 0; - margin-top: -26px; /* BAD HACK - experiement to see how it looks */ - padding-top: $spacer * 5; /* BAD HACK - experiement to see how it looks */ + background-color: $black; + display: flex; + align-items: center; + justify-content: center; + background-image: url(../img/heatmap_bg_test.svg); + background-size: cover; +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill, +textarea:-webkit-autofill, +textarea:-webkit-autofill:hover, +textarea:-webkit-autofill:focus, +select:-webkit-autofill, +select:-webkit-autofill:hover, +select:-webkit-autofill:focus { + -webkit-box-shadow: 0 0 0px 1000px $black inset; + -webkit-text-fill-color: $gray-7 !important; +} + +.login-form-group { + display: flex; + flex-direction: column; + width: 100%; + align-items: center; + margin-bottom: 1rem; } .login-form { - display: inline-block; - max-width: 24rem; + margin-bottom: 1rem; + width: 100%; } -.login-box { - max-width: 700px; - margin: 0 auto; /* was $spacer * 2 auto 0 auto; */ -} +.login-form-input { + border: 1px solid $login-border; + border-radius: 4px; + opacity: 0.6; -.login-box-logo { - text-align: center; - margin-bottom: $spacer * 2; - img { - width: 6rem; + &:focus { + border: 1px solid $login-border; } +} + +.login-button-group { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + width: 100%; + margin-top: 0.5rem; +} + +.login-button-forgot-password { + padding-top: 1rem; +} + +.login-text { + font-size: $font-size-sm; +} + +.login-content { + max-width: 700px; + display: flex; + align-items: stretch; + flex-direction: column; + position: relative; + z-index: 1; +} + +.login-branding { + width: 100%; + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + justify-content: center; + flex-grow: 0; + + .logo-icon { + width: 70px; + margin-bottom: 15px; + } + .icon-gf-grafana_wordmark { color: $link-color; position: relative; - top: -4.5rem; - font-size: 2.5rem; - text-shadow: 3px 3px 5px black; + font-size: 2rem; + text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3); } } .login-inner-box { - background: $panel-bg; text-align: center; - padding-bottom: 3rem; + padding: 2rem 4rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-grow: 1; + max-width: 415px; } .login-tab-header { @@ -45,6 +116,12 @@ margin-bottom: 3rem; } +.btn-signup { + color: $white; + border: 1px solid $login-border; + background-color: $btn-semi-transparent; +} + .btn-login-tab { background: transparent; border: none; @@ -98,30 +175,7 @@ } .login-oauth { - - .btn { - margin: 5px; - } - - .btn-google { - background: #dd4b39; - color: white; - } - - .btn-github { - background: #555; - color: white; - } - - .btn-grafana-com { - @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color); - box-shadow: $card-shadow; - - img { - width: 19px; - vertical-align: sub; - } - } + width: 100%; } .password-recovery { @@ -134,13 +188,15 @@ .login-divider { float: left; - width: 50%; - margin: 5px 25% 25px 25%; + width: 100%; + margin: 0 25% 1rem 25%; + display: flex; + justify-content: space-between; .login-divider-line { - width: 100%; + width: 110px; height: 10px; - border-bottom: 1px solid $gray-1; + border-bottom: 1px solid $login-border; .login-divider-text { background-color: $panel-bg; @@ -150,6 +206,25 @@ } } +.login-signup-box { + display: flex; + align-items: center; + justify-content: flex-end; + width: 100%; + margin-top: 1rem; +} + +.login-signup-title { + justify-self: flex-start; + flex: 1; + text-align: left; +} + +.login-btn { + width: 100%; + margin: 0 0 1rem; +} + .signup-page-background { position: fixed; top: 0; @@ -190,14 +265,135 @@ } } -@include media-breakpoint-up(md) { - .login-box-logo { - img { - width: 125px; - } - .icon-gf-grafana_wordmark { - top: -5px; - font-size: 3rem; +@include media-breakpoint-up(sm) { + .login-content { + flex-direction: row; + } + + .login-branding { + width: 35%; + padding: 4rem 2rem; + border-right: 1px solid $login-border; + justify-content: flex-start; + } + + .login-inner-box { + width: 65%; + padding: 1rem 2rem; + } + + .login-branding { + .logo-icon { + width: 80px; } } } + +@include media-breakpoint-up(md) { + .login-branding { + width: 45%; + padding: 2rem 4rem; + + .logo-icon { + width: 130px; + } + + .icon-gf-grafana_wordmark { + font-size: 3.2rem; + } + } + + .login-inner-box { + width: 55%; + padding: 1rem 4rem; + } + + .login-button-group { + flex-direction: row; + } + + .login-button-forgot-password { + padding-top: 0; + padding-left: 10px; + } +} + +@include media-breakpoint-up(lg) { + .login { + min-height: 100vh; + } + + .login-form-input { + min-width: 300px; + } +} + +.login-bg { + position: absolute; + top: 0; + left: 0; + right: 0; + perspective: 1000px; + display: flex; + flex-wrap: wrap; + z-index: 0; + flex-direction: column; + justify-content: stretch; + justify-items: stretch; + height: 100%; + + .login-bg__row { + display: flex; + flex-grow: 1; + height: 10px; + justify-content: stretch; + } + + .login-bg__item { + width: 4%; + height: 100%; + flex-grow: 1; + // background: hotpink; + // border:1px solid #0F1926; + transition: 1s ease-in-out; + z-index: 1; + transform-style: preserve-3d; + + &.login-bg-flip { + transform: rotateY(180deg); + } + + &:before, + &:after { + backface-visibility: hidden; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + content: ""; + display: block; + } + + &:after { + transform: rotateY(180deg); + background-color: rgb(25, 50, 80); + } + } +} + +.login-bg-fx { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + background-image: -webkit-radial-gradient( + center center, + ellipse farthest-corner, + transparent 0%, + transparent 10%, + rgba(18, 22, 29, 1) 100% + ); + z-index: 2; +} diff --git a/public/sass/pages/_playlist.scss b/public/sass/pages/_playlist.scss index d0d8f04f130..5dd1c92cbd2 100644 --- a/public/sass/pages/_playlist.scss +++ b/public/sass/pages/_playlist.scss @@ -27,7 +27,6 @@ margin-bottom: 15px; } - .playlist-search-field-wrapper { input { width: 100%; @@ -50,18 +49,20 @@ display: block; line-height: 28px; - .search-item:hover, .search-item.selected { - background-color: $grafanaListHighlight; + .search-item:hover, + .search-item.selected { + background-color: $list-item-hover-bg; } .selected { .search-result-tag { - opacity: 0.70; + opacity: 0.7; color: white; } } - .fa-star, .fa-star-o { + .fa-star, + .fa-star-o { padding-left: 13px; } @@ -70,7 +71,7 @@ } .search-result-link { - color: $grafanaListMainLinkColor; + color: $list-item-link-color; .fa { padding-right: 10px; } @@ -80,7 +81,7 @@ display: block; padding: 3px 10px; white-space: nowrap; - background-color: $grafanaListBackground; + background-color: $list-item-bg; margin-bottom: 4px; .search-result-icon:before { content: "\f009"; diff --git a/public/sass/pages/_plugins.scss b/public/sass/pages/_plugins.scss index c93d94f40b5..15149dd0d35 100644 --- a/public/sass/pages/_plugins.scss +++ b/public/sass/pages/_plugins.scss @@ -1,42 +1,3 @@ -.plugin-header { - @include clearfix(); - - padding: $spacer 0 $spacer/2 0; - margin-bottom: 2rem; -} - -.plugin-header-logo { - float: left; - width: 7rem; - img { - width: 7rem; - } - margin-right: $spacer; -} - -.plugin-header-info-block { - float: left; -} - -.plugin-header-author { -} - -.plugin-header-stamps-type { - color: $link-color-disabled; - text-transform: uppercase; -} - -.plugin-info-list-item { - img { - width: 16px; - } - - white-space: nowrap; - max-width: $page-sidebar-width; - text-overflow: ellipsis; - overflow: hidden; -} - .get-more-plugins-link { color: $gray-3; font-size: $font-size-sm; @@ -55,3 +16,14 @@ display: none; } } + +.plugin-info-list-item { + img { + width: 16px; + } + + white-space: nowrap; + max-width: $page-sidebar-width; + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/public/sass/pages/_signup.scss b/public/sass/pages/_signup.scss index 7b2b0b0e84a..2c9d50ebc5d 100644 --- a/public/sass/pages/_signup.scss +++ b/public/sass/pages/_signup.scss @@ -15,3 +15,8 @@ } } +.signup__password-strength { + position: absolute; + margin-left: 9rem; + width: 194px; +} diff --git a/public/sass/pages/_styleguide.scss b/public/sass/pages/_styleguide.scss index ada2f533eca..ef4af9e4930 100644 --- a/public/sass/pages/_styleguide.scss +++ b/public/sass/pages/_styleguide.scss @@ -9,9 +9,15 @@ font-size: $font-size-sm; } -.color-card-body-bg { background-color: $body-bg; } -.color-card-page-bg { background-color: $page-bg; } -.color-card-gray { background-color: $gray-1; } +.color-card-body-bg { + background-color: $body-bg; +} +.color-card-page-bg { + background-color: $page-bg; +} +.color-card-gray { + background-color: $gray-1; +} .style-guide-button-list { padding: $spacer; @@ -25,4 +31,3 @@ font-size: 1.8em; text-align: center; } - diff --git a/public/sass/utils/_angular.scss b/public/sass/utils/_angular.scss index 1519b249920..53ea1573245 100644 --- a/public/sass/utils/_angular.scss +++ b/public/sass/utils/_angular.scss @@ -1,7 +1,5 @@ - - -[ng\:cloak], [ng-cloak], .ng-cloak { +[ng\:cloak], +[ng-cloak], +.ng-cloak { display: none !important; } - - diff --git a/public/sass/utils/_flex.scss b/public/sass/utils/_flex.scss index bcf2c2fa2a5..f1e115528a7 100644 --- a/public/sass/utils/_flex.scss +++ b/public/sass/utils/_flex.scss @@ -2,31 +2,57 @@ @each $breakpoint in map-keys($grid-breakpoints) { // Flex column reordering @include media-breakpoint-up($breakpoint) { - .flex-#{$breakpoint}-first { order: -1; } - .flex-#{$breakpoint}-last { order: 1; } + .flex-#{$breakpoint}-first { + order: -1; + } + .flex-#{$breakpoint}-last { + order: 1; + } } // Alignment for every item @include media-breakpoint-up($breakpoint) { - .flex-items-#{$breakpoint}-top { align-items: flex-start; } - .flex-items-#{$breakpoint}-middle { align-items: center; } - .flex-items-#{$breakpoint}-bottom { align-items: flex-end; } + .flex-items-#{$breakpoint}-top { + align-items: flex-start; + } + .flex-items-#{$breakpoint}-middle { + align-items: center; + } + .flex-items-#{$breakpoint}-bottom { + align-items: flex-end; + } } // Alignment per item @include media-breakpoint-up($breakpoint) { - .flex-#{$breakpoint}-top { align-self: flex-start; } - .flex-#{$breakpoint}-middle { align-self: center; } - .flex-#{$breakpoint}-bottom { align-self: flex-end; } + .flex-#{$breakpoint}-top { + align-self: flex-start; + } + .flex-#{$breakpoint}-middle { + align-self: center; + } + .flex-#{$breakpoint}-bottom { + align-self: flex-end; + } } // Horizontal alignment of item @include media-breakpoint-up($breakpoint) { - .flex-items-#{$breakpoint}-left { justify-content: flex-start; } - .flex-items-#{$breakpoint}-center { justify-content: center; } - .flex-items-#{$breakpoint}-right { justify-content: flex-end; } - .flex-items-#{$breakpoint}-around { justify-content: space-around; } - .flex-items-#{$breakpoint}-between { justify-content: space-between; } + .flex-items-#{$breakpoint}-left { + justify-content: flex-start; + } + .flex-items-#{$breakpoint}-center { + justify-content: center; + } + .flex-items-#{$breakpoint}-right { + justify-content: flex-end; + } + .flex-items-#{$breakpoint}-around { + justify-content: space-around; + } + .flex-items-#{$breakpoint}-between { + justify-content: space-between; + } } } } diff --git a/public/sass/utils/_spacings.scss b/public/sass/utils/_spacings.scss index f106c0d826a..57f931ff2d7 100644 --- a/public/sass/utils/_spacings.scss +++ b/public/sass/utils/_spacings.scss @@ -2,33 +2,42 @@ .m-x-auto { margin-right: auto !important; - margin-left: auto !important; + margin-left: auto !important; } @each $prop, $abbrev in (margin: m, padding: p) { @each $size, $lengths in $spacers { - $length-x: map-get($lengths, x); - $length-y: map-get($lengths, y); + $length-x: map-get($lengths, x); + $length-y: map-get($lengths, y); - .#{$abbrev}-a-#{$size} { #{$prop}: $length-y $length-x !important; } // a = All sides - .#{$abbrev}-t-#{$size} { #{$prop}-top: $length-y !important; } - .#{$abbrev}-r-#{$size} { #{$prop}-right: $length-x !important; } - .#{$abbrev}-b-#{$size} { #{$prop}-bottom: $length-y !important; } - .#{$abbrev}-l-#{$size} { #{$prop}-left: $length-x !important; } + .#{$abbrev}-a-#{$size} { + #{$prop}: $length-y $length-x !important; + } // a = All sides + .#{$abbrev}-t-#{$size} { + #{$prop}-top: $length-y !important; + } + .#{$abbrev}-r-#{$size} { + #{$prop}-right: $length-x !important; + } + .#{$abbrev}-b-#{$size} { + #{$prop}-bottom: $length-y !important; + } + .#{$abbrev}-l-#{$size} { + #{$prop}-left: $length-x !important; + } // Axes .#{$abbrev}-x-#{$size} { - #{$prop}-right: $length-x !important; - #{$prop}-left: $length-x !important; + #{$prop}-right: $length-x !important; + #{$prop}-left: $length-x !important; } .#{$abbrev}-y-#{$size} { - #{$prop}-top: $length-y !important; + #{$prop}-top: $length-y !important; #{$prop}-bottom: $length-y !important; } } } - // Positioning .pos-f-t { diff --git a/public/sass/utils/_utils.scss b/public/sass/utils/_utils.scss index 39e4edfe7ed..60a21e5a675 100644 --- a/public/sass/utils/_utils.scss +++ b/public/sass/utils/_utils.scss @@ -21,7 +21,7 @@ font-weight: bold; line-height: $line-height-base; color: $black; - text-shadow: 0 1px 0 rgba(255,255,255,1); + text-shadow: 0 1px 0 rgba(255, 255, 255, 1); &:hover, &:focus { @@ -47,7 +47,6 @@ button.close { // Utility classes // -------------------------------------------------- - // Quick floats .pull-right { float: right !important; diff --git a/public/sass/utils/_validation.scss b/public/sass/utils/_validation.scss index c0dd44d59a0..86b7c008bfd 100644 --- a/public/sass/utils/_validation.scss +++ b/public/sass/utils/_validation.scss @@ -1,10 +1,7 @@ -input[type=text].ng-dirty.ng-invalid { +input[type="text"].ng-dirty.ng-invalid { } input.validation-error, input.ng-dirty.ng-invalid { box-shadow: inset 0 0px 5px $red; } - - - diff --git a/public/sass/utils/_widths.scss b/public/sass/utils/_widths.scss index cf324b35c72..2000982f08d 100644 --- a/public/sass/utils/_widths.scss +++ b/public/sass/utils/_widths.scss @@ -1,7 +1,9 @@ - - -.max-width { width: 100%; } -.width-auto { width: auto; } +.max-width { + width: 100%; +} +.width-auto { + width: auto; +} // widths @for $i from 1 through 30 { @@ -22,4 +24,3 @@ margin-left: ($spacer * $i) !important; } } - diff --git a/public/test/mocks/backend_srv.ts b/public/test/mocks/backend_srv.ts new file mode 100644 index 00000000000..666de593722 --- /dev/null +++ b/public/test/mocks/backend_srv.ts @@ -0,0 +1,8 @@ +export class BackendSrvMock { + search: any; + + constructor() { + } + +} + diff --git a/public/test/specs/helpers.d.ts b/public/test/specs/helpers.d.ts deleted file mode 100644 index 4b64bd79031..00000000000 --- a/public/test/specs/helpers.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare let helpers: any; -export default helpers; - - diff --git a/public/test/specs/helpers.js b/public/test/specs/helpers.js deleted file mode 100644 index eae57be7115..00000000000 --- a/public/test/specs/helpers.js +++ /dev/null @@ -1,182 +0,0 @@ -define([ - 'lodash', - 'app/core/config', - 'app/core/utils/datemath', -], function(_, config, dateMath) { - 'use strict'; - - config = config.default; - - function ControllerTestContext() { - var self = this; - - this.datasource = {}; - this.$element = {}; - this.annotationsSrv = {}; - this.timeSrv = new TimeSrvStub(); - this.templateSrv = new TemplateSrvStub(); - this.datasourceSrv = { - getMetricSources: function() {}, - get: function() { - return { - then: function(callback) { - callback(self.datasource); - } - }; - } - }; - - this.providePhase = function(mocks) { - return window.module(function($provide) { - $provide.value('datasourceSrv', self.datasourceSrv); - $provide.value('annotationsSrv', self.annotationsSrv); - $provide.value('timeSrv', self.timeSrv); - $provide.value('templateSrv', self.templateSrv); - $provide.value('$element', self.$element); - _.each(mocks, function(value, key) { - $provide.value(key, value); - }); - }); - }; - - this.createPanelController = function(Ctrl) { - return window.inject(function($controller, $rootScope, $q, $location, $browser) { - self.scope = $rootScope.$new(); - self.$location = $location; - self.$browser = $browser; - self.$q = $q; - self.panel = {type: 'test'}; - self.dashboard = {meta: {}}; - - $rootScope.appEvent = sinon.spy(); - $rootScope.onAppEvent = sinon.spy(); - $rootScope.colors = []; - - for (var i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); } - - config.panels['test'] = {info: {}}; - self.ctrl = $controller(Ctrl, {$scope: self.scope}, { - panel: self.panel, dashboard: self.dashboard, row: {} - }); - }); - }; - - this.createControllerPhase = function(controllerName) { - return window.inject(function($controller, $rootScope, $q, $location, $browser) { - self.scope = $rootScope.$new(); - self.$location = $location; - self.$browser = $browser; - self.scope.contextSrv = {}; - self.scope.panel = {}; - self.scope.row = { panels:[] }; - self.scope.dashboard = {meta: {}}; - self.scope.dashboardMeta = {}; - self.scope.dashboardViewState = new DashboardViewStateStub(); - self.scope.appEvent = sinon.spy(); - self.scope.onAppEvent = sinon.spy(); - - $rootScope.colors = []; - for (var i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); } - - self.$q = $q; - self.scope.skipDataOnInit = true; - self.scope.skipAutoInit = true; - self.controller = $controller(controllerName, { - $scope: self.scope - }); - }); - }; - } - - function ServiceTestContext() { - var self = this; - self.templateSrv = new TemplateSrvStub(); - self.timeSrv = new TimeSrvStub(); - self.datasourceSrv = {}; - self.backendSrv = {}; - self.$routeParams = {}; - - this.providePhase = function(mocks) { - return window.module(function($provide) { - _.each(mocks, function(key) { - $provide.value(key, self[key]); - }); - }); - }; - - this.createService = function(name) { - return window.inject(function($q, $rootScope, $httpBackend, $injector, $location, $timeout) { - self.$q = $q; - self.$rootScope = $rootScope; - self.$httpBackend = $httpBackend; - self.$location = $location; - - self.$rootScope.onAppEvent = function() {}; - self.$rootScope.appEvent = function() {}; - self.$timeout = $timeout; - - self.service = $injector.get(name); - }); - }; - } - - function DashboardViewStateStub() { - this.registerPanel = function() { - }; - } - - function TimeSrvStub() { - this.init = sinon.spy(); - this.time = { from:'now-1h', to: 'now'}; - this.timeRange = function(parse) { - if (parse === false) { - return this.time; - } - return { - from : dateMath.parse(this.time.from, false), - to : dateMath.parse(this.time.to, true) - }; - }; - - this.replace = function(target) { - return target; - }; - - this.setTime = function(time) { - this.time = time; - }; - } - - function ContextSrvStub() { - this.hasRole = function() { - return true; - }; - } - - function TemplateSrvStub() { - this.variables = []; - this.templateSettings = { interpolate : /\[\[([\s\S]+?)\]\]/g }; - this.data = {}; - this.replace = function(text) { - return _.template(text, this.templateSettings)(this.data); - }; - this.init = function() {}; - this.getAdhocFilters = function() { return []; }; - this.fillVariableValuesForUrl = function() {}; - this.updateTemplateData = function() { }; - this.variableExists = function() { return false; }; - this.variableInitialized = function() { }; - this.highlightVariablesAsHtml = function(str) { return str; }; - this.setGrafanaVariable = function(name, value) { - this.data[name] = value; - }; - } - - return { - ControllerTestContext: ControllerTestContext, - TimeSrvStub: TimeSrvStub, - ContextSrvStub: ContextSrvStub, - ServiceTestContext: ServiceTestContext - }; - -}); diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts new file mode 100644 index 00000000000..8e83915362f --- /dev/null +++ b/public/test/specs/helpers.ts @@ -0,0 +1,195 @@ +import _ from 'lodash'; +import config from 'app/core/config'; +import * as dateMath from 'app/core/utils/datemath'; +import {angularMocks, sinon} from '../lib/common'; +import {PanelModel} from 'app/features/dashboard/panel_model'; + +export function ControllerTestContext() { + var self = this; + + this.datasource = {}; + this.$element = {}; + this.annotationsSrv = {}; + this.timeSrv = new TimeSrvStub(); + this.templateSrv = new TemplateSrvStub(); + this.datasourceSrv = { + getMetricSources: function() {}, + get: function() { + return { + then: function(callback) { + callback(self.datasource); + }, + }; + }, + }; + + this.providePhase = function(mocks) { + return angularMocks.module(function($provide) { + $provide.value('datasourceSrv', self.datasourceSrv); + $provide.value('annotationsSrv', self.annotationsSrv); + $provide.value('timeSrv', self.timeSrv); + $provide.value('templateSrv', self.templateSrv); + $provide.value('$element', self.$element); + _.each(mocks, function(value, key) { + $provide.value(key, value); + }); + }); + }; + + this.createPanelController = function(Ctrl) { + return angularMocks.inject(function($controller, $rootScope, $q, $location, $browser) { + self.scope = $rootScope.$new(); + self.$location = $location; + self.$browser = $browser; + self.$q = $q; + self.panel = new PanelModel({type: 'test'}); + self.dashboard = {meta: {}}; + + $rootScope.appEvent = sinon.spy(); + $rootScope.onAppEvent = sinon.spy(); + $rootScope.colors = []; + + for (var i = 0; i < 50; i++) { + $rootScope.colors.push('#' + i); + } + + config.panels['test'] = {info: {}}; + self.ctrl = $controller( + Ctrl, + {$scope: self.scope}, + { + panel: self.panel, + dashboard: self.dashboard, + }, + ); + }); + }; + + this.createControllerPhase = function(controllerName) { + return angularMocks.inject(function($controller, $rootScope, $q, $location, $browser) { + self.scope = $rootScope.$new(); + self.$location = $location; + self.$browser = $browser; + self.scope.contextSrv = {}; + self.scope.panel = {}; + self.scope.dashboard = {meta: {}}; + self.scope.dashboardMeta = {}; + self.scope.dashboardViewState = new DashboardViewStateStub(); + self.scope.appEvent = sinon.spy(); + self.scope.onAppEvent = sinon.spy(); + + $rootScope.colors = []; + for (var i = 0; i < 50; i++) { + $rootScope.colors.push('#' + i); + } + + self.$q = $q; + self.scope.skipDataOnInit = true; + self.scope.skipAutoInit = true; + self.controller = $controller(controllerName, { + $scope: self.scope, + }); + }); + }; +} + +export function ServiceTestContext() { + var self = this; + self.templateSrv = new TemplateSrvStub(); + self.timeSrv = new TimeSrvStub(); + self.datasourceSrv = {}; + self.backendSrv = {}; + self.$routeParams = {}; + + this.providePhase = function(mocks) { + return angularMocks.module(function($provide) { + _.each(mocks, function(key) { + $provide.value(key, self[key]); + }); + }); + }; + + this.createService = function(name) { + return angularMocks.inject(function($q, $rootScope, $httpBackend, $injector, $location, $timeout) { + self.$q = $q; + self.$rootScope = $rootScope; + self.$httpBackend = $httpBackend; + self.$location = $location; + + self.$rootScope.onAppEvent = function() {}; + self.$rootScope.appEvent = function() {}; + self.$timeout = $timeout; + + self.service = $injector.get(name); + }); + }; +} + +export function DashboardViewStateStub() { + this.registerPanel = function() {}; +} + +export function TimeSrvStub() { + this.init = sinon.spy(); + this.time = {from: 'now-1h', to: 'now'}; + this.timeRange = function(parse) { + if (parse === false) { + return this.time; + } + return { + from: dateMath.parse(this.time.from, false), + to: dateMath.parse(this.time.to, true), + }; + }; + + this.replace = function(target) { + return target; + }; + + this.setTime = function(time) { + this.time = time; + }; +} + +export function ContextSrvStub() { + this.hasRole = function() { + return true; + }; +} + +export function TemplateSrvStub() { + this.variables = []; + this.templateSettings = {interpolate: /\[\[([\s\S]+?)\]\]/g}; + this.data = {}; + this.replace = function(text) { + return _.template(text, this.templateSettings)(this.data); + }; + this.init = function() {}; + this.getAdhocFilters = function() { + return []; + }; + this.fillVariableValuesForUrl = function() {}; + this.updateTemplateData = function() {}; + this.variableExists = function() { + return false; + }; + this.variableInitialized = function() {}; + this.highlightVariablesAsHtml = function(str) { + return str; + }; + this.setGrafanaVariable = function(name, value) { + this.data[name] = value; + }; +} + +var allDeps = { + ContextSrvStub: ContextSrvStub, + TemplateSrvStub: TemplateSrvStub, + TimeSrvStub: TimeSrvStub, + ControllerTestContext: ControllerTestContext, + ServiceTestContext: ServiceTestContext, + DashboardViewStateStub: DashboardViewStateStub +}; + +// for legacy +export default allDeps; diff --git a/public/vendor/jquery-ui/custom.js b/public/vendor/jquery-ui/custom.js new file mode 100644 index 00000000000..aa3e5d8b6ba --- /dev/null +++ b/public/vendor/jquery-ui/custom.js @@ -0,0 +1,4034 @@ +/*! jQuery UI - v1.12.1 - 2017-06-13 +* http://jqueryui.com +* Includes: widget.js, data.js, disable-selection.js, scroll-parent.js, widgets/draggable.js, widgets/droppable.js, widgets/resizable.js, widgets/mouse.js +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + +$.ui = $.ui || {}; + +var version = $.ui.version = "1.12.1"; + + +/*! + * jQuery UI Widget 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Widget +//>>group: Core +//>>description: Provides a factory for creating stateful widgets with a common API. +//>>docs: http://api.jqueryui.com/jQuery.widget/ +//>>demos: http://jqueryui.com/widget/ + + + +var widgetUuid = 0; +var widgetSlice = Array.prototype.slice; + +$.cleanData = ( function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // Http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; +} )( $.cleanData ); + +$.widget = function( name, base, prototype ) { + var existingConstructor, constructor, basePrototype; + + // ProxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + var proxiedPrototype = {}; + + var namespace = name.split( "." )[ 0 ]; + name = name.split( "." )[ 1 ]; + var fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + if ( $.isArray( prototype ) ) { + prototype = $.extend.apply( null, [ {} ].concat( prototype ) ); + } + + // Create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + + // Allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // Allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + + // Extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + + // Copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + + // Track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + } ); + + basePrototype = new base(); + + // We need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = ( function() { + function _super() { + return base.prototype[ prop ].apply( this, arguments ); + } + + function _superApply( args ) { + return base.prototype[ prop ].apply( this, args ); + } + + return function() { + var __super = this._super; + var __superApply = this._superApply; + var returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + } )(); + } ); + constructor.prototype = $.widget.extend( basePrototype, { + + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + } ); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // Redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, + child._proto ); + } ); + + // Remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); + + return constructor; +}; + +$.widget.extend = function( target ) { + var input = widgetSlice.call( arguments, 1 ); + var inputIndex = 0; + var inputLength = input.length; + var key; + var value; + + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; +}; + +$.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string"; + var args = widgetSlice.call( arguments, 1 ); + var returnValue = this; + + if ( isMethodCall ) { + + // If this is an empty collection, we need to have the instance method + // return undefined instead of the jQuery instance + if ( !this.length && options === "instance" ) { + returnValue = undefined; + } else { + this.each( function() { + var methodValue; + var instance = $.data( this, fullName ); + + if ( options === "instance" ) { + returnValue = instance; + return false; + } + + if ( !instance ) { + return $.error( "cannot call methods on " + name + + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + + if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + + " widget instance" ); + } + + methodValue = instance[ options ].apply( instance, args ); + + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + } ); + } + } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat( args ) ); + } + + this.each( function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + } ); + } + + return returnValue; + }; +}; + +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; + +$.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
    ", + + options: { + classes: {}, + disabled: false, + + // Callbacks + create: null + }, + + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = widgetUuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + this.classesElementLookup = {}; + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + } ); + this.document = $( element.style ? + + // Element within the document + element.ownerDocument : + + // Element is window or document + element.document || element ); + this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow ); + } + + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this._create(); + + if ( this.options.disabled ) { + this._setOptionDisabled( this.options.disabled ); + } + + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + + _getCreateOptions: function() { + return {}; + }, + + _getCreateEventData: $.noop, + + _create: $.noop, + + _init: $.noop, + + destroy: function() { + var that = this; + + this._destroy(); + $.each( this.classesElementLookup, function( key, value ) { + that._removeClass( value, key ); + } ); + + // We can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .off( this.eventNamespace ) + .removeData( this.widgetFullName ); + this.widget() + .off( this.eventNamespace ) + .removeAttr( "aria-disabled" ); + + // Clean up events and states + this.bindings.off( this.eventNamespace ); + }, + + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key; + var parts; + var curOption; + var i; + + if ( arguments.length === 0 ) { + + // Don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + + // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + + _setOption: function( key, value ) { + if ( key === "classes" ) { + this._setOptionClasses( value ); + } + + this.options[ key ] = value; + + if ( key === "disabled" ) { + this._setOptionDisabled( value ); + } + + return this; + }, + + _setOptionClasses: function( value ) { + var classKey, elements, currentElements; + + for ( classKey in value ) { + currentElements = this.classesElementLookup[ classKey ]; + if ( value[ classKey ] === this.options.classes[ classKey ] || + !currentElements || + !currentElements.length ) { + continue; + } + + // We are doing this to create a new jQuery object because the _removeClass() call + // on the next line is going to destroy the reference to the current elements being + // tracked. We need to save a copy of this collection so that we can add the new classes + // below. + elements = $( currentElements.get() ); + this._removeClass( currentElements, classKey ); + + // We don't use _addClass() here, because that uses this.options.classes + // for generating the string of classes. We want to use the value passed in from + // _setOption(), this is the new value of the classes option which was passed to + // _setOption(). We pass this value directly to _classes(). + elements.addClass( this._classes( { + element: elements, + keys: classKey, + classes: value, + add: true + } ) ); + } + }, + + _setOptionDisabled: function( value ) { + this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this._removeClass( this.hoverable, null, "ui-state-hover" ); + this._removeClass( this.focusable, null, "ui-state-focus" ); + } + }, + + enable: function() { + return this._setOptions( { disabled: false } ); + }, + + disable: function() { + return this._setOptions( { disabled: true } ); + }, + + _classes: function( options ) { + var full = []; + var that = this; + + options = $.extend( { + element: this.element, + classes: this.options.classes || {} + }, options ); + + function processClassString( classes, checkOption ) { + var current, i; + for ( i = 0; i < classes.length; i++ ) { + current = that.classesElementLookup[ classes[ i ] ] || $(); + if ( options.add ) { + current = $( $.unique( current.get().concat( options.element.get() ) ) ); + } else { + current = $( current.not( options.element ).get() ); + } + that.classesElementLookup[ classes[ i ] ] = current; + full.push( classes[ i ] ); + if ( checkOption && options.classes[ classes[ i ] ] ) { + full.push( options.classes[ classes[ i ] ] ); + } + } + } + + this._on( options.element, { + "remove": "_untrackClassesElement" + } ); + + if ( options.keys ) { + processClassString( options.keys.match( /\S+/g ) || [], true ); + } + if ( options.extra ) { + processClassString( options.extra.match( /\S+/g ) || [] ); + } + + return full.join( " " ); + }, + + _untrackClassesElement: function( event ) { + var that = this; + $.each( that.classesElementLookup, function( key, value ) { + if ( $.inArray( event.target, value ) !== -1 ) { + that.classesElementLookup[ key ] = $( value.not( event.target ).get() ); + } + } ); + }, + + _removeClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, false ); + }, + + _addClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, true ); + }, + + _toggleClass: function( element, keys, extra, add ) { + add = ( typeof add === "boolean" ) ? add : extra; + var shift = ( typeof element === "string" || element === null ), + options = { + extra: shift ? keys : extra, + keys: shift ? element : keys, + element: shift ? this.element : element, + add: add + }; + options.element.toggleClass( this._classes( options ), add ); + return this; + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement; + var instance = this; + + // No suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // No element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + + // Allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // Copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^([\w:-]*)\s*(.*)$/ ); + var eventName = match[ 1 ] + instance.eventNamespace; + var selector = match[ 2 ]; + + if ( selector ) { + delegateElement.on( eventName, selector, handlerProxy ); + } else { + element.on( eventName, handlerProxy ); + } + } ); + }, + + _off: function( element, eventName ) { + eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; + element.off( eventName ).off( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-hover" ); + }, + mouseleave: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-hover" ); + } + } ); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-focus" ); + }, + focusout: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-focus" ); + } + } ); + }, + + _trigger: function( type, event, data ) { + var prop, orig; + var callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + + // The original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // Copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } +}; + +$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + + var hasOptions; + var effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + + if ( options.delay ) { + element.delay( options.delay ); + } + + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue( function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + } ); + } + }; +} ); + +var widget = $.widget; + + +/*! + * jQuery UI :data 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: :data Selector +//>>group: Core +//>>description: Selects elements which have data stored under the specified key. +//>>docs: http://api.jqueryui.com/data-selector/ + + +var data = $.extend( $.expr[ ":" ], { + data: $.expr.createPseudo ? + $.expr.createPseudo( function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + } ) : + + // Support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + } +} ); + +/*! + * jQuery UI Disable Selection 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: disableSelection +//>>group: Core +//>>description: Disable selection of text content within the set of matched elements. +//>>docs: http://api.jqueryui.com/disableSelection/ + +// This file is deprecated + + +var disableSelection = $.fn.extend( { + disableSelection: ( function() { + var eventType = "onselectstart" in document.createElement( "div" ) ? + "selectstart" : + "mousedown"; + + return function() { + return this.on( eventType + ".ui-disableSelection", function( event ) { + event.preventDefault(); + } ); + }; + } )(), + + enableSelection: function() { + return this.off( ".ui-disableSelection" ); + } +} ); + + +/*! + * jQuery UI Scroll Parent 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: scrollParent +//>>group: Core +//>>description: Get the closest ancestor element that is scrollable. +//>>docs: http://api.jqueryui.com/scrollParent/ + + + +var scrollParent = $.fn.scrollParent = function( includeHidden ) { + var position = this.css( "position" ), + excludeStaticParent = position === "absolute", + overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, + scrollParent = this.parents().filter( function() { + var parent = $( this ); + if ( excludeStaticParent && parent.css( "position" ) === "static" ) { + return false; + } + return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + + parent.css( "overflow-x" ) ); + } ).eq( 0 ); + + return position === "fixed" || !scrollParent.length ? + $( this[ 0 ].ownerDocument || document ) : + scrollParent; +}; + + + + +// This file is deprecated +var ie = $.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); + +/*! + * jQuery UI Mouse 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Mouse +//>>group: Widgets +//>>description: Abstracts mouse-based interactions to assist in creating certain widgets. +//>>docs: http://api.jqueryui.com/mouse/ + + + +var mouseHandled = false; +$( document ).on( "mouseup", function() { + mouseHandled = false; +} ); + +var widgetsMouse = $.widget( "ui.mouse", { + version: "1.12.1", + options: { + cancel: "input, textarea, button, select, option", + distance: 1, + delay: 0 + }, + _mouseInit: function() { + var that = this; + + this.element + .on( "mousedown." + this.widgetName, function( event ) { + return that._mouseDown( event ); + } ) + .on( "click." + this.widgetName, function( event ) { + if ( true === $.data( event.target, that.widgetName + ".preventClickEvent" ) ) { + $.removeData( event.target, that.widgetName + ".preventClickEvent" ); + event.stopImmediatePropagation(); + return false; + } + } ); + + this.started = false; + }, + + // TODO: make sure destroying one instance of mouse doesn't mess with + // other instances of mouse + _mouseDestroy: function() { + this.element.off( "." + this.widgetName ); + if ( this._mouseMoveDelegate ) { + this.document + .off( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .off( "mouseup." + this.widgetName, this._mouseUpDelegate ); + } + }, + + _mouseDown: function( event ) { + + // don't let more than one widget handle mouseStart + if ( mouseHandled ) { + return; + } + + this._mouseMoved = false; + + // We may have missed mouseup (out of window) + ( this._mouseStarted && this._mouseUp( event ) ); + + this._mouseDownEvent = event; + + var that = this, + btnIsLeft = ( event.which === 1 ), + + // event.target.nodeName works around a bug in IE 8 with + // disabled inputs (#7620) + elIsCancel = ( typeof this.options.cancel === "string" && event.target.nodeName ? + $( event.target ).closest( this.options.cancel ).length : false ); + if ( !btnIsLeft || elIsCancel || !this._mouseCapture( event ) ) { + return true; + } + + this.mouseDelayMet = !this.options.delay; + if ( !this.mouseDelayMet ) { + this._mouseDelayTimer = setTimeout( function() { + that.mouseDelayMet = true; + }, this.options.delay ); + } + + if ( this._mouseDistanceMet( event ) && this._mouseDelayMet( event ) ) { + this._mouseStarted = ( this._mouseStart( event ) !== false ); + if ( !this._mouseStarted ) { + event.preventDefault(); + return true; + } + } + + // Click event may never have fired (Gecko & Opera) + if ( true === $.data( event.target, this.widgetName + ".preventClickEvent" ) ) { + $.removeData( event.target, this.widgetName + ".preventClickEvent" ); + } + + // These delegates are required to keep context + this._mouseMoveDelegate = function( event ) { + return that._mouseMove( event ); + }; + this._mouseUpDelegate = function( event ) { + return that._mouseUp( event ); + }; + + this.document + .on( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .on( "mouseup." + this.widgetName, this._mouseUpDelegate ); + + event.preventDefault(); + + mouseHandled = true; + return true; + }, + + _mouseMove: function( event ) { + + // Only check for mouseups outside the document if you've moved inside the document + // at least once. This prevents the firing of mouseup in the case of IE<9, which will + // fire a mousemove event if content is placed under the cursor. See #7778 + // Support: IE <9 + if ( this._mouseMoved ) { + + // IE mouseup check - mouseup happened when mouse was out of window + if ( $.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && + !event.button ) { + return this._mouseUp( event ); + + // Iframe mouseup check - mouseup occurred in another document + } else if ( !event.which ) { + + // Support: Safari <=8 - 9 + // Safari sets which to 0 if you press any of the following keys + // during a drag (#14461) + if ( event.originalEvent.altKey || event.originalEvent.ctrlKey || + event.originalEvent.metaKey || event.originalEvent.shiftKey ) { + this.ignoreMissingWhich = true; + } else if ( !this.ignoreMissingWhich ) { + return this._mouseUp( event ); + } + } + } + + if ( event.which || event.button ) { + this._mouseMoved = true; + } + + if ( this._mouseStarted ) { + this._mouseDrag( event ); + return event.preventDefault(); + } + + if ( this._mouseDistanceMet( event ) && this._mouseDelayMet( event ) ) { + this._mouseStarted = + ( this._mouseStart( this._mouseDownEvent, event ) !== false ); + ( this._mouseStarted ? this._mouseDrag( event ) : this._mouseUp( event ) ); + } + + return !this._mouseStarted; + }, + + _mouseUp: function( event ) { + this.document + .off( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .off( "mouseup." + this.widgetName, this._mouseUpDelegate ); + + if ( this._mouseStarted ) { + this._mouseStarted = false; + + if ( event.target === this._mouseDownEvent.target ) { + $.data( event.target, this.widgetName + ".preventClickEvent", true ); + } + + this._mouseStop( event ); + } + + if ( this._mouseDelayTimer ) { + clearTimeout( this._mouseDelayTimer ); + delete this._mouseDelayTimer; + } + + this.ignoreMissingWhich = false; + mouseHandled = false; + event.preventDefault(); + }, + + _mouseDistanceMet: function( event ) { + return ( Math.max( + Math.abs( this._mouseDownEvent.pageX - event.pageX ), + Math.abs( this._mouseDownEvent.pageY - event.pageY ) + ) >= this.options.distance + ); + }, + + _mouseDelayMet: function( /* event */ ) { + return this.mouseDelayMet; + }, + + // These are placeholder methods, to be overriden by extending plugin + _mouseStart: function( /* event */ ) {}, + _mouseDrag: function( /* event */ ) {}, + _mouseStop: function( /* event */ ) {}, + _mouseCapture: function( /* event */ ) { return true; } +} ); + + + + +// $.ui.plugin is deprecated. Use $.widget() extensions instead. +var plugin = $.ui.plugin = { + add: function( module, option, set ) { + var i, + proto = $.ui[ module ].prototype; + for ( i in set ) { + proto.plugins[ i ] = proto.plugins[ i ] || []; + proto.plugins[ i ].push( [ option, set[ i ] ] ); + } + }, + call: function( instance, name, args, allowDisconnected ) { + var i, + set = instance.plugins[ name ]; + + if ( !set ) { + return; + } + + if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode || + instance.element[ 0 ].parentNode.nodeType === 11 ) ) { + return; + } + + for ( i = 0; i < set.length; i++ ) { + if ( instance.options[ set[ i ][ 0 ] ] ) { + set[ i ][ 1 ].apply( instance.element, args ); + } + } + } +}; + + + +var safeActiveElement = $.ui.safeActiveElement = function( document ) { + var activeElement; + + // Support: IE 9 only + // IE9 throws an "Unspecified error" accessing document.activeElement from an - - - [[end]] +
    +
    +
    + + +
    +
    + + + [[if .GoogleTagManagerId]] + + + + + + [[end]] + + - diff --git a/scripts/grunt/release_task.js b/scripts/grunt/release_task.js index ecce22f20bd..28208ed0086 100644 --- a/scripts/grunt/release_task.js +++ b/scripts/grunt/release_task.js @@ -26,7 +26,7 @@ module.exports = function(grunt) { }); grunt.config('copy.backend_files', { expand: true, - src: ['conf/*', 'vendor/phantomjs/*', 'scripts/*'], + src: ['conf/**', 'vendor/phantomjs/*', 'scripts/*'], options: { mode: true}, dest: '<%= tempDir %>' }); diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 037212c26de..195ef54c5cc 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -10,7 +10,7 @@ const WebpackCleanupPlugin = require('webpack-cleanup-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = merge(common, { - devtool: "eval-source-map", + devtool: "cheap-module-source-map", entry: { dark: './public/sass/grafana.dark.scss', @@ -21,7 +21,7 @@ module.exports = merge(common, { module: { rules: [ require('./sass.rule.js')({ - sourceMap: true, minimize: false + sourceMap: false, minimize: false }) ] }, @@ -29,6 +29,7 @@ module.exports = merge(common, { plugins: [ new ExtractTextPlugin({ // define where to save the file filename: 'grafana.[name].css', + allChunks: true, }), new HtmlWebpackPlugin({ filename: path.resolve(__dirname, '../../public/views/index.html'), diff --git a/scripts/webpack/webpack.test.js b/scripts/webpack/webpack.test.js index 8983541a612..f30c7876185 100644 --- a/scripts/webpack/webpack.test.js +++ b/scripts/webpack/webpack.test.js @@ -3,7 +3,7 @@ const merge = require('webpack-merge'); const common = require('./webpack.common.js'); config = merge(common, { - devtool: 'inline-source-map', + devtool: 'cheap-module-source-map', externals: { 'react/addons': true, 'react/lib/ExecutionEnvironment': true, diff --git a/tslint.json b/tslint.json index 789e83975db..e7a51295701 100644 --- a/tslint.json +++ b/tslint.json @@ -14,7 +14,7 @@ "forin": false, "indent": [true, "spaces", 2], "label-position": true, - "max-line-length": [true, 140], + "max-line-length": [true, 150], "member-access": false, "no-arg": true, "no-bitwise": false, diff --git a/yarn.lock b/yarn.lock index 770633ed1df..a419e1020c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,8 +3,8 @@ "@types/cheerio@*": - version "0.22.2" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.2.tgz#539625874bc856086ad491c2fdc9b10c05ae308e" + version "0.22.5" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.5.tgz#db749e8470d98f103d51407db9bee5a8b9d20d45" "@types/d3-array@*": version "1.2.1" @@ -39,30 +39,30 @@ resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-1.0.5.tgz#f1f9187b538ecb05157569d8dc2f70dfb04f1b52" "@types/d3-drag@*": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-1.1.0.tgz#9105e35ca58aa0c4783f3ce83082bcb24ccb6960" + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-1.2.0.tgz#5ee6279432c894f85cb72fcda911a959bae11952" dependencies: "@types/d3-selection" "*" "@types/d3-dsv@*": - version "1.0.30" - resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.0.30.tgz#78e0dddde4283566f463e51551a97a63c170d5a8" + version "1.0.31" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.0.31.tgz#468302f18ac44db2a3944086388d862503ab9c6c" "@types/d3-ease@*": version "1.0.7" resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-1.0.7.tgz#93a301868be9e15061f3d44343b1ab3f8acb6f09" "@types/d3-force@*": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.0.7.tgz#8e3c533697143ebb70275d56840206e8ba789185" + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.1.0.tgz#40925ca3512b63bd424f7c9685e1781b5b0a1d7e" "@types/d3-format@*": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.2.1.tgz#9435fb1771d2fbf6a858c93218f4097c9aa396c1" "@types/d3-geo@*": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.7.0.tgz#f2e250124203f99d1f56033cc9fd7d435c94b6d7" + version "1.9.3" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.9.3.tgz#742ceafa808c6853affccfb11f956cfc8bdccecb" dependencies: "@types/geojson" "*" @@ -109,8 +109,8 @@ "@types/d3-time" "*" "@types/d3-selection@*": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.1.0.tgz#59b88f10d2cff7d9ffd7fe986b3aaef3de048224" + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.2.0.tgz#f0a4cca0a0e4187c336c6712a82600cdcd24093f" "@types/d3-shape@*": version "1.2.1" @@ -119,8 +119,8 @@ "@types/d3-path" "*" "@types/d3-time-format@*": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.0.5.tgz#1d4c5ba77ed5352b10c7fce062c883382f1e16e0" + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.1.0.tgz#011e0fb7937be34a9a8f580ae1e2f2f1336a8a22" "@types/d3-time@*": version "1.0.7" @@ -131,8 +131,8 @@ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-1.0.6.tgz#786d4e20731adf03af2c5df6c86fe29667fe429b" "@types/d3-transition@*": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.0.tgz#74475d4a8f8a0944a517d5ef861970cc30287e40" + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.1.tgz#c209fce6a966d6696356dd42b091a9c6cc79929f" dependencies: "@types/d3-selection" "*" @@ -141,15 +141,15 @@ resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.7.tgz#c0a145cf04395927e01706ff6c4ff835c97a8ece" "@types/d3-zoom@*": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.5.0.tgz#21f690b25a8419fd1bcc95ac629cefdfb462c70f" + version "1.7.0" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.7.0.tgz#1221bbf6434820f044c80b551c5519b817008961" dependencies: "@types/d3-interpolate" "*" "@types/d3-selection" "*" "@types/d3@^4.10.1": - version "4.10.1" - resolved "https://registry.yarnpkg.com/@types/d3/-/d3-4.10.1.tgz#a888ac8780ac241d770b2025b3d7e379c4d417f0" + version "4.12.0" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-4.12.0.tgz#445ede4ab7707db1a011ef43b2bd187d21bdaffc" dependencies: "@types/d3-array" "*" "@types/d3-axis" "*" @@ -183,37 +183,34 @@ "@types/d3-zoom" "*" "@types/enzyme@^2.8.9": - version "2.8.9" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-2.8.9.tgz#17db10bb223a2c81ed8ac6ba6fdba52d2639d8d8" + version "2.8.12" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-2.8.12.tgz#a669d79ce1760d7241bc4b6fb7535d68669d78ad" dependencies: "@types/cheerio" "*" "@types/react" "*" "@types/geojson@*": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-1.0.4.tgz#f6e011bf3f7eea616cce79b6f1a0722010822f3a" + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-1.0.6.tgz#3e02972728c69248c2af08d60a48cbb8680fffdf" "@types/jest@^21.1.4": - version "21.1.4" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-21.1.4.tgz#83c5c474dd6dee5bef9d014ff36787edfd4ab5a7" + version "21.1.8" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-21.1.8.tgz#d497213725684f1e5a37900b17a47c9c018f1a97" -"@types/node@^6.0.46": - version "6.0.88" - resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.88.tgz#f618f11a944f6a18d92b5c472028728a3e3d4b66" +"@types/node@*", "@types/node@^8.0.31": + version "8.0.53" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8" -"@types/node@^8.0.31": - version "8.0.31" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.31.tgz#d9af61093cf4bfc9f066ca34de0175012cfb0ce9" - -"@types/react-dom@^15.5.4": - version "15.5.5" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-15.5.5.tgz#6b117c7697b61fe74132bfe5c72bceb3319433b8" +"@types/react-dom@^16.0.3": + version "16.0.3" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.3.tgz#8accad7eabdab4cca3e1a56f5ccb57de2da0ff64" dependencies: + "@types/node" "*" "@types/react" "*" -"@types/react@*", "@types/react@^16.0.5": - version "16.0.7" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.7.tgz#f85b6c33c988a1631e2f32fedae71ec6d9718a0d" +"@types/react@*", "@types/react@^16.0.25": + version "16.0.25" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.25.tgz#bf696b83fe480c5e0eff4335ee39ebc95884a1ed" JSONStream@~1.3.1: version "1.3.1" @@ -230,7 +227,7 @@ abab@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" -abbrev@1, abbrev@~1.1.0: +abbrev@1, abbrev@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -274,9 +271,9 @@ acorn@^4.0.3, acorn@^4.0.4: version "4.0.13" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" -acorn@^5.0.0, acorn@^5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7" +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@~2.6.4: version "2.6.4" @@ -287,8 +284,8 @@ after@0.8.2: resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" agent-base@4, agent-base@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.1.1.tgz#92d8a4fc2524a3b09b3666a33b6c97960f23d6a4" + version "4.1.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.1.2.tgz#80fa6cde440f4dcf9af2617cf246099b5d99f0c8" dependencies: es6-promisify "^5.0.0" @@ -303,8 +300,8 @@ ajv-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" ajv-keywords@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" ajv@^4.7.0, ajv@^4.9.1: version "4.11.8" @@ -313,14 +310,14 @@ ajv@^4.7.0, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.0.0, ajv@^5.1.5: - version "5.2.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2" +ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5: + version "5.5.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.0.tgz#eb2840746e9dc48bd5e063a36e3fd400c5eab5a9" dependencies: co "^4.6.0" fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" - json-stable-stringify "^1.0.1" align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" @@ -349,24 +346,24 @@ angular-bindonce@^0.3.1: resolved "https://registry.yarnpkg.com/angular-bindonce/-/angular-bindonce-0.3.1.tgz#af19574abd43f608b9236a302cc5ce49d71dc9c6" angular-mocks@^1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.6.6.tgz#c93018e7838c6dc5ceaf1a6bcf9be13c830ea515" + version "1.6.7" + resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.6.7.tgz#85bf45a2537eac59fc6f4cf319846102e8000e65" angular-native-dragdrop@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/angular-native-dragdrop/-/angular-native-dragdrop-1.2.2.tgz#d646c6b75b131c48073c3f6e36a225b2726d8bae" angular-route@^1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.6.6.tgz#8c11748aa195c717b1b615a7e746442bfc7c61f4" + version "1.6.7" + resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.6.7.tgz#020970d93d8b2ce4ca6aff0e0d7922579543cbcf" angular-sanitize@^1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.6.6.tgz#0fd065a19931517fbece66596d325d72b6e06041" + version "1.6.7" + resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.6.7.tgz#5a3d61ad7b8b699923329635d99248bcfce26408" angular@^1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/angular/-/angular-1.6.6.tgz#fd5a3cfb437ce382d854ee01120797978527cb64" + version "1.6.7" + resolved "https://registry.yarnpkg.com/angular/-/angular-1.6.7.tgz#0f89837dae1776b01ccb1fa2096db0d9373d9897" ansi-align@^2.0.0: version "2.0.0" @@ -400,6 +397,10 @@ ansi-styles@^3.1.0, ansi-styles@^3.2.0: dependencies: color-convert "^1.9.0" +ansi-styles@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" + ansicolors@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" @@ -408,6 +409,10 @@ ansistyles@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" +any-observable@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242" + anymatch@^1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" @@ -425,9 +430,9 @@ append-transform@^0.4.0: dependencies: default-require-extensions "^1.0.0" -aproba@^1.0.3, aproba@^1.1.1, aproba@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" +aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2, aproba@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" archiver-utils@^1.3.0: version "1.3.0" @@ -488,7 +493,7 @@ arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" -arr-flatten@^1.0.1, arr-flatten@^1.0.3: +arr-flatten@^1.0.1, arr-flatten@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" @@ -504,6 +509,10 @@ array-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" +array-filter@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" + array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" @@ -512,6 +521,14 @@ array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" +array-map@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" + +array-reduce@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" + array-slice@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" @@ -547,8 +564,8 @@ asap@^2.0.0, asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" asn1.js@^4.0.0: - version "4.9.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" + version "4.9.2" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a" dependencies: bn.js "^4.0.0" inherits "^2.0.1" @@ -588,6 +605,10 @@ async-foreach@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + async@0.2.x, async@~0.2.6, async@~0.2.9: version "0.2.10" resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" @@ -597,8 +618,8 @@ async@^1.4.0, async@^1.5.2, async@~1.5.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" async@^2.0.0, async@^2.1.2, async@^2.1.4, async@^2.1.5, async@^2.4.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + version "2.6.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" dependencies: lodash "^4.14.0" @@ -630,8 +651,8 @@ autoprefixer@^6.3.1, autoprefixer@^6.4.0: postcss-value-parser "^3.2.3" awesome-typescript-loader@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/awesome-typescript-loader/-/awesome-typescript-loader-3.2.3.tgz#aa2119b7c808a031e2b28945b031450a8975367f" + version "3.4.0" + resolved "https://registry.yarnpkg.com/awesome-typescript-loader/-/awesome-typescript-loader-3.4.0.tgz#aed2c83af614d617d11e3ec368ac3befb55d002f" dependencies: colors "^1.1.2" enhanced-resolve "3.3.0" @@ -646,7 +667,11 @@ aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" -aws4@^1.2.1: +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" @@ -1130,6 +1155,10 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +batch-processor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -1151,8 +1180,8 @@ big.js@^3.1.3: resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" binary-extensions@^1.0.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" + version "1.11.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" bl@^1.0.0: version "1.2.1" @@ -1171,8 +1200,8 @@ block-stream@*: inherits "~2.0.0" bluebird@^3.3.0, bluebird@^3.4.7, bluebird@^3.5.0, bluebird@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -1203,9 +1232,21 @@ boom@2.x.x: dependencies: hoek "2.x.x" -boxen@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.2.1.tgz#0f11e7fe344edb9397977fc13ede7f64d956481d" +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + +boxen@^1.0.0, boxen@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.2.2.tgz#3f1d4032c30ffea9d4b02c322eaf2ea741dcbce5" dependencies: ansi-align "^2.0.0" camelcase "^4.0.0" @@ -1242,20 +1283,20 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" -braces@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.2.2.tgz#241f868c2b2690d9febeee5a7c83fbbf25d00b1b" +braces@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.0.tgz#a46941cb5fb492156b3d6a656e06c35364e3e66e" dependencies: - arr-flatten "^1.0.3" + arr-flatten "^1.1.0" array-unique "^0.3.2" define-property "^1.0.0" extend-shallow "^2.0.1" fill-range "^4.0.0" - isobject "^3.0.0" + isobject "^3.0.1" repeat-element "^1.1.2" snapdragon "^0.8.1" snapdragon-node "^2.0.1" - split-string "^2.1.0" + split-string "^3.0.2" to-regex "^3.0.1" brorand@^1.0.1: @@ -1273,8 +1314,8 @@ browser-stdout@1.3.0: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.0.8" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.8.tgz#c8fa3b1b7585bb7ba77c5560b60996ddec6d5309" + version "1.1.1" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f" dependencies: buffer-xor "^1.0.3" cipher-base "^1.0.0" @@ -1324,6 +1365,12 @@ browserify-zlib@^0.1.4: dependencies: pako "~0.2.0" +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + dependencies: + pako "~1.0.5" + browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: version "1.7.7" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" @@ -1353,7 +1400,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -builtin-modules@^1.0.0: +builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1369,7 +1416,43 @@ bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" -cacache@^9.2.9, cacache@~9.2.9: +cacache@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.1.tgz#3e05f6e616117d9b54665b1b20c8aeb93ea5d36f" + dependencies: + bluebird "^3.5.0" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^1.3.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.1" + ssri "^5.0.0" + unique-filename "^1.1.0" + y18n "^3.2.1" + +cacache@^9.2.9: + version "9.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-9.3.0.tgz#9cd58f2dd0b8c8cacf685b7067b416d6d3cf9db1" + dependencies: + bluebird "^3.5.0" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^1.3.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.1" + ssri "^4.1.6" + unique-filename "^1.1.0" + y18n "^3.2.1" + +cacache@~9.2.9: version "9.2.9" resolved "https://registry.yarnpkg.com/cacache/-/cacache-9.2.9.tgz#f9d7ffe039851ec94c28290662afa4dd4bb9e8dd" dependencies: @@ -1463,13 +1546,17 @@ caniuse-api@^1.5.2: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000740" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000740.tgz#03fcaaa176e3ed075895f72d46c1a12149bbeac9" + version "1.0.30000772" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b" capture-stack-trace@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -1491,14 +1578,22 @@ 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: - version "2.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" +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: ansi-styles "^3.1.0" escape-string-regexp "^1.0.5" supports-color "^4.0.0" +chalk@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" + dependencies: + ansi-styles "~1.0.0" + has-color "~0.1.0" + strip-ansi "~0.1.0" + change-case@3.0.x: version "3.0.1" resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.0.1.tgz#ee5f5ad0415ad1ad9e8072cf49cd4cfa7660a554" @@ -1533,7 +1628,7 @@ cheerio@^1.0.0-rc.2: lodash "^4.15.0" parse5 "^3.0.1" -chokidar@^1.4.1, chokidar@^1.7.0: +chokidar@^1.4.1, chokidar@^1.6.0, chokidar@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -1553,8 +1648,12 @@ chownr@^1.0.1, chownr@~1.0.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" ci-info@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.1.tgz#47b44df118c48d2597b56d342e7e25791060171a" + version "1.1.2" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4" + +cidr-regex@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1" cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" @@ -1583,6 +1682,10 @@ class-utils@^0.3.5: lazy-cache "^2.0.2" static-extend "^0.1.1" +classnames@2.x, classnames@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" + clean-css@3.4.x, clean-css@~3.4.2: version "3.4.28" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff" @@ -1610,6 +1713,15 @@ cli-spinners@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" +cli-table2@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97" + dependencies: + lodash "^3.10.1" + string-width "^1.0.1" + optionalDependencies: + colors "^1.1.2" + cli-table@~0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" @@ -1668,8 +1780,8 @@ clone-deep@^0.3.0: shallow-clone "^0.1.2" clone@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" clone@^2.1.1: version "2.1.1" @@ -1708,8 +1820,8 @@ collection-visit@^1.0.0: object-visit "^1.0.0" color-convert@^1.3.0, color-convert@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" dependencies: color-name "^1.1.1" @@ -1774,7 +1886,11 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@2, commander@2.11.0, commander@2.11.x, commander@^2.8.1, commander@^2.9.0, commander@~2.11.0: +commander@2, commander@2.12.x, commander@^2.11.0, commander@^2.8.1, commander@^2.9.0, commander@~2.12.1: + version "2.12.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.1.tgz#468635c4168d06145b9323356d1da84d14ac4a7a" + +commander@2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" @@ -1817,8 +1933,8 @@ component-inherit@0.0.3: resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" compress-commons@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-1.2.0.tgz#58587092ef20d37cb58baf000112c9278ff73b9f" + version "1.2.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-1.2.2.tgz#524a9f10903f3a813389b0225d27c48bb751890f" dependencies: buffer-crc32 "^0.2.1" crc32-stream "^2.0.0" @@ -1890,16 +2006,16 @@ content-disposition@0.5.2: resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" content-type-parser@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94" + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" convert-source-map@^1.4.0, convert-source-map@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" convert-source-map@~1.1.2: version "1.1.3" @@ -1940,19 +2056,6 @@ core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" -cosmiconfig@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-1.1.0.tgz#0dea0f9804efdfb929fbb1b188e25553ea053d37" - dependencies: - graceful-fs "^4.1.2" - js-yaml "^3.4.3" - minimist "^1.2.0" - object-assign "^4.0.1" - os-homedir "^1.0.1" - parse-json "^2.2.0" - pinkie-promise "^2.0.0" - require-from-string "^1.1.0" - cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: version "2.2.2" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" @@ -1965,6 +2068,31 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: parse-json "^2.2.0" require-from-string "^1.1.0" +cosmiconfig@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.9.0" + parse-json "^3.0.0" + require-from-string "^2.0.1" + +cpx@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/cpx/-/cpx-1.5.0.tgz#185be018511d87270dedccc293171e37655ab88f" + dependencies: + babel-runtime "^6.9.2" + chokidar "^1.6.0" + duplexer "^0.1.1" + glob "^7.0.5" + glob2base "^0.0.12" + minimatch "^3.0.2" + mkdirp "^0.5.1" + resolve "^1.1.7" + safe-buffer "^5.0.1" + shell-quote "^1.6.1" + subarg "^1.0.0" + crc32-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-2.0.0.tgz#e3cdd3b4df3168dd74e3de3fbbcb7b297fe908f4" @@ -2030,9 +2158,15 @@ cryptiles@2.x.x: dependencies: boom "2.x.x" +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + crypto-browserify@^3.11.0: - version "3.11.1" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" dependencies: browserify-cipher "^1.0.0" browserify-sign "^4.0.0" @@ -2044,6 +2178,7 @@ crypto-browserify@^3.11.0: pbkdf2 "^3.0.3" public-encrypt "^4.0.0" randombytes "^2.0.0" + randomfill "^1.0.3" crypto-random-string@^1.0.0: version "1.0.0" @@ -2221,9 +2356,9 @@ d3-drag@1, d3-drag@1.2.1: d3-dispatch "1" d3-selection "1" -d3-dsv@1, d3-dsv@1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.0.7.tgz#137076663f398428fc3d031ae65370522492b78f" +d3-dsv@1, d3-dsv@1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.0.8.tgz#907e240d57b386618dc56468bacfe76bf19764ae" dependencies: commander "2" iconv-lite "0.4" @@ -2242,13 +2377,13 @@ d3-force@1.1.0: d3-quadtree "1" d3-timer "1" -d3-format@1, d3-format@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.0.tgz#6b480baa886885d4651dc248a8f4ac9da16db07a" +d3-format@1, d3-format@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.1.tgz#4e19ecdb081a341dafaf5f555ee956bcfdbf167f" -d3-geo@1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.8.1.tgz#50615c33454487e350db71059f84f71cda2dd983" +d3-geo@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.0.tgz#15c7d7a8ea9346e59ed150dc7b1f7f95479056e9" dependencies: d3-array "1" @@ -2256,9 +2391,9 @@ d3-hierarchy@1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26" -d3-interpolate@1, d3-interpolate@1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.5.tgz#69e099ff39214716e563c9aec3ea9d1ea4b8a79f" +d3-interpolate@1, d3-interpolate@1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.6.tgz#2cf395ae2381804df08aa1bf766b7f97b5f68fb6" dependencies: d3-color "1" @@ -2297,9 +2432,9 @@ d3-scale-chromatic@^1.1.1: dependencies: d3-interpolate "1" -d3-scale@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.6.tgz#bce19da80d3a0cf422c9543ae3322086220b34ed" +d3-scale@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d" dependencies: d3-array "^1.2.0" d3-collection "1" @@ -2309,9 +2444,9 @@ d3-scale@1.0.6: d3-time "1" d3-time-format "2" -d3-selection@1, d3-selection@1.1.0, d3-selection@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.1.0.tgz#1998684896488f839ca0372123da34f1d318809c" +d3-selection@1, d3-selection@1.2.0, d3-selection@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.2.0.tgz#1b8ec1c7cedadfb691f2ba20a4a3cfbeb71bbc88" d3-shape@1.2.0: version "1.2.0" @@ -2319,29 +2454,23 @@ d3-shape@1.2.0: dependencies: d3-path "1" -d3-time-format@2: - version "2.1.0" - resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.0.tgz#a1d9540a1dc498817d44066b121b19a4a83e3531" +d3-time-format@2, d3-time-format@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31" dependencies: d3-time "1" -d3-time-format@2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.0.5.tgz#9d7780204f7c9119c9170b1a56db4de9a8af972e" - dependencies: - d3-time "1" - -d3-time@1, d3-time@1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.7.tgz#94caf6edbb7879bb809d0d1f7572bc48482f7270" +d3-time@1, d3-time@1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84" d3-timer@1, d3-timer@1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531" -d3-transition@1, d3-transition@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.0.tgz#cfc85c74e5239324290546623572990560c3966f" +d3-transition@1, d3-transition@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039" dependencies: d3-color "1" d3-dispatch "1" @@ -2354,9 +2483,9 @@ d3-voronoi@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c" -d3-zoom@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.6.0.tgz#eb645b07fd0c37acc8b36b88476b781ed277b40e" +d3-zoom@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63" dependencies: d3-dispatch "1" d3-drag "1" @@ -2365,8 +2494,8 @@ d3-zoom@1.6.0: d3-transition "1" d3@^4.11.0: - version "4.11.0" - resolved "https://registry.yarnpkg.com/d3/-/d3-4.11.0.tgz#15ce99ec33e6941718cfd8fb826071b4fb7c48cb" + version "4.12.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-4.12.0.tgz#75eccb39ea40f6018de8cfa2752905bee7daa46f" dependencies: d3-array "1.2.1" d3-axis "1.0.8" @@ -2376,28 +2505,28 @@ d3@^4.11.0: d3-color "1.0.3" d3-dispatch "1.0.3" d3-drag "1.2.1" - d3-dsv "1.0.7" + d3-dsv "1.0.8" d3-ease "1.0.3" d3-force "1.1.0" - d3-format "1.2.0" - d3-geo "1.8.1" + d3-format "1.2.1" + d3-geo "1.9.0" d3-hierarchy "1.1.5" - d3-interpolate "1.1.5" + d3-interpolate "1.1.6" d3-path "1.0.5" d3-polygon "1.0.3" d3-quadtree "1.0.3" d3-queue "3.0.7" d3-random "1.1.0" d3-request "1.0.6" - d3-scale "1.0.6" - d3-selection "1.1.0" + d3-scale "1.0.7" + d3-selection "1.2.0" d3-shape "1.2.0" - d3-time "1.0.7" - d3-time-format "2.0.5" + d3-time "1.0.8" + d3-time-format "2.1.1" d3-timer "1.0.7" - d3-transition "1.1.0" + d3-transition "1.1.1" d3-voronoi "1.1.2" - d3-zoom "1.6.0" + d3-zoom "1.7.1" d@1: version "1.0.0" @@ -2412,8 +2541,8 @@ dashdash@^1.12.0: assert-plus "^1.0.0" date-fns@^1.27.2: - version "1.28.5" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf" + version "1.29.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" date-now@^0.1.4: version "0.1.4" @@ -2426,7 +2555,7 @@ dateformat@~1.0.12: get-stdin "^4.0.1" meow "^3.3.0" -debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.4.1, debug@^2.6.3, debug@^2.6.8: +debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.4.1, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -2444,7 +2573,7 @@ debug@2.3.3: dependencies: ms "0.7.2" -debug@3.1.0: +debug@3.1.0, debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -2458,6 +2587,14 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + deep-equal@*: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -2560,6 +2697,14 @@ detect-indent@~5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" +detect-libc@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-0.2.0.tgz#47fdf567348a17ec25fcbf0b9e446348a76f9fb5" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + dezalgo@^1.0.0, dezalgo@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -2571,7 +2716,7 @@ di@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" -diff@3.3.1, diff@^3.2.0: +diff@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" @@ -2579,6 +2724,10 @@ diff@^2.0.2: version "2.2.3" resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" +diff@^3.2.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" + diffie-hellman@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" @@ -2656,13 +2805,20 @@ domutils@1.1: dependencies: domelementtype "1" -domutils@1.5, domutils@1.5.1, domutils@^1.5.1: +domutils@1.5, domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" dependencies: dom-serializer "0" domelementtype "1" +domutils@^1.5.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" + dependencies: + dom-serializer "0" + domelementtype "1" + dot-case@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-2.1.1.tgz#34dcf37f50a8e93c2b3bca8bb7fb9155c7da3bee" @@ -2722,13 +2878,19 @@ ejs@^2.5.6: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" electron-to-chromium@^1.2.7: - version "1.3.24" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.24.tgz#9b7b88bb05ceb9fa016a177833cc2dde388f21b6" + version "1.3.27" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz#78ecb8a399066187bb374eede35d9c70565a803d" elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" +element-resize-detector@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.1.12.tgz#8b3fd6eedda17f9c00b360a0ea2df9927ae80ba2" + dependencies: + batch-processor "^1.0.0" + elliptic@^6.0.0: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" @@ -2800,7 +2962,7 @@ engine.io@1.8.3: engine.io-parser "1.3.2" ws "1.1.2" -enhanced-resolve@3.3.0, enhanced-resolve@^3.0.0: +enhanced-resolve@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.3.0.tgz#950964ecc7f0332a42321b673b38dc8ff15535b3" dependencies: @@ -2809,7 +2971,7 @@ enhanced-resolve@3.3.0, enhanced-resolve@^3.0.0: object-assign "^4.0.1" tapable "^0.2.5" -enhanced-resolve@^3.4.0: +enhanced-resolve@^3.0.0, enhanced-resolve@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" dependencies: @@ -2831,38 +2993,39 @@ entities@^1.1.1, entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" enzyme-adapter-react-16@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.2.tgz#8c6f431f17c69e1e9eeb25ca4bd92f31971eb2dd" + version "1.1.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.0.tgz#86c5db7c10f0be6ec25d54ca41b59f2abb397cf4" dependencies: - enzyme-adapter-utils "^1.0.0" + enzyme-adapter-utils "^1.1.0" lodash "^4.17.4" object.assign "^4.0.4" object.values "^1.0.4" prop-types "^15.5.10" react-test-renderer "^16.0.0-0" -enzyme-adapter-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.0.0.tgz#e94eee63da9a798d498adb1162a2102ed04fc638" +enzyme-adapter-utils@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.2.0.tgz#7f4471ee0a70b91169ec8860d2bf0a6b551664b2" dependencies: lodash "^4.17.4" object.assign "^4.0.4" prop-types "^15.5.10" enzyme@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.1.0.tgz#d8ca84085790fbcec6ed40badd14478faee4c25a" + version "3.2.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.2.0.tgz#998bdcda0fc71b8764a0017f7cc692c943f54a7a" dependencies: cheerio "^1.0.0-rc.2" function.prototype.name "^1.0.3" + has "^1.0.1" is-subset "^0.1.1" lodash "^4.17.4" object-is "^1.0.1" object.assign "^4.0.4" object.entries "^1.0.4" object.values "^1.0.4" - raf "^3.3.2" - rst-selector-parser "^2.2.2" + raf "^3.4.0" + rst-selector-parser "^2.2.3" err-code@^1.0.0: version "1.1.2" @@ -2874,15 +3037,15 @@ errno@^0.1.3, errno@^0.1.4: dependencies: prr "~0.0.0" -error-ex@^1.2.0: +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" dependencies: is-arrayish "^0.2.1" es-abstract@^1.6.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.2.tgz#25103263dc4decbda60e0c737ca32313518027ee" + version "1.10.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" dependencies: es-to-primitive "^1.1.1" function-bind "^1.1.1" @@ -2898,20 +3061,20 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.1" -es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.30" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.30.tgz#7141a16836697dbabfaaaeee41495ce29f52c939" +es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.37" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.37.tgz#0ee741d148b80069ba27d020393756af257defc3" dependencies: - es6-iterator "2" - es6-symbol "~3.1" + es6-iterator "~2.0.1" + es6-symbol "~3.1.1" -es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" +es6-iterator@^2.0.1, es6-iterator@~2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" dependencies: d "1" - es5-ext "^0.10.14" - es6-symbol "^3.1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" es6-map@^0.1.3: version "0.1.5" @@ -2928,9 +3091,9 @@ es6-promise@^3.0.2: version "3.3.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" -es6-promise@^4.0.3, es6-promise@~4.0.3: - version "4.0.5" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42" +es6-promise@^4.0.3: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" es6-promisify@^5.0.0: version "5.0.0" @@ -2952,7 +3115,7 @@ es6-shim@^0.35.3: version "0.35.3" resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.3.tgz#9bfb7363feffff87a6cdb6cd93e405ec3c4b6f26" -es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1: +es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" dependencies: @@ -2984,15 +3147,15 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" escodegen@^1.6.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" + version "1.9.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852" dependencies: - esprima "^2.7.1" - estraverse "^1.9.1" + esprima "^3.1.3" + estraverse "^4.2.0" esutils "^2.0.2" optionator "^0.8.1" optionalDependencies: - source-map "~0.2.0" + source-map "~0.5.6" escope@^3.6.0: version "3.6.0" @@ -3042,24 +3205,24 @@ eslint@^2.7.0: user-home "^2.0.0" espree@^3.1.6: - version "3.5.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.1.tgz#0c988b8ab46db53100a1954ae4ba995ddd27d87e" + version "3.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca" dependencies: - acorn "^5.1.1" + acorn "^5.2.1" acorn-jsx "^3.0.0" -esprima@^2.6.0, esprima@^2.7.1: +esprima@^2.6.0: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" +esprima@^3.1.3, esprima@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" -esprima@~3.1.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - esrecurse@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" @@ -3067,10 +3230,6 @@ esrecurse@^4.1.0: estraverse "^4.1.0" object-assign "^4.0.1" -estraverse@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" - estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" @@ -3098,7 +3257,7 @@ eventemitter3@1.x.x: version "1.2.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" -eventemitter3@^2.0.3: +eventemitter3@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" @@ -3190,6 +3349,10 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" +expand-template@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-1.1.0.tgz#e09efba977bf98f9ee0ed25abd0c692e02aec3fc" + expect.js@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.3.1.tgz#b0a59a0d2eff5437544ebf0ceaa6015841d09b5b" @@ -3210,12 +3373,12 @@ expect@^21.2.1: jest-regex-util "^21.2.0" expose-loader@^0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-0.7.3.tgz#35fbd3659789e4faa81f59de8b7e9fc39e466d51" + version "0.7.4" + resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-0.7.4.tgz#9bcdd3878b5da9107930b55a03f65afe90b3314a" express@^4.15.2: - version "4.16.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.1.tgz#6b33b560183c9b253b7b62144df33a4654ac9ed0" + version "4.16.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" dependencies: accepts "~1.3.4" array-flatten "1.1.1" @@ -3254,7 +3417,13 @@ extend-shallow@^2.0.1: dependencies: is-extendable "^0.1.0" -extend@^3.0.0, extend@~3.0.0: +extend-shallow@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.1.tgz#4b6d8c49b147fee029dc9eb9484adb770f689844" + dependencies: + is-extendable "^1.0.1" + +extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -3278,20 +3447,20 @@ extglob@^2.0.2: to-regex "^3.0.1" extract-text-webpack-plugin@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612" + version "3.0.2" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz#5f043eaa02f9750a9258b78c0a6e0dc1408fb2f7" dependencies: async "^2.4.1" loader-utils "^1.1.0" schema-utils "^0.3.0" webpack-sources "^1.0.1" -extract-zip@~1.6.5: - version "1.6.5" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.5.tgz#99a06735b6ea20ea9b705d779acffcc87cff0440" +extract-zip@^1.6.5: + version "1.6.6" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" dependencies: concat-stream "1.6.0" - debug "2.2.0" + debug "2.6.9" mkdirp "0.5.0" yauzl "2.4.1" @@ -3307,6 +3476,10 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -3379,8 +3552,8 @@ fileset@^2.0.2: minimatch "^3.0.3" filesize@^3.5.9: - version "3.5.10" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.10.tgz#fc8fa23ddb4ef9e5e0ab6e1e64f679a24a56761f" + version "3.5.11" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.11.tgz#1919326749433bb3cf77368bd158caabcc19e9ee" fill-range@^2.1.0: version "2.2.3" @@ -3433,6 +3606,14 @@ find-cache-dir@^1.0.0: make-dir "^1.0.0" pkg-dir "^2.0.0" +find-index@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" + +find-parent-dir@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -3508,6 +3689,14 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + formatio@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9" @@ -3554,6 +3743,14 @@ fs-access@^1.0.0: dependencies: null-check "^1.0.0" +fs-extra@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + fs-extra@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" @@ -3562,7 +3759,7 @@ fs-extra@^3.0.1: jsonfile "^3.0.0" universalify "^0.1.0" -fs-extra@^4.0.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: @@ -3570,14 +3767,6 @@ fs-extra@^4.0.0: 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" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - fs-vacuum@~1.2.10: version "1.2.10" resolved "https://registry.yarnpkg.com/fs-vacuum/-/fs-vacuum-1.2.10.tgz#b7629bec07a4031a2548fdf99f5ecf1cc8b31e36" @@ -3600,11 +3789,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" fsevents@^1.0.0, fsevents@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" + version "1.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" dependencies: nan "^2.3.0" - node-pre-gyp "^0.6.36" + node-pre-gyp "^0.6.39" fstream-ignore@^1.0.5: version "1.0.5" @@ -3698,6 +3887,10 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + glob-base@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" @@ -3711,6 +3904,12 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" +glob2base@^0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" + dependencies: + find-index "^0.1.1" + glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1, glob@~7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -3732,6 +3931,16 @@ glob@^5.0.1, glob@~5.0.0: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@~7.0.0: version "7.0.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" @@ -3743,6 +3952,12 @@ glob@~7.0.0: once "^1.3.0" path-is-absolute "^1.0.0" +global-dirs@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + dependencies: + ini "^1.3.4" + globals@^9.18.0, globals@^9.2.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -3766,9 +3981,9 @@ globule@^1.0.0: lodash "~4.17.4" minimatch "~3.0.2" -gonzales-pe@^4.1.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.2.2.tgz#f50a8c17842f13a9007909b7cb32188266e4d74c" +gonzales-pe-sl@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz#6a868bc380645f141feeb042c6f97fcc71b59fe6" dependencies: minimist "1.1.x" @@ -3943,10 +4158,10 @@ grunt-postcss@^0.8.0: postcss "^5.0.0" grunt-sass-lint@^0.2.2: - version "0.2.3" - resolved "https://registry.yarnpkg.com/grunt-sass-lint/-/grunt-sass-lint-0.2.3.tgz#47d9a85de5aead62ea422667adf1e811dfa6909d" + version "0.2.4" + resolved "https://registry.yarnpkg.com/grunt-sass-lint/-/grunt-sass-lint-0.2.4.tgz#06f77635ad8a5048968ea33c5584b40a18281e35" dependencies: - sass-lint "^1.11.0" + sass-lint "^1.12.0" grunt-sass@^2.0.0: version "2.0.0" @@ -4007,8 +4222,8 @@ gzip-size@^3.0.0: duplexer "^0.1.1" handlebars@^4.0.3: - version "4.0.10" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f" + version "4.0.11" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" dependencies: async "^1.4.0" optimist "^0.6.1" @@ -4020,6 +4235,19 @@ har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + har-validator@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" @@ -4027,6 +4255,13 @@ har-validator@~4.2.1: ajv "^4.9.1" har-schema "^1.0.5" +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -4039,6 +4274,10 @@ has-binary@0.1.7: dependencies: isarray "0.0.1" +has-color@~0.1.0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" + has-cors@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" @@ -4108,7 +4347,7 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" -hasha@~2.2.0: +hasha@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" dependencies: @@ -4124,6 +4363,15 @@ hawk@3.1.3, hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + he@1.1.1, he@1.1.x: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -4147,6 +4395,10 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoek@4.x.x: + version "4.2.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + 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" @@ -4167,8 +4419,8 @@ html-comment-regex@^1.1.0: resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" html-encoding-sniffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz#79bf7a785ea495fe66165e734153f363ff5437da" + version "1.0.2" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" dependencies: whatwg-encoding "^1.0.1" @@ -4183,17 +4435,17 @@ html-loader@^0.5.1: object-assign "^4.1.0" html-minifier@^3.0.1, html-minifier@^3.2.3: - version "3.5.5" - resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.5.tgz#3bdc9427e638bbe3dbde96c0eb988b044f02739e" + version "3.5.7" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.7.tgz#511e69bb5a8e7677d1012ebe03819aa02ca06208" dependencies: camel-case "3.0.x" clean-css "4.1.x" - commander "2.11.x" + commander "2.12.x" he "1.1.x" ncname "1.0.x" param-case "2.1.x" relateurl "0.2.x" - uglify-js "3.1.x" + uglify-js "3.2.x" html-minifier@~2.1.2: version "2.1.7" @@ -4248,9 +4500,9 @@ htmlparser2@~3.3.0: domutils "1.1" readable-stream "1.0" -http-cache-semantics@^3.7.3: - version "3.7.3" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.7.3.tgz#2f35c532ecd29f1e5413b9af833b724a3c6f7f72" +http-cache-semantics@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.0.tgz#1e3ce248730e189ac692a6697b9e3fdea2ff8da3" http-errors@1.6.2, http-errors@~1.6.2: version "1.6.2" @@ -4283,11 +4535,19 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" -https-proxy-agent@^2.0.0: +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + +https-proxy-agent@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.1.0.tgz#1391bee7fd66aeabc0df2a1fa90f58954f43e443" dependencies: @@ -4309,17 +4569,13 @@ husky@^0.14.3: strip-indent "^2.0.0" i@0.3.x: - version "0.3.5" - resolved "https://registry.yarnpkg.com/i/-/i-0.3.5.tgz#1d2b854158ec8169113c6cb7f6b6801e99e211d5" + version "0.3.6" + resolved "https://registry.yarnpkg.com/i/-/i-0.3.6.tgz#d96c92732076f072711b6b10fd7d4f65ad8ee23d" iconv-lite@0.4, iconv-lite@0.4.19, iconv-lite@~0.4.13: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" -iconv-lite@0.4.13: - version "0.4.13" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" - icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -4338,22 +4594,24 @@ iferr@^0.1.5, iferr@~0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" -ignore-walk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.0.tgz#e407919edee5c47c63473b319bfe3ea4a771a57e" +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" dependencies: minimatch "^3.0.4" ignore@^3.1.2: - version "3.3.5" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6" + version "3.3.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" iltorb@^1.0.13: - version "1.3.6" - resolved "https://registry.yarnpkg.com/iltorb/-/iltorb-1.3.6.tgz#890a63d7435690376bb671f2b0533f85ff85e4f2" + version "1.3.10" + resolved "https://registry.yarnpkg.com/iltorb/-/iltorb-1.3.10.tgz#a0d9e4e7d52bf510741442236cbe0cc4230fc9f8" dependencies: + detect-libc "^0.2.0" nan "^2.6.2" - node-pre-gyp "0.6.35" + node-gyp "^3.6.2" + prebuild-install "^2.3.0" import-lazy@^2.1.0: version "2.1.0" @@ -4405,8 +4663,8 @@ inherits@2.0.1: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" ini@^1.3.4, ini@~1.3.0, ini@~1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" init-package-json@~1.10.1: version "1.10.1" @@ -4462,8 +4720,8 @@ ipaddr.js@1.5.2: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" irregular-plurals@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.3.0.tgz#7af06931bdf74be33dcf585a13e06fccc16caecf" + version "1.4.0" + resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.4.0.tgz#2ca9b033651111855412f16be5d77c62a458a766" is-absolute-url@^2.0.0: version "2.1.0" @@ -4486,8 +4744,8 @@ is-binary-path@^1.0.0: binary-extensions "^1.0.0" is-buffer@^1.0.2, is-buffer@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" is-builtin-module@^1.0.0: version "1.0.0" @@ -4505,6 +4763,12 @@ is-ci@^1.0.10: dependencies: ci-info "^1.0.0" +is-cidr@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-1.0.0.tgz#fb5aacf659255310359da32cae03e40c6a1c2afc" + dependencies: + cidr-regex "1.0.6" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -4549,6 +4813,12 @@ is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" @@ -4585,13 +4855,20 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" + is-lower-case@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393" dependencies: lower-case "^1.1.0" -is-my-json-valid@^2.10.0: +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: version "2.16.1" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" dependencies: @@ -4624,6 +4901,12 @@ is-obj@^1.0.0, is-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" +is-observable@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2" + dependencies: + symbol-observable "^0.2.2" + is-odd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088" @@ -4650,7 +4933,7 @@ is-plain-obj@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" -is-plain-object@^2.0.1, is-plain-object@^2.0.3: +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" dependencies: @@ -4766,17 +5049,17 @@ isstream@0.1.x, isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" istanbul-api@^1.1.1: - version "1.1.14" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.14.tgz#25bc5701f7c680c0ffff913de46e3619a3a6e680" + version "1.2.1" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.2.1.tgz#0c60a0515eb11c7d65c6b50bba2c6e999acd8620" dependencies: async "^2.1.4" fileset "^2.0.2" istanbul-lib-coverage "^1.1.1" - istanbul-lib-hook "^1.0.7" - istanbul-lib-instrument "^1.8.0" - istanbul-lib-report "^1.1.1" - istanbul-lib-source-maps "^1.2.1" - istanbul-reports "^1.1.2" + istanbul-lib-hook "^1.1.0" + istanbul-lib-instrument "^1.9.1" + istanbul-lib-report "^1.1.2" + istanbul-lib-source-maps "^1.2.2" + istanbul-reports "^1.1.3" js-yaml "^3.7.0" mkdirp "^0.5.1" once "^1.4.0" @@ -4785,15 +5068,15 @@ istanbul-lib-coverage@^1.0.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" -istanbul-lib-hook@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz#dd6607f03076578fe7d6f2a630cf143b49bacddc" +istanbul-lib-hook@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz#8538d970372cb3716d53e55523dd54b557a8d89b" dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.8.0.tgz#66f6c9421cc9ec4704f76f2db084ba9078a2b532" +istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, 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: babel-generator "^6.18.0" babel-template "^6.16.0" @@ -4803,28 +5086,28 @@ istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-ins istanbul-lib-coverage "^1.1.1" semver "^5.3.0" -istanbul-lib-report@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#f0e55f56655ffa34222080b7a0cd4760e1405fc9" +istanbul-lib-report@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.2.tgz#922be27c13b9511b979bd1587359f69798c1d425" dependencies: istanbul-lib-coverage "^1.1.1" mkdirp "^0.5.1" path-parse "^1.0.5" supports-color "^3.1.2" -istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#a6fe1acba8ce08eebc638e572e294d267008aa0c" +istanbul-lib-source-maps@^1.1.0, 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: - debug "^2.6.3" + debug "^3.1.0" istanbul-lib-coverage "^1.1.1" mkdirp "^0.5.1" rimraf "^2.6.1" source-map "^0.5.3" -istanbul-reports@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.2.tgz#0fb2e3f6aa9922bd3ce45d05d8ab4d5e8e07bd4f" +istanbul-reports@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.3.tgz#3b9e1e8defb6d18b1d425da8e8b32c5a163f2d10" dependencies: handlebars "^4.0.3" @@ -5065,14 +5348,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@^3.4.3, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@~3.5.2: - version "3.5.5" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.5.5.tgz#0377c38017cabc7322b0d1fbcd25a491641f2fbe" - dependencies: - argparse "^1.0.2" - esprima "^2.6.0" - -js-yaml@^3.7.0: +js-yaml@^3.4.3, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.9.0: version "3.10.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" dependencies: @@ -5087,6 +5363,13 @@ js-yaml@~3.4.0: esprima "^2.6.0" inherit "^2.2.2" +js-yaml@~3.5.2: + version "3.5.5" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.5.5.tgz#0377c38017cabc7322b0d1fbcd25a491641f2fbe" + dependencies: + argparse "^1.0.2" + esprima "^2.6.0" + js-yaml@~3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" @@ -5319,14 +5602,14 @@ karma-sourcemap-loader@^0.3.7: graceful-fs "^4.1.2" karma-webpack@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.4.tgz#3e2d4f48ba94a878e1c66bb8e1ae6128987a175b" + version "2.0.6" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.6.tgz#967918e59750ebe0f40829263435fde7ac81bdb4" dependencies: async "~0.9.0" loader-utils "^0.2.5" lodash "^3.8.0" - source-map "^0.1.41" - webpack-dev-middleware "^1.0.11" + source-map "^0.5.6" + webpack-dev-middleware "^1.12.0" karma@1.7.0: version "1.7.0" @@ -5360,7 +5643,7 @@ karma@1.7.0: tmp "0.0.31" useragent "^2.1.12" -kew@~0.7.0: +kew@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" @@ -5383,8 +5666,12 @@ kind-of@^4.0.0: is-buffer "^1.1.5" kind-of@^5.0.0, kind-of@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.0.2.tgz#f57bec933d9a2209ffa96c5c08343607b7035fda" + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + +kind-of@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.1.tgz#4948e6263553ac3712fc44d305b77851d9e40ea4" klaw@^1.0.0: version "1.3.1" @@ -5456,22 +5743,28 @@ libnpx@~9.6.0: y18n "^3.2.1" yargs "^8.0.2" -lint-staged@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-4.2.3.tgz#5a1f12256af06110b96225f109dbf215009a37a9" +lint-staged@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-6.0.0.tgz#7ab7d345f2fe302ff196f1de6a005594ace03210" dependencies: app-root-path "^2.0.0" chalk "^2.1.0" - cosmiconfig "^1.1.0" + commander "^2.11.0" + cosmiconfig "^3.1.0" + debug "^3.1.0" + dedent "^0.7.0" execa "^0.8.0" + find-parent-dir "^0.3.0" is-glob "^4.0.0" jest-validate "^21.1.0" - listr "^0.12.0" + listr "^0.13.0" lodash "^4.17.4" log-symbols "^2.0.0" minimatch "^3.0.0" npm-which "^3.0.1" p-map "^1.1.1" + path-is-inside "^1.0.2" + pify "^3.0.0" staged-git-files "0.0.4" stringify-object "^3.2.0" @@ -5479,9 +5772,9 @@ listr-silent-renderer@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" -listr-update-renderer@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz#ca80e1779b4e70266807e8eed1ad6abe398550f9" +listr-update-renderer@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7" dependencies: chalk "^1.1.3" cli-truncate "^0.2.1" @@ -5493,33 +5786,34 @@ listr-update-renderer@^0.2.0: strip-ansi "^3.0.1" listr-verbose-renderer@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.0.tgz#44dc01bb0c34a03c572154d4d08cde9b1dc5620f" + version "0.4.1" + resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35" dependencies: chalk "^1.1.3" cli-cursor "^1.0.2" date-fns "^1.27.2" figures "^1.7.0" -listr@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.12.0.tgz#6bce2c0f5603fa49580ea17cd6a00cc0e5fa451a" +listr@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d" dependencies: chalk "^1.1.3" cli-truncate "^0.2.1" figures "^1.7.0" indent-string "^2.1.0" + is-observable "^0.2.0" is-promise "^2.1.0" is-stream "^1.1.0" listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.2.0" + listr-update-renderer "^0.4.0" listr-verbose-renderer "^0.4.0" log-symbols "^1.0.2" log-update "^1.0.2" ora "^0.2.3" p-map "^1.1.1" - rxjs "^5.0.0-beta.11" - stream-to-observable "^0.1.0" + rxjs "^5.4.2" + stream-to-observable "^0.2.0" strip-ansi "^3.0.1" load-grunt-tasks@3.5.2: @@ -5617,6 +5911,10 @@ lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" +lodash.isequal@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + lodash.kebabcase@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -5742,26 +6040,26 @@ macaddress@^0.2.8: resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" make-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" + version "1.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" dependencies: - pify "^2.3.0" + pify "^3.0.0" -make-fetch-happen@^2.4.13: - version "2.5.0" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.5.0.tgz#08c22d499f4f30111addba79fe87c98cf01b6bc8" +make-fetch-happen@^2.4.13, make-fetch-happen@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38" dependencies: agentkeepalive "^3.3.0" - cacache "^9.2.9" - http-cache-semantics "^3.7.3" + cacache "^10.0.0" + http-cache-semantics "^3.8.0" http-proxy-agent "^2.0.0" - https-proxy-agent "^2.0.0" + https-proxy-agent "^2.1.0" lru-cache "^4.1.1" mississippi "^1.2.0" - node-fetch-npm "^2.0.1" + node-fetch-npm "^2.0.2" promise-retry "^1.1.1" - socks-proxy-agent "^3.0.0" - ssri "^4.1.6" + socks-proxy-agent "^3.0.1" + ssri "^5.0.0" makeerror@1.0.x: version "1.0.11" @@ -5803,7 +6101,7 @@ md5.js@^1.3.4: hash-base "^3.0.0" inherits "^2.0.1" -meant@~1.0.0: +meant@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.1.tgz#66044fea2f23230ec806fb515efea29c44d2115d" @@ -5870,18 +6168,18 @@ micromatch@^2.1.5, micromatch@^2.3.11: regex-cache "^0.4.2" micromatch@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.0.tgz#5102d4eaf20b6997d6008e3acfe1c44a3fa815e2" + version "3.1.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.4.tgz#bb812e741a41f982c854e42b421a7eac458796f4" dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" - braces "^2.2.2" + braces "^2.3.0" define-property "^1.0.0" extend-shallow "^2.0.1" extglob "^2.0.2" fragment-cache "^0.2.1" - kind-of "^5.0.2" - nanomatch "^1.2.1" + kind-of "^6.0.0" + nanomatch "^1.2.5" object.pick "^1.3.0" regex-not "^1.0.0" snapdragon "^0.8.1" @@ -5898,16 +6196,20 @@ mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" -mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: +mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7: version "2.1.17" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" dependencies: mime-db "~1.30.0" -mime@1.4.1, mime@^1.3.4: +mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" +mime@^1.3.4, mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -5932,7 +6234,7 @@ minimatch@3.0.3: dependencies: brace-expansion "^1.0.0" -minimist@0.0.8, minimist@~0.0.1: +minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -5940,21 +6242,25 @@ minimist@1.1.x: version "1.1.3" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" -minimist@1.2.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: +minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -minipass@^2.0.0, minipass@^2.0.2: +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + +minipass@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.1.tgz#5ada97538b1027b4cf7213432428578cb564011f" dependencies: yallist "^3.0.0" -minizlib@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.0.3.tgz#d5c1abf77be154619952e253336eccab9b2a32f5" +minizlib@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.0.4.tgz#8ebb51dd8bbe40b0126b5633dbb36b284a2f523c" dependencies: - minipass "^2.0.0" + minipass "^2.2.1" mississippi@^1.2.0, mississippi@^1.3.0, mississippi@~1.3.0: version "1.3.0" @@ -6013,8 +6319,8 @@ mocha@^4.0.1: supports-color "4.4.0" moment@^2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" + version "2.19.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe" mousetrap@^1.6.0: version "1.6.1" @@ -6061,12 +6367,12 @@ mute-stream@~0.0.4: 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: - version "2.7.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" + version "2.8.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" -nanomatch@^1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.3.tgz#15e1c02dcf990c27a283b08c0ba1801ce249a6a6" +nanomatch@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.5.tgz#5c9ab02475c76676275731b0bf0a7395c624a9c4" dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" @@ -6127,7 +6433,7 @@ ng-annotate-webpack-plugin@^0.2.1-pre: ng-annotate "^1.2.1" webpack-core "^0.6.5" -ng-annotate@1.2.1, ng-annotate@^1.2.1: +ng-annotate@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ng-annotate/-/ng-annotate-1.2.1.tgz#eb8bc1a6731c70d08af6b02c3eaf1a6e3fb9e6bb" dependencies: @@ -6144,9 +6450,22 @@ ng-annotate@1.2.1, ng-annotate@^1.2.1: stringset "~0.2.1" tryor "~0.1.2" -ngreact@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/ngreact/-/ngreact-0.4.1.tgz#53d8f0db7c687c6daa340827a5ef04a903c14701" +ng-annotate@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ng-annotate/-/ng-annotate-1.2.2.tgz#dc3fc51ba0b2f8b385dbe047f4da06f580a1fd61" + dependencies: + acorn "~2.6.4" + alter "~0.2.0" + convert-source-map "~1.1.2" + optimist "~0.6.1" + ordered-ast-traverse "~1.1.1" + simple-fmt "~0.1.0" + simple-is "~0.2.0" + source-map "~0.5.3" + stable "~0.1.5" + stringmap "~0.2.2" + stringset "~0.2.1" + tryor "~0.1.2" ngtemplate-loader@^2.0.1: version "2.0.1" @@ -6161,7 +6480,13 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-fetch-npm@^2.0.1: +node-abi@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.1.2.tgz#4da6caceb6685fcd31e7dd1994ef6bb7d0a9c0b2" + dependencies: + semver "^5.4.1" + +node-fetch-npm@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz#7258c9046182dca345b4208eda918daf33697ff7" dependencies: @@ -6176,7 +6501,7 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" -node-gyp@^3.3.1, node-gyp@~3.6.2: +node-gyp@^3.3.1, node-gyp@^3.6.2, node-gyp@~3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" dependencies: @@ -6199,28 +6524,28 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" node-libs-browser@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646" + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" dependencies: assert "^1.1.1" - browserify-zlib "^0.1.4" + browserify-zlib "^0.2.0" buffer "^4.3.0" console-browserify "^1.1.0" constants-browserify "^1.0.0" crypto-browserify "^3.11.0" domain-browser "^1.1.1" events "^1.0.0" - https-browserify "0.0.1" - os-browserify "^0.2.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" path-browserify "0.0.0" - process "^0.11.0" + process "^0.11.10" punycode "^1.2.4" querystring-es3 "^0.2.0" - readable-stream "^2.0.5" + readable-stream "^2.3.3" stream-browserify "^2.0.1" - stream-http "^2.3.1" - string_decoder "^0.10.25" - timers-browserify "^2.0.2" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" tty-browserify "0.0.0" url "^0.11.0" util "^0.10.3" @@ -6235,24 +6560,11 @@ node-notifier@^5.0.2: shellwords "^0.1.0" which "^1.2.12" -node-pre-gyp@0.6.35: - version "0.6.35" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.35.tgz#1c161fc9fbf1f3ffecd751959f0fdbd12a56c4ab" - dependencies: - mkdirp "^0.5.1" - nopt "^4.0.1" - npmlog "^4.0.2" - rc "^1.1.7" - request "^2.81.0" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^2.2.1" - tar-pack "^3.4.0" - -node-pre-gyp@^0.6.36: - version "0.6.38" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.38.tgz#e92a20f83416415bb4086f6d1fb78b3da73d113d" +node-pre-gyp@^0.6.39: + version "0.6.39" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" dependencies: + detect-libc "^1.0.2" hawk "3.1.3" mkdirp "^0.5.1" nopt "^4.0.1" @@ -6265,8 +6577,8 @@ node-pre-gyp@^0.6.36: tar-pack "^3.4.0" node-sass@^4.0.0: - version "4.5.3" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.3.tgz#d09c9d1179641239d1b97ffc6231fdcec53e1568" + version "4.7.2" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e" dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -6283,17 +6595,29 @@ node-sass@^4.0.0: nan "^2.3.2" node-gyp "^3.3.1" npmlog "^4.0.0" - request "^2.79.0" - sass-graph "^2.1.1" + request "~2.79.0" + sass-graph "^2.2.4" stdout-stream "^1.4.0" + "true-case-path" "^1.0.2" -"nomnom@>= 1.5.x", nomnom@~1.6.2: +"nomnom@>= 1.5.x": + version "1.8.1" + resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" + dependencies: + chalk "~0.4.0" + underscore "~1.6.0" + +nomnom@~1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.6.2.tgz#84a66a260174408fc5b77a18f888eccc44fb6971" dependencies: colors "0.5.x" underscore "~1.4.4" +noop-logger@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" + "nopt@2 || 3", nopt@~3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -6316,7 +6640,7 @@ normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package- semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@2.0.1, normalize-path@^2.0.0, normalize-path@^2.0.1: +normalize-path@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" @@ -6324,6 +6648,12 @@ normalize-path@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" +normalize-path@^2.0.0, normalize-path@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" @@ -6351,7 +6681,7 @@ npm-install-checks@~3.0.0: dependencies: semver "^2.3.0 || 3.x || 4 || 5" -npm-lifecycle@~1.0.2: +npm-lifecycle@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-1.0.3.tgz#4cd60543247dbba631281e48ce665ffd52380cce" dependencies: @@ -6370,11 +6700,11 @@ npm-lifecycle@~1.0.2: semver "^5.1.0" validate-npm-package-name "^3.0.0" -npm-packlist@^1.1.6, npm-packlist@~1.1.8: - version "1.1.9" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.9.tgz#bd24a0b7a31a307315b07c2e54f4888f10577548" +npm-packlist@^1.1.6, npm-packlist@~1.1.9: + version "1.1.10" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a" dependencies: - ignore-walk "^3.0.0" + ignore-walk "^3.0.1" npm-bundled "^1.0.1" npm-path@^2.0.2: @@ -6390,9 +6720,16 @@ npm-pick-manifest@^1.0.4: npm-package-arg "^5.1.2" semver "^5.3.0" -npm-registry-client@~8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.4.0.tgz#d52b901685647fc62a4c03eafecb6ceaa5018d4c" +npm-profile@~2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-2.0.5.tgz#0e61b8f1611bd19d1eeff5e3d5c82e557da3b9d7" + dependencies: + aproba "^1.1.2" + make-fetch-happen "^2.5.0" + +npm-registry-client@~8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.5.0.tgz#4878fb6fa1f18a5dc08ae83acf94d0d0112d7ed0" dependencies: concat-stream "^1.5.2" graceful-fs "^4.1.6" @@ -6426,20 +6763,21 @@ npm-which@^3.0.1: which "^1.2.10" npm@^5.4.2: - version "5.4.2" - resolved "https://registry.yarnpkg.com/npm/-/npm-5.4.2.tgz#830b5cabb5f735264e7cc39b2163b90854b2eaa8" + version "5.5.1" + resolved "https://registry.yarnpkg.com/npm/-/npm-5.5.1.tgz#5bef2b01c51c8144412d5873caf83e22f1ec6b84" dependencies: JSONStream "~1.3.1" - abbrev "~1.1.0" + abbrev "~1.1.1" ansi-regex "~3.0.0" ansicolors "~0.3.2" ansistyles "~0.1.3" - aproba "~1.1.2" + aproba "~1.2.0" archy "~1.0.0" bluebird "~3.5.0" cacache "~9.2.9" call-limit "~1.1.0" chownr "~1.0.1" + cli-table2 "~0.2.0" cmd-shim "~2.0.2" columnify "~1.5.4" config-chain "~1.1.11" @@ -6457,6 +6795,7 @@ npm@^5.4.2: inherits "~2.0.3" ini "~1.3.4" init-package-json "~1.10.1" + is-cidr "~1.0.0" lazy-property "~1.0.0" libnpx "~9.6.0" lockfile "~1.0.3" @@ -6466,7 +6805,7 @@ npm@^5.4.2: lodash.uniq "~4.5.0" lodash.without "~4.4.0" lru-cache "~4.1.1" - meant "~1.0.0" + meant "~1.0.1" mississippi "~1.3.0" mkdirp "~0.5.1" move-concurrently "~1.0.1" @@ -6475,10 +6814,11 @@ npm@^5.4.2: normalize-package-data "~2.4.0" npm-cache-filename "~1.0.2" npm-install-checks "~3.0.0" - npm-lifecycle "~1.0.2" + npm-lifecycle "~1.0.3" npm-package-arg "~5.1.2" - npm-packlist "~1.1.8" - npm-registry-client "~8.4.0" + npm-packlist "~1.1.9" + npm-profile "~2.0.4" + npm-registry-client "~8.5.0" npm-user-validate "~1.0.0" npmlog "~4.1.2" once "~1.4.0" @@ -6487,15 +6827,18 @@ npm@^5.4.2: pacote "~6.0.2" path-is-inside "~1.0.2" promise-inflight "~1.0.1" + qrcode-terminal "~0.11.0" + query-string "~5.0.0" + qw "~1.0.1" read "~1.0.7" read-cmd-shim "~1.0.1" read-installed "~4.0.3" read-package-json "~2.0.12" read-package-tree "~5.1.6" readable-stream "~2.3.3" - request "~2.81.0" + request "~2.83.0" retry "~0.10.1" - rimraf "~2.6.1" + rimraf "~2.6.2" safe-buffer "~5.1.1" semver "~5.4.1" sha "~2.0.1" @@ -6518,7 +6861,7 @@ npm@^5.4.2: wrappy "~1.0.2" write-file-atomic "~2.1.0" -"npmlog@0 || 1 || 2 || 3 || 4", "npmlog@2 || ^3.1.0 || ^4.0.0", npmlog@^4.0.0, npmlog@^4.0.2, npmlog@~4.1.2: +"npmlog@0 || 1 || 2 || 3 || 4", "npmlog@2 || ^3.1.0 || ^4.0.0", npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.0.2, npmlog@~4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" dependencies: @@ -6549,7 +6892,7 @@ number-is-nan@^1.0.0: version "1.4.3" resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.3.tgz#64348e3b3d80f035b40ac11563d278f8b72db89c" -oauth-sign@~0.8.1: +oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" @@ -6687,9 +7030,9 @@ ordered-esprima-props@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ordered-esprima-props/-/ordered-esprima-props-1.1.0.tgz#a9827086df5f010aa60e9bd02b6e0335cea2ffcb" -os-browserify@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" @@ -6709,7 +7052,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -6752,8 +7095,8 @@ package-json@^4.0.0: semver "^5.1.0" pacote@~6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-6.0.2.tgz#c618a3c08493aeb390e79aa73f95af331ffc6171" + version "6.0.4" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-6.0.4.tgz#9384c4ca9a9dbbaa625bfbe653e0330eeaa1427b" dependencies: bluebird "^3.5.0" cacache "^9.2.9" @@ -6781,6 +7124,10 @@ pako@~0.2.0: version "0.2.9" resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" +pako@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" + parallel-transform@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" @@ -6820,15 +7167,21 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse-json@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-3.0.0.tgz#fa6f47b18e23826ead32f263e744d0e1e847fb13" + 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: - version "3.0.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510" + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" dependencies: - "@types/node" "^6.0.46" + "@types/node" "*" parsejson@0.0.3: version "0.0.3" @@ -6891,7 +7244,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1, path-is-absolute@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-is-inside@^1.0.1, path-is-inside@~1.0.2: +path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" @@ -6939,6 +7292,10 @@ pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" +perfect-scrollbar@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/perfect-scrollbar/-/perfect-scrollbar-1.2.0.tgz#ad23a2529c17f4535f21d1486f8bc3046e31a9d2" + performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" @@ -6948,20 +7305,20 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" phantomjs-prebuilt@^2.1.15, phantomjs-prebuilt@^2.1.7: - version "2.1.15" - resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.15.tgz#20f86e82d3349c505917527745b7a411e08b3903" + version "2.1.16" + resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef" dependencies: - es6-promise "~4.0.3" - extract-zip "~1.6.5" - fs-extra "~1.0.0" - hasha "~2.2.0" - kew "~0.7.0" - progress "~1.1.8" - request "~2.81.0" - request-progress "~2.0.1" - which "~1.2.10" + es6-promise "^4.0.3" + extract-zip "^1.6.5" + fs-extra "^1.0.0" + hasha "^2.2.0" + kew "^0.7.0" + progress "^1.1.8" + request "^2.81.0" + request-progress "^2.0.1" + which "^1.2.10" -pify@^2.0.0, pify@^2.3.0: +pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -7104,11 +7461,11 @@ postcss-load-plugins@^2.3.0: object-assign "^4.1.0" postcss-loader@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.6.tgz#8c7e0055a3df1889abc6bad52dd45b2f41bbc6fc" + version "2.0.9" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.9.tgz#001fdf7bfeeb159405ee61d1bb8e59b528dbd309" dependencies: loader-utils "^1.1.0" - postcss "^6.0.2" + postcss "^6.0.0" postcss-load-config "^1.2.0" schema-utils "^0.3.0" @@ -7290,22 +7647,41 @@ postcss-zindex@^2.0.1: uniqs "^2.0.0" postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16: - version "5.2.17" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b" + version "5.2.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" dependencies: chalk "^1.1.3" js-base64 "^2.1.9" source-map "^0.5.6" supports-color "^3.2.3" -postcss@^6.0.1, postcss@^6.0.2, postcss@^6.0.8: - version "6.0.12" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.12.tgz#6b0155089d2d212f7bd6a0cecd4c58c007403535" +postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.8: + version "6.0.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.14.tgz#5534c72114739e75d0afcf017db853099f562885" dependencies: - chalk "^2.1.0" - source-map "^0.5.7" + chalk "^2.3.0" + source-map "^0.6.1" supports-color "^4.4.0" +prebuild-install@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.3.0.tgz#19481247df728b854ab57b187ce234211311b485" + dependencies: + expand-template "^1.0.2" + github-from-package "0.0.0" + minimist "^1.2.0" + mkdirp "^0.5.1" + node-abi "^2.1.1" + noop-logger "^0.1.1" + npmlog "^4.0.1" + os-homedir "^1.0.1" + pump "^1.0.1" + rc "^1.1.6" + simple-get "^1.4.2" + tar-fs "^1.13.0" + tunnel-agent "^0.6.0" + xtend "4.0.1" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -7318,9 +7694,9 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier@1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.3.tgz#8e6974725273914b1c47439959dd3d3ba53664b6" +prettier@1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.9.2.tgz#96bc2132f7a32338e6078aeb29727178c6335827" pretty-bytes@^1.0.0: version "1.0.4" @@ -7348,18 +7724,18 @@ pretty-format@^21.2.1: ansi-styles "^3.2.0" private@^0.1.6, private@^0.1.7, private@~0.1.5: - version "0.1.7" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" -process@^0.11.0: +process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" -progress@^1.1.8, progress@~1.1.8: +progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" @@ -7396,7 +7772,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.5.10, prop-types@^15.6.0: +prop-types@15.x, prop-types@^15.5.10, prop-types@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: @@ -7439,9 +7815,9 @@ public-encrypt@^4.0.0: parse-asn1 "^5.0.0" randombytes "^2.0.1" -pump@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.2.tgz#3b3ee6512f94f0e575538c17995f9f16990a5d51" +pump@^1.0.0, pump@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" dependencies: end-of-stream "^1.1.0" once "^1.3.1" @@ -7463,17 +7839,25 @@ punycode@^1.2.4, punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" q@^1.1.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" qjobs@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" -qs@6.5.1: +qrcode-terminal@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz#ffc6c28a2fc0bfb47052b47e23f4f446a5fbdb9e" + +qs@6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" +qs@~6.3.0: + version "6.3.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" + qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -7485,6 +7869,14 @@ query-string@^4.1.0: object-assign "^4.1.0" strict-uri-encode "^1.0.0" +query-string@~5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.1.tgz#6e2b86fe0e08aef682ecbe86e85834765402bd88" + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -7493,9 +7885,13 @@ querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" -raf@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.3.2.tgz#0c13be0b5b49b46f76d6669248d527cf2b02fe27" +qw@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4" + +raf@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" dependencies: performance-now "^2.1.0" @@ -7517,12 +7913,19 @@ randomatic@^1.1.3: is-number "^3.0.0" kind-of "^4.0.0" -randombytes@^2.0.0, randombytes@^2.0.1: +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" dependencies: safe-buffer "^5.1.0" +randomfill@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62" + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + range-parser@^1.0.3, range-parser@^1.2.0, range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" @@ -7537,33 +7940,66 @@ raw-body@2.3.2: unpipe "1.0.0" rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: - version "1.2.1" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" + version "1.2.2" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077" dependencies: deep-extend "~0.4.0" ini "~1.3.0" minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^16.0.0: - version "16.0.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.0.0.tgz#9cc3079c3dcd70d4c6e01b84aab2a7e34c303f58" +react-dom@^16.1.1: + version "16.1.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.1.1.tgz#b2e331b6d752faf1a2d31399969399a41d8d45f8" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.0" +"react-draggable@^2.2.6 || ^3.0.3", react-draggable@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.3.tgz#a6f9b3a7171981b76dadecf238316925cb9eacf4" + dependencies: + classnames "^2.2.5" + prop-types "^15.5.10" + +react-grid-layout@^0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.1.tgz#45eb5687da2ae6912b63c1c0b79b7fb6bbd32202" + dependencies: + classnames "2.x" + lodash.isequal "^4.0.0" + prop-types "15.x" + react-draggable "^3.0.3" + react-resizable "^1.7.5" + +react-resizable@^1.7.5: + version "1.7.5" + resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e" + dependencies: + prop-types "15.x" + react-draggable "^2.2.6 || ^3.0.3" + +react-sizeme@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.3.6.tgz#d60ea2634acc3fd827a3c7738d41eea0992fa678" + dependencies: + element-resize-detector "^1.1.12" + invariant "^2.2.2" + lodash "^4.17.4" + react-test-renderer@^16.0.0, react-test-renderer@^16.0.0-0: - version "16.0.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.0.0.tgz#9fe7b8308f2f71f29fc356d4102086f131c9cb15" + version "16.1.1" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.1.1.tgz#a05184688d564be799f212449262525d1e350537" dependencies: fbjs "^0.8.16" object-assign "^4.1.1" + prop-types "^15.6.0" -react@^16.0.0: - version "16.0.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.0.0.tgz#ce7df8f1941b036f02b2cca9dbd0cb1f0e855e2d" +react@^16.1.1: + version "16.1.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.1.1.tgz#d5c4ef795507e3012282dd51261ff9c0e824fe1f" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -7646,7 +8082,7 @@ read@1, read@1.0.x, read@~1.0.1, read@~1.0.7: dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@~2.3.3: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.3.3, readable-stream@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -7667,7 +8103,16 @@ readable-stream@1.0, readable-stream@~1.0.2: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@1.1, readable-stream@~1.1.10: +readable-stream@1.1: + version "1.1.13" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~1.1.10: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" dependencies: @@ -7818,6 +8263,10 @@ remarkable@^1.7.1: argparse "~0.1.15" autolinker "~0.15.0" +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + renderkid@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.1.tgz#898cabfc8bede4b7b91135a3ffd323e58c0db319" @@ -7846,13 +8295,40 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request-progress@~2.0.1: +request-progress@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" dependencies: throttleit "^1.0.0" -request@2, request@2.81.0, request@^2.74.0, request@^2.79.0, request@^2.81.0, request@~2.81.0: +request@2, request@^2.74.0, request@^2.79.0, request@^2.81.0, request@~2.83.0: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +request@2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" dependencies: @@ -7879,6 +8355,31 @@ request@2, request@2.81.0, request@^2.74.0, request@^2.79.0, request@^2.81.0, re tunnel-agent "^0.6.0" uuid "^3.0.0" +request@~2.79.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -7887,6 +8388,10 @@ require-from-string@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" +require-from-string@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff" + require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" @@ -7928,9 +8433,9 @@ resolve@1.1.7, resolve@~1.1.0: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.1.6, resolve@^1.3.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86" +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" dependencies: path-parse "^1.0.5" @@ -7959,7 +8464,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@~2.6.1: +rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@~2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: @@ -7976,9 +8481,9 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" -rst-selector-parser@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.2.tgz#9927b619bd5af8dc23a76c64caef04edf90d2c65" +rst-selector-parser@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" dependencies: lodash.flattendeep "^4.4.0" nearley "^2.7.10" @@ -8003,9 +8508,15 @@ rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" -rxjs@^5.0.0-beta.11, rxjs@^5.4.3: - version "5.4.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f" +rxjs@^5.4.2: + version "5.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.5.tgz#e164f11d38eaf29f56f08c3447f74ff02dd84e97" + dependencies: + symbol-observable "1.0.1" + +rxjs@^5.4.3: + version "5.5.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.2.tgz#28d403f0071121967f18ad665563255d54236ac3" dependencies: symbol-observable "^1.0.1" @@ -8013,14 +8524,14 @@ safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" -safe-buffer@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" - -samsam@1.1.2, samsam@~1.1: +samsam@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" +samsam@~1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621" + sane@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/sane/-/sane-2.2.0.tgz#d6d2e2fcab00e3d283c93b912b7c3a20846f1d56" @@ -8035,7 +8546,7 @@ sane@^2.0.0: optionalDependencies: fsevents "^1.1.1" -sass-graph@^2.1.1: +sass-graph@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" dependencies: @@ -8044,9 +8555,9 @@ sass-graph@^2.1.1: scss-tokenizer "^0.2.3" yargs "^7.0.0" -sass-lint@^1.10.2, sass-lint@^1.11.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.11.1.tgz#1ccea9be01e60fd0ca7ddf379a0096311b218240" +sass-lint@^1.10.2, sass-lint@^1.12.0: + version "1.12.1" + resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.12.1.tgz#630f69c216aa206b8232fb2aa907bdf3336b6d83" dependencies: commander "^2.8.1" eslint "^2.7.0" @@ -8054,7 +8565,7 @@ sass-lint@^1.10.2, sass-lint@^1.11.0: fs-extra "^3.0.1" glob "^7.0.0" globule "^1.0.0" - gonzales-pe "^4.1.1" + gonzales-pe-sl "^4.2.3" js-yaml "^3.5.4" known-css-properties "^0.3.0" lodash.capitalize "^4.1.0" @@ -8223,6 +8734,15 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" +shell-quote@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" + dependencies: + array-filter "~0.0.0" + array-map "~0.0.0" + array-reduce "~0.0.0" + jsonify "~0.0.0" + shelljs@0.3.x: version "0.3.0" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1" @@ -8235,7 +8755,7 @@ shellwords@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" -signal-exit@^3.0.0: +signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -8243,6 +8763,14 @@ simple-fmt@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/simple-fmt/-/simple-fmt-0.1.0.tgz#191bf566a59e6530482cb25ab53b4a8dc85c3a6b" +simple-get@^1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-1.4.3.tgz#e9755eda407e96da40c5e5158c9ea37b33becbeb" + dependencies: + once "^1.3.1" + unzip-response "^1.0.0" + xtend "^4.0.0" + simple-is@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/simple-is/-/simple-is-0.2.0.tgz#2abb75aade39deb5cc815ce10e6191164850baf0" @@ -8311,6 +8839,12 @@ sntp@1.x.x: dependencies: hoek "2.x.x" +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + dependencies: + hoek "4.x.x" + socket.io-adapter@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz#cb6d4bb8bec81e1078b99677f9ced0046066bb8b" @@ -8355,7 +8889,7 @@ socket.io@1.7.3: socket.io-client "1.7.3" socket.io-parser "2.3.1" -socks-proxy-agent@^3.0.0: +socks-proxy-agent@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz#2eae7cf8e2a82d34565761539a7f9718c5617659" dependencies: @@ -8395,10 +8929,11 @@ source-list-map@~0.1.7: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" source-map-resolve@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.0.tgz#fcad0b64b70afb27699e425950cb5ebcd410bc20" + version "0.5.1" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a" dependencies: atob "^2.0.0" + decode-uri-component "^0.2.0" resolve-url "^0.2.1" source-map-url "^0.4.0" urix "^0.1.0" @@ -8429,26 +8964,14 @@ source-map@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" -source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: +source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3, source-map@~0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" -source-map@^0.1.41: - version "0.1.43" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" - dependencies: - amdefine ">=0.0.4" - -source-map@^0.6.0: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" -source-map@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" - dependencies: - amdefine ">=0.0.4" - spdx-correct@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" @@ -8463,17 +8986,11 @@ spdx-license-ids@^1.0.2: version "1.2.2" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" -split-string@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-2.1.1.tgz#af4b06d821560426446c3cd931cda618940d37d0" +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" dependencies: - extend-shallow "^2.0.1" - -split-string@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.0.2.tgz#6129bc92731716e5aa1fb73c333078f0b7c114c8" - dependencies: - extend-shallow "^2.0.1" + extend-shallow "^3.0.0" sprintf-js@~1.0.2: version "1.0.3" @@ -8499,6 +9016,12 @@ ssri@^4.1.2, ssri@^4.1.6, ssri@~4.1.6: dependencies: safe-buffer "^5.1.0" +ssri@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.0.0.tgz#13c19390b606c821f2a10d02b351c1729b94d8cf" + dependencies: + safe-buffer "^5.1.0" + stable@~0.1.3, stable@~0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.6.tgz#910f5d2aed7b520c6e777499c1f32e139fdecb10" @@ -8522,7 +9045,11 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.3.1 < 2", statuses@~1.3.1: +"statuses@>= 1.3.1 < 2": + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + +statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" @@ -8544,13 +9071,13 @@ stream-buffers@^2.1.0: resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4" stream-each@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.0.tgz#1e95d47573f580d814dc0ff8cd0f66f1ce53c991" + version "1.2.2" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" dependencies: end-of-stream "^1.1.0" stream-shift "^1.0.0" -stream-http@^2.3.1: +stream-http@^2.7.2: version "2.7.2" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad" dependencies: @@ -8571,9 +9098,11 @@ stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" -stream-to-observable@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe" +stream-to-observable@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10" + dependencies: + any-observable "^0.2.0" strict-uri-encode@^1.0.0: version "1.1.0" @@ -8607,16 +9136,16 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string_decoder@^0.10.25, string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - -string_decoder@~1.0.3: +string_decoder@^1.0.0, string_decoder@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" dependencies: safe-buffer "~5.1.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + stringify-object@^3.2.0: version "3.2.1" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d" @@ -8633,7 +9162,7 @@ stringset@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/stringset/-/stringset-0.2.1.tgz#ef259c4e349344377fcd1c913dd2e848c9c042b5" -stringstream@~0.0.4: +stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -8649,6 +9178,10 @@ strip-ansi@^4.0.0, strip-ansi@~4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" + strip-bom@3.0.0, strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -8681,7 +9214,13 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" -supports-color@4.4.0, supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0: +subarg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" + dependencies: + minimist "^1.1.0" + +supports-color@4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" dependencies: @@ -8697,6 +9236,12 @@ supports-color@^3.1.2, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" +supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" @@ -8716,6 +9261,14 @@ swap-case@^1.1.0: lower-case "^1.1.1" upper-case "^1.1.1" +symbol-observable@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" + +symbol-observable@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" + symbol-observable@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" @@ -8747,9 +9300,18 @@ tapable@^0.2.5, tapable@^0.2.7: version "0.2.8" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" +tar-fs@^1.13.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.0.tgz#e877a25acbcc51d8c790da1c57c9cf439817b896" + dependencies: + chownr "^1.0.1" + mkdirp "^0.5.1" + pump "^1.0.0" + tar-stream "^1.1.2" + tar-pack@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" + version "3.4.1" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" dependencies: debug "^2.2.0" fstream "^1.0.10" @@ -8760,9 +9322,9 @@ tar-pack@^3.4.0: tar "^2.2.1" uid-number "^0.0.6" -tar-stream@^1.5.0: - version "1.5.4" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.4.tgz#36549cf04ed1aee9b2a30c0143252238daf94016" +tar-stream@^1.1.2, tar-stream@^1.5.0: + version "1.5.5" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.5.tgz#5cad84779f45c83b1f2508d96b09d88c7218af55" dependencies: bl "^1.0.0" end-of-stream "^1.0.0" @@ -8778,12 +9340,12 @@ tar@^2.0.0, tar@^2.2.1: inherits "2" tar@^4.0.0, tar@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.0.1.tgz#3f5b2e5289db30c2abe4c960f43d0d9fff96aaf0" + version "4.0.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.0.2.tgz#e8e22bf3eec330e5c616d415a698395e294e8fad" dependencies: chownr "^1.0.1" - minipass "^2.0.2" - minizlib "^1.0.3" + minipass "^2.2.1" + minizlib "^1.0.4" mkdirp "^0.5.0" yallist "^3.0.2" @@ -8810,8 +9372,8 @@ test-exclude@^4.1.1: tether "^1.1.0" tether@^1.1.0, tether@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.0.tgz#0f9fa171f75bf58485d8149e94799d7ae74d1c1a" + version "1.4.2" + resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.2.tgz#ab9605b5ecf38f088b3da3d54d2b439207e48d04" text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" @@ -8844,7 +9406,7 @@ timed-out@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" -timers-browserify@^2.0.2: +timers-browserify@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6" dependencies: @@ -8865,12 +9427,18 @@ title-case@^2.1.0: no-case "^2.2.0" upper-case "^1.0.3" -tmp@0.0.31, tmp@0.0.x: +tmp@0.0.31: version "0.0.31" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" dependencies: os-tmpdir "~1.0.1" +tmp@0.0.x: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -8917,10 +9485,10 @@ to-single-quotes@^2.0.0: resolved "https://registry.yarnpkg.com/to-single-quotes/-/to-single-quotes-2.0.1.tgz#7cc29151f0f5f2c41946f119f5932fe554170125" toposort@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.4.tgz#a86107690cbee8cae43b349d2f60162500924dfc" + 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.2, 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: @@ -8938,6 +9506,12 @@ trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +"true-case-path@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" + dependencies: + glob "^6.0.4" + tryit@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" @@ -8947,19 +9521,20 @@ tryor@~0.1.2: resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b" ts-jest@^21.1.3: - version "21.1.3" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-21.1.3.tgz#cc3c552e7e8a67db9ededc28c00ae98223614ddc" + version "21.2.3" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-21.2.3.tgz#d90cd143c433e8dfd9b8df54921c7f3f1d8b9819" 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" - fs-extra "^4.0.0" + cpx "^1.5.0" + fs-extra "^4.0.2" jest-config "^21.2.1" jest-util "^21.2.1" pkg-dir "^2.0.0" source-map-support "^0.5.0" - yargs "^9.0.1" + yargs "^10.0.3" ts-loader@^2.3.7: version "2.3.7" @@ -8971,8 +9546,8 @@ ts-loader@^2.3.7: semver "^5.0.1" tslib@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.7.1.tgz#bc8004164691923a79fe8378bbeb3da2017538ec" + version "1.8.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.0.tgz#dc604ebad64bcbf696d613da6c954aa0e7ea1eb6" tslint-loader@^3.5.3: version "3.5.3" @@ -8985,11 +9560,12 @@ tslint-loader@^3.5.3: semver "^5.3.0" tslint@^5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552" + version "5.8.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.8.0.tgz#1f49ad5b2e77c76c3af4ddcae552ae4e3612eb13" dependencies: babel-code-frame "^6.22.0" - colors "^1.1.2" + builtin-modules "^1.1.1" + chalk "^2.1.0" commander "^2.9.0" diff "^3.2.0" glob "^7.1.1" @@ -8997,11 +9573,11 @@ tslint@^5.7.0: resolve "^1.3.2" semver "^5.3.0" tslib "^1.7.1" - tsutils "^2.8.1" + tsutils "^2.12.1" -tsutils@^2.8.1: - version "2.10.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.10.0.tgz#ae94511df2656eb06e4424056fba5c388887040c" +tsutils@^2.12.1: + version "2.12.2" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.12.2.tgz#ad58a4865d17ec3ddb6631b6ca53be14a5656ff3" dependencies: tslib "^1.7.1" @@ -9015,6 +9591,10 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -9037,12 +9617,12 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" typescript@^2.5.2: - version "2.5.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d" + version "2.6.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631" ua-parser-js@^0.7.9: - version "0.7.14" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca" + version "0.7.17" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" uglify-js@2.6.x: version "2.6.4" @@ -9053,12 +9633,12 @@ uglify-js@2.6.x: uglify-to-browserify "~1.0.0" yargs "~3.10.0" -uglify-js@3.1.x: - version "3.1.2" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.1.2.tgz#b50bcf15a5fd9e9ed40afbcdef3b59d6891b291f" +uglify-js@3.2.x: + version "3.2.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.2.0.tgz#cb411ee4ca0e0cadbfe3a4e1a1da97e6fa0d19c1" dependencies: - commander "~2.11.0" - source-map "~0.5.1" + commander "~2.12.1" + source-map "~0.6.1" uglify-js@^2.6, uglify-js@^2.8.29: version "2.8.29" @@ -9090,8 +9670,8 @@ ultron@1.0.x: resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" ultron@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" + version "1.1.1" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" umask@^1.1.0, umask@~1.1.0: version "1.1.0" @@ -9109,6 +9689,10 @@ underscore@~1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604" +underscore@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" + underscore@~1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209" @@ -9169,11 +9753,29 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +unzip-response@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" + unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" -update-notifier@^2.2.0, update-notifier@~2.2.0: +update-notifier@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.3.0.tgz#4e8827a6bb915140ab093559d7014e3ebb837451" + dependencies: + boxen "^1.2.1" + chalk "^2.0.1" + configstore "^3.0.0" + import-lazy "^2.1.0" + is-installed-globally "^0.1.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + +update-notifier@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.2.0.tgz#1b5837cf90c0736d88627732b661c138f86de72f" dependencies: @@ -9275,7 +9877,7 @@ uuid@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" -uuid@^3.0.0, uuid@~3.1.0: +uuid@^3.0.0, uuid@^3.1.0, uuid@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" @@ -9328,14 +9930,14 @@ vow-fs@~0.3.4: vow-queue "^0.4.1" vow-queue@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/vow-queue/-/vow-queue-0.4.2.tgz#e7fe17160e15c7c4184d1b666a9bc64e18e30184" + version "0.4.3" + resolved "https://registry.yarnpkg.com/vow-queue/-/vow-queue-0.4.3.tgz#4ba8f64b56e9212c0dbe57f1405aeebd54cce78d" dependencies: - vow "~0.4.0" + vow "^0.4.17" -vow@^0.4.7, vow@~0.4.0, vow@~0.4.1, vow@~0.4.8: - version "0.4.16" - resolved "https://registry.yarnpkg.com/vow/-/vow-0.4.16.tgz#bb9d54d938d5f80520d658a740e7a895e30feeeb" +vow@^0.4.17, vow@^0.4.7, vow@~0.4.1, vow@~0.4.8: + version "0.4.17" + resolved "https://registry.yarnpkg.com/vow/-/vow-0.4.17.tgz#b16e08fae58c52f3ebc6875f2441b26a92682904" w3c-blob@0.0.1: version "0.0.1" @@ -9381,8 +9983,8 @@ webidl-conversions@^4.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" webpack-bundle-analyzer@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.9.0.tgz#b58bc34cc30b27ffdbaf3d00bf27aba6fa29c6e3" + version "2.9.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.9.1.tgz#c2c8e03e8e5768ed288b39ae9e27a8b8d7b9d476" dependencies: acorn "^5.1.1" chalk "^1.1.3" @@ -9394,7 +9996,7 @@ webpack-bundle-analyzer@^2.9.0: lodash "^4.17.4" mkdirp "^0.5.1" opener "^1.4.3" - ws "^2.3.1" + ws "^3.3.1" webpack-cleanup-plugin@^0.5.1: version "0.5.1" @@ -9411,32 +10013,32 @@ webpack-core@^0.6.5: source-list-map "~0.1.7" source-map "~0.4.1" -webpack-dev-middleware@^1.0.11: - version "1.12.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709" +webpack-dev-middleware@^1.12.0: + version "1.12.1" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.1.tgz#338be3ca930973be1c2ce07d84d275e997e1a25a" dependencies: memory-fs "~0.4.1" - mime "^1.3.4" + mime "^1.4.1" path-is-absolute "^1.0.0" range-parser "^1.0.3" time-stamp "^2.0.0" webpack-merge@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.0.tgz#6ad72223b3e0b837e531e4597c199f909361511e" + version "4.1.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.1.tgz#f1197a0a973e69c6fbeeb6d658219aa8c0c13555" dependencies: lodash "^4.17.4" webpack-sources@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" + version "1.1.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" dependencies: source-list-map "^2.0.0" - source-map "~0.5.3" + source-map "~0.6.1" webpack@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.6.0.tgz#a89a929fbee205d35a4fa2cc487be9cbec8898bc" + version "3.8.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.8.1.tgz#b16968a81100abe61608b0153c9159ef8bb2bd83" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0" @@ -9462,10 +10064,10 @@ webpack@^3.6.0: yargs "^8.0.2" whatwg-encoding@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4" + version "1.0.3" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz#57c235bc8657e914d24e1a397d3c82daee0a6ba3" dependencies: - iconv-lite "0.4.13" + iconv-lite "0.4.19" whatwg-fetch@>=0.10.0: version "2.0.3" @@ -9496,7 +10098,7 @@ which@1, which@^1.2.1, which@^1.2.10, which@^1.2.12, which@^1.2.14, which@^1.2.4 dependencies: isexe "^2.0.0" -which@~1.2.1, which@~1.2.10: +which@~1.2.1: version "1.2.14" resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" dependencies: @@ -9543,8 +10145,8 @@ wordwrap@~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: - version "1.5.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d" + version "1.5.2" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae" dependencies: errno "^0.1.4" xtend "^4.0.1" @@ -9560,7 +10162,15 @@ wrappy@1, wrappy@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" -write-file-atomic@^2.0.0, write-file-atomic@^2.1.0, write-file-atomic@~2.1.0: +write-file-atomic@^2.0.0, write-file-atomic@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-file-atomic@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.1.0.tgz#1769f4b551eedce419f0505deae2e26763542d37" dependencies: @@ -9581,11 +10191,12 @@ ws@1.1.2: options ">=0.0.5" ultron "1.0.x" -ws@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-2.3.1.tgz#6b94b3e447cb6a363f785eaf94af6359e8e81c80" +ws@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.2.tgz#96c1d08b3fefda1d5c1e33700d3bfaa9be2d5608" dependencies: - safe-buffer "~5.0.1" + async-limiter "~1.0.0" + safe-buffer "~5.1.0" ultron "~1.1.0" wtf-8@1.0.0: @@ -9618,7 +10229,7 @@ xmlhttprequest@1: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" -xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: +xtend@4.0.1, xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -9646,6 +10257,29 @@ yargs-parser@^7.0.0: dependencies: camelcase "^4.1.0" +yargs-parser@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.0.0.tgz#21d476330e5a82279a4b881345bf066102e219c6" + dependencies: + camelcase "^4.1.0" + +yargs@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.0.3.tgz#6542debd9080ad517ec5048fb454efe9e4d4aaae" + dependencies: + cliui "^3.2.0" + decamelize "^1.1.1" + find-up "^2.1.0" + get-caller-file "^1.0.1" + os-locale "^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 "^8.0.0" + yargs@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" @@ -9682,7 +10316,7 @@ yargs@^8.0.2: y18n "^3.2.1" yargs-parser "^7.0.0" -yargs@^9.0.0, yargs@^9.0.1: +yargs@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c" dependencies: