Merge branch 'master' into loki-query-editor

pull/15012/head
Torkel Ödegaard 6 years ago
commit 6e0b873739
  1. 1
      .circleci/config.yml
  2. 17
      CHANGELOG.md
  3. 6
      devenv/datasources.yaml
  4. 22
      devenv/docker/blocks/loki/docker-compose.yaml
  5. 4
      docs/sources/features/datasources/stackdriver.md
  6. 26
      docs/sources/features/explore/index.md
  7. 2
      docs/sources/guides/whats-new-in-v5-3.md
  8. 159
      docs/sources/guides/whats-new-in-v6-0.md
  9. 5
      docs/sources/reference/templating.md
  10. 9
      package.json
  11. 4
      packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx
  12. 8
      packages/grafana-ui/src/components/FormLabel/FormLabel.tsx
  13. 8
      packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
  14. 29
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  15. 1
      packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss
  16. 2
      packages/grafana-ui/src/components/Switch/Switch.tsx
  17. 12
      packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
  18. 17
      packages/grafana-ui/src/components/Tooltip/_Tooltip.scss
  19. 17
      packages/grafana-ui/src/types/data.ts
  20. 4
      packages/grafana-ui/src/types/datasource.ts
  21. 2
      packages/grafana-ui/src/types/index.ts
  22. 7
      packages/grafana-ui/src/types/panel.ts
  23. 2
      pkg/api/app_routes.go
  24. 2
      pkg/api/grafana_com_proxy.go
  25. 2
      pkg/api/pluginproxy/ds_auth_provider.go
  26. 6
      pkg/api/pluginproxy/ds_proxy.go
  27. 2
      pkg/api/pluginproxy/pluginproxy.go
  28. 2
      pkg/api/render.go
  29. 2
      pkg/middleware/dashboard_redirect_test.go
  30. 2
      pkg/models/datasource.go
  31. 4
      pkg/plugins/frontend_plugin.go
  32. 2
      pkg/plugins/plugins.go
  33. 144
      pkg/services/alerting/notifiers/pushover.go
  34. 2
      pkg/services/auth/auth_token.go
  35. 2
      pkg/services/dashboards/dashboard_service.go
  36. 4
      pkg/services/provisioning/notifiers/alert_notifications.go
  37. 4
      pkg/services/sqlstore/alert_notification.go
  38. 2
      pkg/services/sqlstore/dashboard.go
  39. 4
      pkg/services/sqlstore/dashboard_test.go
  40. 27
      pkg/services/sqlstore/sqlstore.go
  41. 2
      pkg/tsdb/mssql/mssql.go
  42. 8
      pkg/tsdb/stackdriver/stackdriver.go
  43. 8
      pkg/tsdb/stackdriver/stackdriver_test.go
  44. 9
      pkg/util/encoding.go
  45. 2
      pkg/util/encryption.go
  46. 6
      pkg/util/filepath.go
  47. 3
      pkg/util/ip.go
  48. 10
      pkg/util/ip_test.go
  49. 1
      pkg/util/json.go
  50. 2
      pkg/util/md5.go
  51. 12
      pkg/util/shortid_generator.go
  52. 2
      pkg/util/shortid_generator_test.go
  53. 4
      pkg/util/strings.go
  54. 15
      pkg/util/url.go
  55. 20
      pkg/util/url_test.go
  56. 1
      pkg/util/validation.go
  57. 4
      public/app/core/components/Page/Page.tsx
  58. 2
      public/app/core/components/PageHeader/PageHeader.tsx
  59. 2
      public/app/core/components/PageLoader/PageLoader.tsx
  60. 103
      public/app/core/profiler.ts
  61. 83
      public/app/core/redux/actionCreatorFactory.test.ts
  62. 57
      public/app/core/redux/actionCreatorFactory.ts
  63. 4
      public/app/core/redux/index.ts
  64. 97
      public/app/core/redux/reducerFactory.test.ts
  65. 45
      public/app/core/redux/reducerFactory.ts
  66. 2
      public/app/core/services/backend_srv.ts
  67. 11
      public/app/core/time_series2.ts
  68. 22
      public/app/core/utils/CancelablePromise.ts
  69. 20
      public/app/features/admin/ServerStats.tsx
  70. 322
      public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap
  71. 5
      public/app/features/alerting/AlertRuleList.test.tsx
  72. 25
      public/app/features/alerting/AlertRuleList.tsx
  73. 4
      public/app/features/alerting/AlertTab.tsx
  74. 2
      public/app/features/alerting/StateHistory.tsx
  75. 2
      public/app/features/alerting/TestRuleResult.test.tsx
  76. 2
      public/app/features/alerting/TestRuleResult.tsx
  77. 30
      public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap
  78. 18
      public/app/features/alerting/state/actions.ts
  79. 2
      public/app/features/alerting/state/reducers.test.ts
  80. 10
      public/app/features/alerting/state/reducers.ts
  81. 2
      public/app/features/annotations/annotations_srv.ts
  82. 2
      public/app/features/annotations/specs/annotations_srv.test.ts
  83. 4
      public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx
  84. 2
      public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts
  85. 2
      public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
  86. 2
      public/app/features/dashboard/components/DashNav/DashNavCtrl.ts
  87. 2
      public/app/features/dashboard/components/DashNav/template.html
  88. 4
      public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx
  89. 35
      public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx
  90. 1
      public/app/features/dashboard/components/DashboardRow/index.ts
  91. 2
      public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
  92. 2
      public/app/features/dashboard/components/RowOptions/RowOptionsCtrl.ts
  93. 2
      public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts
  94. 2
      public/app/features/dashboard/components/VersionHistory/HistorySrv.test.ts
  95. 2
      public/app/features/dashboard/components/VersionHistory/HistorySrv.ts
  96. 4
      public/app/features/dashboard/containers/DashboardCtrl.ts
  97. 123
      public/app/features/dashboard/containers/SoloPanelPage.tsx
  98. 59
      public/app/features/dashboard/dashgrid/DashboardGrid.tsx
  99. 5
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  100. 61
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -333,6 +333,7 @@ jobs:
docker:
- image: grafana/grafana-ci-deploy:1.2.0
steps:
- checkout
- attach_workspace:
at: .
- run:

@ -1,15 +1,23 @@
# 6.0.0-beta1 (unreleased)
# 6.0.0-beta2 (unreleased)
### Minor
* **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae)
# 6.0.0-beta1 (2019-01-30)
### New Features
* **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
* **Elasticsearch**: Support bucket script pipeline aggregations [#5968](https://github.com/grafana/grafana/issues/5968)
* **Influxdb**: Add support for time zone (`tz`) clause [#10322](https://github.com/grafana/grafana/issues/10322), thx [@cykl](https://github.com/cykl)
* **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
* **Provisioning**: Provisioning support for alert notifiers [#10487](https://github.com/grafana/grafana/issues/10487), thx [@pbakulev](https://github.com/pbakulev)
* **Explore**: A whole new way to do ad-hoc metric queries and exploration. Split view in half and compare metrics & logs and much much more. [Read more here](http://docs.grafana.org/features/explore/)
### Minor
* **Alerting**: Use seperate timeouts for alert evals and notifications [#14701](https://github.com/grafana/grafana/issues/14701), thx [@sharkpc0813](https://github.com/sharkpc0813)
* **Templating**: Built in time range variables `$__from` and `$__to`, [#1909](https://github.com/grafana/grafana/issues/1909)
* **Alerting**: Use separate timeouts for alert evals and notifications [#14701](https://github.com/grafana/grafana/issues/14701), thx [@sharkpc0813](https://github.com/sharkpc0813)
* **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
* **Elasticsearch**: Add support for moving average and derivative using doc count (metric count) [#8843](https://github.com/grafana/grafana/issues/8843) [#11175](https://github.com/grafana/grafana/issues/11175)
* **Elasticsearch**: Add support for template variable interpolation in alias field [#4075](https://github.com/grafana/grafana/issues/4075), thx [@SamuelToh](https://github.com/SamuelToh)
@ -27,18 +35,21 @@
* **Dashboard**: `Min width` changed to `Max per row` for repeating panels. This lets you specify the maximum number of panels to show per row and by that repeated panels will always take up full width of row [#12991](https://github.com/grafana/grafana/pull/12991), thx [@pgiraud](https://github.com/pgiraud)
* **Dashboard**: Retain decimal precision when exporting CSV [#13929](https://github.com/grafana/grafana/issues/13929), thx [@cinaglia](https://github.com/cinaglia)
* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
* **Templating**: Add percentencode formatting to variable interpolation to be used mainly for url escaping [#12764](https://github.com/grafana/grafana/issues/12764), thx [@cxcv](https://github.com/cxcv)
* **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
* **Units**: Add Floating Point Operations per Second units [#14558](https://github.com/grafana/grafana/pull/14558), thx [@hahnjo](https://github.com/hahnjo)
* **Table**: Renders epoch string as date if date column style [#14484](https://github.com/grafana/grafana/issues/14484)
* **Piechart/Flot**: Fixes multiple piechart instances with donut bug [#15062](https://github.com/grafana/grafana/pull/15062)
* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
* **Dataproxy**: Add global datasource proxy timeout setting [#5699](https://github.com/grafana/grafana/issues/5699), thx [@RangerRick](https://github.com/RangerRick)
* **Database**: Support specifying database host using IPV6 for backend database and sql datasources [#13711](https://github.com/grafana/grafana/issues/13711), thx [@ellisvlad](https://github.com/ellisvlad)
* **Database**: Support defining additonal database connection string args when using `url` property in database settings [#14709](https://github.com/grafana/grafana/pull/14709), thx [@tpetr](https://github.com/tpetr)
* **Stackdriver**: crossSeriesAggregation not being sent with the query [#15129](https://github.com/grafana/grafana/issues/15129), thx [@Legogris](https://github.com/Legogris)
### Bug fixes
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
* **Prometheus**: Query for annotation always uses 60s step regardless of dashboard range, fixes [#14795](https://github.com/grafana/grafana/issues/14795)
* **Annotations**: Fix creating annotation when graph panel has no data points position the popup outside viewport [#13765](https://github.com/grafana/grafana/issues/13765), thx [@banjeremy](https://github.com/banjeremy)
* **Piechart/Flot**: Fixes multiple piechart instances with donut bug [#15062](https://github.com/grafana/grafana/pull/15062)
### Breaking changes
* **Text Panel**: The text panel does no longer by default allow unsantizied HTML. [#4117](https://github.com/grafana/grafana/issues/4117). This means that if you have text panels with scripts tags they will no longer work as before. To enable unsafe javascript execution in text panels enable the settings `disable_sanitize_html` under the section `[panels]` in your Grafana ini file, or set env variable `GF_PANELS_DISABLE_SANITIZE_HTML=true`.

@ -152,4 +152,10 @@ datasources:
authType: credentials
defaultRegion: eu-west-2
- name: gdev-loki
type: loki
access: proxy
url: http://localhost:3100
editable: false

@ -0,0 +1,22 @@
version: "3"
networks:
loki:
services:
loki:
image: grafana/loki:master
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
networks:
- loki
promtail:
image: grafana/promtail:master
volumes:
- /var/log:/var/log
command:
-config.file=/etc/promtail/docker-config.yaml
networks:
- loki

@ -12,8 +12,8 @@ weight = 11
# Using Google Stackdriver in Grafana
> Only available in Grafana v5.3+.
> The datasource is currently a beta feature and is subject to change.
> Available as a beta feature in Grafana v5.3.x and v5.4.x.
> Officially released in Grafana v6.0.0
Grafana ships with built-in support for Google Stackdriver. Just add it as a datasource and you are ready to build dashboards for your Stackdriver metrics.

@ -27,23 +27,6 @@ For infrastructure monitoring and incident response, you no longer need to switc
If you just want to explore your data and do not want to create a dashboard then Explore makes this much easier. Explore will show the results as both a graph and a table enabling you to see trends in the data and more detail at the same time (if the datasource supports both graph and table data).
## Turning the Explore Feature On
Explore will be officially released in Grafana 6.0. It is however already in the latest nightly builds of Grafana and can be turned using a feature flag in the config file. Restart Grafana after making the config file change.
```ini
[explore]
# Enable the Explore section
enabled = true
```
Or if using docker:
```bash
docker pull grafana/grafana:master
docker run --name grafana -p 3000:3000 -e "GF_EXPLORE_ENABLED=true" grafana/grafana:master
```
## How to Start Exploring
There is a new Explore icon on the menu bar to the left. This opens a new empty Explore tab.
@ -116,7 +99,14 @@ The Logs Explorer (the `Log labels` button) next to the query field shows a list
Once the result is returned, the log panel shows a list of log rows and a bar chart where the x-axis shows the time and the y-axis shows the frequency/count.
{{< docs-imagebox img="/img/docs/v60/explore_loki.png" class="docs-image--no-shadow" caption="Explore Loki Log Streams" >}}
<div class="medium-6 columns">
<video width="800" height="500" controls>
<source src="/assets/videos/explore_loki.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<br />
#### Log Stream Selector

@ -26,7 +26,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article
{{< docs-imagebox img="/img/docs/v53/stackdriver-with-heatmap.png" max-width= "600px" class="docs-image--no-shadow docs-image--right" >}}
Grafana v5.3 ships with built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) and enables you to visualize your Stackdriver metrics in Grafana.
Grafana v5.3 ships with built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) and enables you to visualize your Stackdriver metrics in Grafana.
Getting started with the plugin is easy. Simply create a GCE Service account that has access to the Stackdriver API scope, download the Service Account key file from Google and upload it on the Stackdriver datasource config page in Grafana and you should have a secure server-to-server authentication setup. Like other core plugins, Stackdriver has built-in support for alerting. It also comes with support for heatmaps and basic variables.

@ -0,0 +1,159 @@
+++
title = "What's New in Grafana v6.0"
description = "Feature & improvement highlights for Grafana v6.0"
keywords = ["grafana", "new", "documentation", "6.0"]
type = "docs"
[menu.docs]
name = "Version 6.0"
identifier = "v6.0"
parent = "whatsnew"
weight = -11
+++
# What's New in Grafana v6.0
This update to Grafana introduces a new way of exploring your data, support for log data and tons of other features.
Grafana v6.0 is out in **Beta**, [Download Now!](https://grafana.com/grafana/download/beta)
The main highlights are:
- [Explore]({{< relref "#explore" >}}) - A new query focused workflow for ad-hoc data exploration and troubleshooting.
- [Grafana Loki]({{< relref "#explore-and-grafana-loki" >}}) - Integration with the new open source log aggregation system from Grafana Labs.
- [Gauge Panel]({{< relref "#gauge-panel" >}}) - A new standalone panel for gauges.
- [New Panel Editor UX]({{< relref "#new-panel-editor" >}}) improves panel editing
and enables easy switching between different visualizations.
- [Google Stackdriver Datasource]({{< relref "#google-stackdriver-datasource" >}}) is out of beta and is officially released.
- [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource
- [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins.
- [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker.
## Explore
{{< docs-imagebox img="/img/docs/v60/explore_prometheus.png" max-width="800px" class="docs-image--right" caption="Screenshot of the new Explore option in the panel menu" >}}
Grafana's dashboard UI is all about building dashboards for visualization. **Explore** strips away all the dashboard and panel options so that you can focus on the query & metric exploration. Iterate until you have a working query and then think about building a dashboard. You can also jump from a dashboard panel into **Explore** and from there do some ad-hoc query exporation with the panel queries as a starting point.
For infrastructure monitoring and incident response, you no longer need to switch to other tools to debug what went wrong. **Explore** allows you to dig deeper into your metrics and logs to find the cause. Grafana's new logging datasource, [Loki](https://github.com/grafana/loki) is tightly integrated into Explore and allows you to correlate metrics and logs by viewing them side-by-side.
**Explore** is a new paradigm for Grafana. It creates a new interactive debugging workflow that integrates two pillars
of observability - metrics and logs. Explore works with every datasource but for Prometheus we have customized the
query editor and the experience to provide the best possible exploration UX.
### Explore and Prometheus
Explore features a new [Prometheus query editor](/features/explore/#prometheus-specific-features). This new editor has improved autocomplete, metric tree selector,
integrations with the Explore table view for easy label filtering and useful query hints that can automatically apply
functions to your query. There is also integration between Prometheus and Grafana Loki (see more about Loki below) that
enabled jumping between metrics query and logs query with preserved label filters.
### Explore splits
Explore supports splitting the view so you can compare different queries, different datasources and metrics & logs side by side!
{{< docs-imagebox img="/img/docs/v60/explore_split.png" max-width="800px" caption="Screenshot of the new Explore option in the panel menu" >}}
<br />
### Explore and Grafana Loki
The log exploration & visualization features in Explore are available to any data source but are currently only implemented by the new open source log
aggregation system from Grafana Lab called [Grafana Loki](https://github.com/grafana/loki).
Loki is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus. It is designed to be very cost effective, as it does not index the contents of the logs, but rather a set of labels for each log stream. The logs from Loki are queried in a similar way to querying with label selectors in Prometheus. It uses labels to group log streams which can be made to match up with your Prometheus labels.
Read more about Grafana Loki [here](https://github.com/grafana/loki) or [Grafana Labs hosted Loki](https://grafana.com/loki).
The Explore feature allows you to query logs and features a new log panel. In the near future, we will be adding support
for other log sources to Explore and the next planned integration is Elasticsearch.
<div class="medium-6 columns">
<video width="800" height="500" controls>
<source src="/assets/videos/explore_loki.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<br />
## New Panel Editor
Grafana v6.0 has a completely redesigned UX around editing panels. You can now resize the visualization area if you want
more space for queries & options and vice versa. You can now also change visualization (panel type) from within the new
panel edit mode. No need to add a new panel to try out different visualizations! Checkout the
video below to see the new Panel Editor in action.
<div class="medium-6 columns">
<video width="800" height="500" controls>
<source src="/assets/videos/panel_change_viz.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<br>
### Gauge Panel
We have created a new separate Gauge panel as we felt having this visualization be a hidden option in the Singlestat panel
was not ideal. When it supports 100% of the Singlestat Gauge features we plan to add a migration so all
singlestats that use it become Gauge panels instead. This new panel contains a new **Threshold** editor that we will
continue to refine and start using in other panels.
{{< docs-imagebox img="/img/docs/v60/gauge_panel.png" max-width="600px" caption="Gauge Panel" >}}
<br>
### React Panels & Query Editors
A major part of all the work that has gone into Grafana v6.0 has been on the migration to React. This investment
is part of the future proofing of Grafana and it's code base and ecosystem. Starting in v6.0 **Panels** and **Data
source** plugins can be written in React using our published `@grafana/ui` sdk library. More information on this
will be shared closer to or just after release.
{{< docs-imagebox img="/img/docs/v60/react_panels.png" max-width="600px" caption="React Panel" >}}
<br />
### Google Stackdriver Datasource
Built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) is officially released in Grafana 6.0. Beta support was added in Grafana 5.3 and we have added lots of improvements since then.
To get started read the guide: [Using Google Stackdriver in Grafana](/features/datasources/stackdriver/).
### Azure Monitor Datasource
One of the goals of the Grafana v6.0 release is to add support for the three major clouds. Amazon Cloudwatch has been a core datasource for years and Google Stackdriver is also now supported. We developed an external plugin for Azure Monitor last year and for this release the [plugin](https://grafana.com/plugins/grafana-azure-monitor-datasource) is being moved into Grafana to be one of the built-in datasources. For users of the external plugin, Grafana will automatically start using the built-in version. As a core datasource, the Azure Monitor datasource will get alerting support for the official 6.0 release.
The Azure Monitor datasource integrates four Azure services with Grafana - Azure Monitor, Azure Log Analytics, Azure Application Insights and Azure Application Insights Analytics.
### Provisioning support for alert notifiers
Grafana now added support for provisioning alert notifiers from configuration files. Allowing operators to provision notifiers without using the UI or the API. A new field called `uid` has been introduced which is a string identifier that the administrator can set themselves. Same kind of identifier used for dashboards since v5.0. This feature makes it possible to use the same notifier configuration in multiple environments and refer to notifiers in dashboard json by a string identifier instead of the numeric id which depends on insert order and how many notifiers that exists in the instance.
### Auth and session token improvements
The previous session storage implementation in Grafana was causing problems in larger HA setups due to too many write requests to the database. The remember me token also have several security issues which is why we decided to rewrite auth middleware in Grafana and remove the session storage since most operations using the session storage could be rewritten to use cookies or data already made available earlier in the request.
If you are using `Auth proxy` for authentication the session storage will still be used but our goal is to remove this ASAP as well.
This release will force all users to log in again since their previous token is not valid anymore.
### Named Colors
{{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}}
We have updated the color picker to show named colors and primary colors. We hope this will improve accessibility and
helps making colors more consistent across dashboards. We hope to do more in this color picker in the future, like show
colors used in the dashboard.
Named colors also enables Grafana to adapt colors to the current theme.
<div class="clearfix"></div>
### Other features
- The ElasticSearch datasource now supports [bucket script pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html). This gives the ability to do per bucket computations like the difference or ratio between two metrics.
- Support for Google Hangouts Chat alert notifications
- New built in template variables for the current time range in `$__from` and `$__to`
## Changelog
Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list of new features, changes, and bug fixes.

@ -245,6 +245,11 @@ summarize($myinterval, sum, false)
Grafana has global built-in variables that can be used in expressions in the query editor.
### Time range variables
Grafana has two built in time range variables in `$__from` and `$__to`. They are currently always interpolated
as epoch milliseconds. These variables are only available in Grafana v6.0 and above.
### The $__interval Variable
This $__interval variable is similar to the `auto` interval variable that is described above. It can be used as a parameter to group by time (for InfluxDB, MySQL, Postgres, MSSQL), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite).

@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "6.0.0-pre1",
"version": "6.0.0-prebeta2",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@ -25,7 +25,9 @@
"@types/node": "^8.0.31",
"@types/react": "^16.7.6",
"@types/react-dom": "^16.0.9",
"@types/react-grid-layout": "^0.16.6",
"@types/react-select": "^2.0.4",
"@types/react-virtualized": "^9.18.12",
"angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0",
"axios": "^0.17.1",
@ -69,6 +71,7 @@
"load-grunt-tasks": "3.5.2",
"mini-css-extract-plugin": "^0.4.0",
"mocha": "^4.0.1",
"monaco-editor": "^0.15.6",
"ng-annotate-loader": "^0.6.1",
"ng-annotate-webpack-plugin": "^0.3.0",
"ngtemplate-loader": "^2.0.1",
@ -82,6 +85,7 @@
"prettier": "1.9.2",
"react-hot-loader": "^4.3.6",
"react-test-renderer": "^16.5.0",
"regexp-replace-loader": "^1.0.1",
"sass-lint": "^1.10.2",
"sass-loader": "^7.0.1",
"sinon": "1.17.6",
@ -114,7 +118,8 @@
"typecheck": "tsc --noEmit",
"jest": "jest --notify --watch",
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
"precommit": "grunt precommit"
"precommit": "grunt precommit",
"storybook": "cd packages/grafana-ui && yarn storybook"
},
"husky": {
"hooks": {

@ -1,7 +1,6 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs';
import SpectrumPalette from './SpectrumPalette';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
@ -11,8 +10,9 @@ const SpectrumPaletteStories = storiesOf('UI/ColorPicker/Palettes/SpectrumPalett
SpectrumPaletteStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
SpectrumPaletteStories.add('Named colors swatch - support for named colors', () => {
SpectrumPaletteStories.add('default', () => {
const selectedTheme = getThemeKnob();
return (
<UseState initialState="red">
{(selectedColor, updateSelectedColor) => {

@ -1,6 +1,6 @@
import React, { FunctionComponent, ReactNode } from 'react';
import classNames from 'classnames';
import { Tooltip } from '..';
import { Tooltip } from '../Tooltip/Tooltip';
interface Props {
children: ReactNode;
@ -31,9 +31,9 @@ export const FormLabel: FunctionComponent<Props> = ({
<label className={classes} {...rest} htmlFor={htmlFor}>
{children}
{tooltip && (
<Tooltip placement="auto" content={tooltip}>
<div className="gf-form-help-icon--right-normal">
<i className="gicon gicon-question gicon--has-hover" />
<Tooltip placement="top" content={tooltip} theme={"info"}>
<div className="gf-form-help-icon gf-form-help-icon--right-normal">
<i className="fa fa-info-circle" />
</div>
</Tooltip>
)}

@ -2,7 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { Gauge, Props } from './Gauge';
import { TimeSeriesVMs } from '../../types/series';
import { TimeSeriesVMs } from '../../types/data';
import { ValueMapping, MappingType } from '../../types';
jest.mock('jquery', () => ({
@ -116,7 +116,7 @@ describe('Format value', () => {
const result = instance.formatValue(value);
expect(result).toEqual(' 6.0 ');
expect(result).toEqual('6.0');
});
it('should return formatted value if there are no matching value mappings', () => {
@ -129,7 +129,7 @@ describe('Format value', () => {
const result = instance.formatValue(value);
expect(result).toEqual(' 10.0 ');
expect(result).toEqual('10.0');
});
it('should return mapped value if there are matching value mappings', () => {
@ -142,6 +142,6 @@ describe('Format value', () => {
const result = instance.formatValue(value);
expect(result).toEqual(' 1-20 ');
expect(result).toEqual('1-20');
});
});

@ -1,12 +1,9 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel';
import { TimeSeriesVMs } from '../../types/series';
import { GrafanaTheme } from '../../types';
import { ValueMapping, Threshold, BasicGaugeColor, TimeSeriesVMs, GrafanaTheme } from '../../types';
import { getMappedValue } from '../../utils/valueMappings';
import { getValueFormat } from '../../utils/valueFormats/valueFormats';
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
import { getColorFromHexRgbOrName, getValueFormat } from '../../utils';
type TimeSeriesValue = string | number | null;
@ -28,6 +25,8 @@ export interface Props {
theme?: GrafanaTheme;
}
const FONT_SCALE = 1;
export class Gauge extends PureComponent<Props> {
canvasElement: any;
@ -63,7 +62,7 @@ export class Gauge extends PureComponent<Props> {
if (valueMappings.length > 0) {
const valueMappedValue = getMappedValue(valueMappings, value);
if (valueMappedValue) {
return `${prefix} ${valueMappedValue.text} ${suffix}`;
return `${prefix && prefix + ' '}${valueMappedValue.text}${suffix && ' ' + suffix}`;
}
}
@ -71,7 +70,7 @@ export class Gauge extends PureComponent<Props> {
const formattedValue = formatFunc(value as number, decimals);
const handleNoValueValue = formattedValue || 'no value';
return `${prefix} ${handleNoValueValue} ${suffix}`;
return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`;
}
getFontColor(value: TimeSeriesValue) {
@ -102,7 +101,7 @@ export class Gauge extends PureComponent<Props> {
const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
const formattedThresholds = [
return [
...thresholdsSortedByIndex.map(threshold => {
if (threshold.index === 0) {
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme) };
@ -113,8 +112,13 @@ export class Gauge extends PureComponent<Props> {
}),
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme) },
];
}
return formattedThresholds;
getFontScale(length: number): number {
if (length > 12) {
return FONT_SCALE - length * 5 / 120;
}
return FONT_SCALE - length * 5 / 105;
}
draw() {
@ -138,13 +142,14 @@ export class Gauge extends PureComponent<Props> {
value = null;
}
const formattedValue = this.formatValue(value) as string;
const dimension = Math.min(width, height * 1.3);
const backgroundColor = theme === GrafanaTheme.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
const fontScale = parseInt('80', 10) / 100;
const fontSize = Math.min(dimension / 5, 100) * fontScale;
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
const thresholdMarkersWidth = gaugeWidth / 5;
const fontSize =
Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1);
const thresholdLabelFontSize = fontSize / 2.5;
const options = {
@ -175,7 +180,7 @@ export class Gauge extends PureComponent<Props> {
value: {
color: this.getFontColor(value),
formatter: () => {
return this.formatValue(value);
return formattedValue;
},
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
},

@ -10,6 +10,7 @@
font-size: 1.1rem;
background: $panel-options-group-header-bg;
position: relative;
border-radius: $border-radius $border-radius 0 0;
.btn {
position: absolute;

@ -23,7 +23,7 @@ export class Switch extends PureComponent<Props, State> {
internalOnChange = (event: React.FormEvent<HTMLInputElement>) => {
event.stopPropagation();
this.props.onChange();
this.props.onChange(event);
};
render() {

@ -1,20 +1,14 @@
import React, { createRef } from 'react';
import React, { createRef } from 'react';
import * as PopperJS from 'popper.js';
import Popper from './Popper';
import PopperController, { UsingPopperProps } from './PopperController';
export enum Themes {
Default = 'popper__background--default',
Error = 'popper__background--error',
Brand = 'popper__background--brand',
}
interface TooltipProps extends UsingPopperProps {
theme?: Themes;
theme?: 'info' | 'error';
}
export const Tooltip = ({ children, theme, ...controllerProps }: TooltipProps) => {
const tooltipTriggerRef = createRef<PopperJS.ReferenceObject>();
const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
const popperBackgroundClassName = 'popper__background' + (theme ? ' popper__background--' + theme : '');
return (
<PopperController {...controllerProps}>

@ -1,9 +1,11 @@
$popper-margin-from-ref: 5px;
@mixin popper-theme($backgroundColor, $arrowColor) {
@mixin popper-theme($backgroundColor, $textColor) {
background: $backgroundColor;
color: $textColor;
.popper__arrow {
border-color: $arrowColor;
border-color: $backgroundColor;
}
}
@ -17,9 +19,11 @@ $popper-margin-from-ref: 5px;
.popper__background {
background: $tooltipBackground;
border-radius: $border-radius;
border-radius: $border-radius-sm;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
padding: 10px;
padding: 6px 10px;
color: $tooltipColor;
font-weight: 500;
.popper__arrow {
border-color: $tooltipBackground;
@ -30,9 +34,8 @@ $popper-margin-from-ref: 5px;
@include popper-theme($tooltipBackgroundError, $tooltipBackgroundError);
}
&.popper__background--brand {
@include popper-theme($tooltipBackgroundBrand, $tooltipBackgroundBrand);
@include gradient-vertical($red, $orange);
&.popper__background--info {
@include popper-theme($popover-help-bg, $popover-help-color);
}
}

@ -52,3 +52,20 @@ export interface TimeSeriesVMs {
[index: number]: TimeSeriesVM;
length: number;
}
interface Column {
text: string;
title?: string;
type?: string;
sort?: boolean;
desc?: boolean;
filterable?: boolean;
unit?: string;
}
export interface TableData {
columns: Column[];
rows: any[];
type: string;
columnMap: any;
}

@ -1,9 +1,9 @@
import { TimeRange, RawTimeRange } from './time';
import { TimeSeries } from './series';
import { PluginMeta } from './plugin';
import { TableData, TimeSeries } from './data';
export interface DataQueryResponse {
data: TimeSeries[] | any;
data: TimeSeries[] | [TableData] | any;
}
export interface DataQuery {

@ -1,4 +1,4 @@
export * from './series';
export * from './data';
export * from './time';
export * from './panel';
export * from './plugin';

@ -1,4 +1,4 @@
import { TimeSeries, LoadingState } from './series';
import { TimeSeries, LoadingState, TableData } from './data';
import { TimeRange } from './time';
export type InterpolateFunction = (value: string, format?: string | Function) => string;
@ -14,6 +14,11 @@ export interface PanelProps<T = any> {
onInterpolate: InterpolateFunction;
}
export interface PanelData {
timeSeries?: TimeSeries[];
tableData?: TableData;
}
export interface PanelOptionsProps<T = any> {
options: T;
onChange: (options: T) => void;

@ -35,7 +35,7 @@ func (hs *HTTPServer) initAppPluginRoutes(r *macaron.Macaron) {
for _, plugin := range plugins.Apps {
for _, route := range plugin.Routes {
url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
url := util.JoinURLFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
handlers := make([]macaron.Handler, 0)
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{
ReqSignedIn: true,

@ -30,7 +30,7 @@ func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy {
req.URL.Host = url.Host
req.Host = url.Host
req.URL.Path = util.JoinUrlFragments(url.Path+"/api", proxyPath)
req.URL.Path = util.JoinURLFragments(url.Path+"/api", proxyPath)
// clear cookie headers
req.Header.Del("Cookie")

@ -39,7 +39,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
req.URL.Scheme = routeURL.Scheme
req.URL.Host = routeURL.Host
req.Host = routeURL.Host
req.URL.Path = util.JoinUrlFragments(routeURL.Path, proxyPath)
req.URL.Path = util.JoinURLFragments(routeURL.Path, proxyPath)
if err := addHeaders(&req.Header, route, data); err != nil {
logger.Error("Failed to render plugin headers", "error", err)

@ -139,19 +139,19 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
reqQueryVals := req.URL.Query()
if proxy.ds.Type == m.DS_INFLUXDB_08 {
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
reqQueryVals.Add("u", proxy.ds.User)
reqQueryVals.Add("p", proxy.ds.Password)
req.URL.RawQuery = reqQueryVals.Encode()
} else if proxy.ds.Type == m.DS_INFLUXDB {
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, proxy.proxyPath)
req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
req.URL.RawQuery = reqQueryVals.Encode()
if !proxy.ds.BasicAuth {
req.Header.Del("Authorization")
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.User, proxy.ds.Password))
}
} else {
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, proxy.proxyPath)
req.URL.Path = util.JoinURLFragments(proxy.targetUrl.Path, proxy.proxyPath)
}
if proxy.ds.BasicAuth {
req.Header.Del("Authorization")

@ -46,7 +46,7 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
req.URL.Host = targetURL.Host
req.Host = targetURL.Host
req.URL.Path = util.JoinUrlFragments(targetURL.Path, proxyPath)
req.URL.Path = util.JoinURLFragments(targetURL.Path, proxyPath)
// clear cookie headers
req.Header.Del("Cookie")

@ -14,7 +14,7 @@ import (
)
func (hs *HTTPServer) RenderToPng(c *m.ReqContext) {
queryReader, err := util.NewUrlQueryReader(c.Req.URL)
queryReader, err := util.NewURLQueryReader(c.Req.URL)
if err != nil {
c.Handle(400, "Render parameters error", err)
return

@ -20,7 +20,7 @@ func TestMiddlewareDashboardRedirect(t *testing.T) {
fakeDash.Id = 1
fakeDash.FolderId = 1
fakeDash.HasAcl = false
fakeDash.Uid = util.GenerateShortUid()
fakeDash.Uid = util.GenerateShortUID()
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash

@ -23,6 +23,7 @@ const (
DS_ACCESS_DIRECT = "direct"
DS_ACCESS_PROXY = "proxy"
DS_STACKDRIVER = "stackdriver"
DS_AZURE_MONITOR = "azure-monitor"
)
var (
@ -73,6 +74,7 @@ var knownDatasourcePlugins = map[string]bool{
DS_MYSQL: true,
DS_MSSQL: true,
DS_STACKDRIVER: true,
DS_AZURE_MONITOR: true,
"opennms": true,
"abhisant-druid-datasource": true,
"dalmatinerdb-datasource": true,

@ -45,9 +45,9 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
fp.BaseUrl = app.BaseUrl
if isExternalPlugin(app.PluginDir) {
fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
fp.Module = util.JoinURLFragments("plugins/"+app.Id, appSubPath) + "/module"
} else {
fp.Module = util.JoinUrlFragments("app/plugins/app/"+app.Id, appSubPath) + "/module"
fp.Module = util.JoinURLFragments("app/plugins/app/"+app.Id, appSubPath) + "/module"
}
}

@ -169,7 +169,7 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
}
if f.Name() == "node_modules" {
return util.WalkSkipDir
return util.ErrWalkSkipDir
}
if f.IsDir() {

@ -1,8 +1,11 @@
package notifiers
import (
"bytes"
"fmt"
"net/url"
"io"
"mime/multipart"
"os"
"strconv"
"github.com/grafana/grafana/pkg/bus"
@ -91,6 +94,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error)
retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString())
expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString())
sound := model.Settings.Get("sound").MustString()
uploadImage := model.Settings.Get("uploadImage").MustBool(true)
if userKey == "" {
return nil, alerting.ValidationError{Reason: "User key not given"}
@ -107,6 +111,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error)
Expire: expire,
Device: device,
Sound: sound,
Upload: uploadImage,
log: log.New("alerting.notifier.pushover"),
}, nil
}
@ -120,6 +125,7 @@ type PushoverNotifier struct {
Expire int
Device string
Sound string
Upload bool
log log.Logger
}
@ -140,38 +146,22 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
if evalContext.Error != nil {
message += fmt.Sprintf("\n<b>Error message:</b> %s", evalContext.Error.Error())
}
if evalContext.ImagePublicUrl != "" {
message += fmt.Sprintf("\n<a href=\"%s\">Show graph image</a>", evalContext.ImagePublicUrl)
}
if message == "" {
message = "Notification message missing (Set a notification message to replace this text.)"
}
q := url.Values{}
q.Add("user", this.UserKey)
q.Add("token", this.ApiToken)
q.Add("priority", strconv.Itoa(this.Priority))
if this.Priority == 2 {
q.Add("retry", strconv.Itoa(this.Retry))
q.Add("expire", strconv.Itoa(this.Expire))
}
if this.Device != "" {
q.Add("device", this.Device)
}
if this.Sound != "default" {
q.Add("sound", this.Sound)
headers, uploadBody, err := this.genPushoverBody(evalContext, message, ruleUrl)
if err != nil {
this.log.Error("Failed to generate body for pushover", "error", err)
return err
}
q.Add("title", evalContext.GetNotificationTitle())
q.Add("url", ruleUrl)
q.Add("url_title", "Show dashboard with alert")
q.Add("message", message)
q.Add("html", "1")
cmd := &m.SendWebhookSync{
Url: PUSHOVER_ENDPOINT,
HttpMethod: "POST",
HttpHeader: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
Body: q.Encode(),
HttpHeader: headers,
Body: uploadBody.String(),
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
@ -181,3 +171,109 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
return nil
}
func (this *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext, message string, ruleUrl string) (map[string]string, bytes.Buffer, error) {
var b bytes.Buffer
var err error
w := multipart.NewWriter(&b)
// Add image only if requested and available
if this.Upload && evalContext.ImageOnDiskPath != "" {
f, err := os.Open(evalContext.ImageOnDiskPath)
if err != nil {
return nil, b, err
}
defer f.Close()
fw, err := w.CreateFormFile("attachment", evalContext.ImageOnDiskPath)
if err != nil {
return nil, b, err
}
_, err = io.Copy(fw, f)
if err != nil {
return nil, b, err
}
}
// Add the user token
err = w.WriteField("user", this.UserKey)
if err != nil {
return nil, b, err
}
// Add the api token
err = w.WriteField("token", this.ApiToken)
if err != nil {
return nil, b, err
}
// Add priority
err = w.WriteField("priority", strconv.Itoa(this.Priority))
if err != nil {
return nil, b, err
}
if this.Priority == 2 {
err = w.WriteField("retry", strconv.Itoa(this.Retry))
if err != nil {
return nil, b, err
}
err = w.WriteField("expire", strconv.Itoa(this.Expire))
if err != nil {
return nil, b, err
}
}
// Add device
if this.Device != "" {
err = w.WriteField("device", this.Device)
if err != nil {
return nil, b, err
}
}
// Add sound
if this.Sound != "default" {
err = w.WriteField("sound", this.Sound)
if err != nil {
return nil, b, err
}
}
// Add title
err = w.WriteField("title", evalContext.GetNotificationTitle())
if err != nil {
return nil, b, err
}
// Add URL
err = w.WriteField("url", ruleUrl)
if err != nil {
return nil, b, err
}
// Add URL title
err = w.WriteField("url_title", "Show dashboard with alert")
if err != nil {
return nil, b, err
}
// Add message
err = w.WriteField("message", message)
if err != nil {
return nil, b, err
}
// Mark as html message
err = w.WriteField("html", "1")
if err != nil {
return nil, b, err
}
w.Close()
headers := map[string]string{
"Content-Type": w.FormDataContentType(),
}
return headers, b, nil
}

@ -151,7 +151,7 @@ func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent
func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) {
hashedToken := hashToken(unhashedToken)
if setting.Env == setting.DEV {
s.log.Info("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
}
expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix()

@ -80,7 +80,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
return nil, models.ErrDashboardFolderNameExists
}
if !util.IsValidShortUid(dash.Uid) {
if !util.IsValidShortUID(dash.Uid) {
return nil, models.ErrDashboardInvalidUid
} else if len(dash.Uid) > 40 {
return nil, models.ErrDashboardUidToLong

@ -92,7 +92,7 @@ func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*not
}
if cmd.Result == nil {
dc.log.Info("Inserting alert notification from configuration ", "name", notification.Name, "uid", notification.Uid)
dc.log.Debug("inserting alert notification from configuration", "name", notification.Name, "uid", notification.Uid)
insertCmd := &models.CreateAlertNotificationCommand{
Uid: notification.Uid,
Name: notification.Name,
@ -109,7 +109,7 @@ func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*not
return err
}
} else {
dc.log.Info("Updating alert notification from configuration", "name", notification.Name)
dc.log.Debug("updating alert notification from configuration", "name", notification.Name)
updateCmd := &models.UpdateAlertNotificationWithUidCommand{
Uid: notification.Uid,
Name: notification.Name,

@ -85,7 +85,7 @@ func GetAlertNotificationsWithUidToSend(query *m.GetAlertNotificationsWithUidToS
var sql bytes.Buffer
params := make([]interface{}, 0)
sql.WriteString(`SELECT
sql.WriteString(`SELECT
alert_notification.id,
alert_notification.uid,
alert_notification.org_id,
@ -276,7 +276,7 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
func generateNewAlertNotificationUid(sess *DBSession, orgId int64) (string, error) {
for i := 0; i < 3; i++ {
uid := util.GenerateShortUid()
uid := util.GenerateShortUID()
exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&m.AlertNotification{})
if err != nil {
return "", err

@ -27,7 +27,7 @@ func init() {
bus.AddHandler("sql", HasEditPermissionInFolders)
}
var generateNewUid func() string = util.GenerateShortUid
var generateNewUid func() string = util.GenerateShortUID
func SaveDashboard(cmd *m.SaveDashboardCommand) error {
return inTransaction(func(sess *DBSession) error {

@ -106,7 +106,7 @@ func TestDashboardDataAccess(t *testing.T) {
if timesCalled <= 2 {
return savedDash.Uid
}
return util.GenerateShortUid()
return util.GenerateShortUID()
}
cmd := m.SaveDashboardCommand{
OrgId: 1,
@ -119,7 +119,7 @@ func TestDashboardDataAccess(t *testing.T) {
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
generateNewUid = util.GenerateShortUid
generateNewUid = util.GenerateShortUID
})
Convey("Should be able to create dashboard", func() {

@ -196,6 +196,23 @@ func (ss *SqlStore) ensureAdminUser() error {
return err
}
func (ss *SqlStore) buildExtraConnectionString(sep rune) string {
if ss.dbCfg.UrlQueryParams == nil {
return ""
}
var sb strings.Builder
for key, values := range ss.dbCfg.UrlQueryParams {
for _, value := range values {
sb.WriteRune(sep)
sb.WriteString(key)
sb.WriteRune('=')
sb.WriteString(value)
}
}
return sb.String()
}
func (ss *SqlStore) buildConnectionString() (string, error) {
cnnstr := ss.dbCfg.ConnectionString
@ -222,8 +239,10 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
mysql.RegisterTLSConfig("custom", tlsCert)
cnnstr += "&tls=custom"
}
cnnstr += ss.buildExtraConnectionString('&')
case migrator.POSTGRES:
host, port, err := util.SplitIpPort(ss.dbCfg.Host, "5432")
host, port, err := util.SplitIPPort(ss.dbCfg.Host, "5432")
if err != nil {
return "", err
}
@ -234,6 +253,8 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
ss.dbCfg.User = "''"
}
cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", ss.dbCfg.User, ss.dbCfg.Pwd, host, port, ss.dbCfg.Name, ss.dbCfg.SslMode, ss.dbCfg.ClientCertPath, ss.dbCfg.ClientKeyPath, ss.dbCfg.CaCertPath)
cnnstr += ss.buildExtraConnectionString(' ')
case migrator.SQLITE:
// special case for tests
if !filepath.IsAbs(ss.dbCfg.Path) {
@ -241,6 +262,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) {
}
os.MkdirAll(path.Dir(ss.dbCfg.Path), os.ModePerm)
cnnstr = fmt.Sprintf("file:%s?cache=%s&mode=rwc", ss.dbCfg.Path, ss.dbCfg.CacheMode)
cnnstr += ss.buildExtraConnectionString('&')
default:
return "", fmt.Errorf("Unknown database type: %s", ss.dbCfg.Type)
}
@ -297,6 +319,8 @@ func (ss *SqlStore) readConfig() {
ss.dbCfg.User = userInfo.Username()
ss.dbCfg.Pwd, _ = userInfo.Password()
}
ss.dbCfg.UrlQueryParams = dbURL.Query()
} else {
ss.dbCfg.Type = sec.Key("type").String()
ss.dbCfg.Host = sec.Key("host").String()
@ -406,4 +430,5 @@ type DatabaseConfig struct {
MaxIdleConn int
ConnMaxLifetime int
CacheMode string
UrlQueryParams map[string][]string
}

@ -49,7 +49,7 @@ func generateConnectionString(datasource *models.DataSource) (string, error) {
}
}
server, port, err := util.SplitIpPort(datasource.Url, "1433")
server, port, err := util.SplitIPPort(datasource.Url, "1433")
if err != nil {
return "", err
}

@ -233,12 +233,12 @@ func buildFilterString(metricType string, filterParts []interface{}) string {
}
func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) {
primaryAggregation := query.Model.Get("primaryAggregation").MustString()
crossSeriesReducer := query.Model.Get("crossSeriesReducer").MustString()
perSeriesAligner := query.Model.Get("perSeriesAligner").MustString()
alignmentPeriod := query.Model.Get("alignmentPeriod").MustString()
if primaryAggregation == "" {
primaryAggregation = "REDUCE_NONE"
if crossSeriesReducer == "" {
crossSeriesReducer = "REDUCE_NONE"
}
if perSeriesAligner == "" {
@ -267,7 +267,7 @@ func setAggParams(params *url.Values, query *tsdb.Query, durationSeconds int) {
alignmentPeriod = "+3600s"
}
params.Add("aggregation.crossSeriesReducer", primaryAggregation)
params.Add("aggregation.crossSeriesReducer", crossSeriesReducer)
params.Add("aggregation.perSeriesAligner", perSeriesAligner)
params.Add("aggregation.alignmentPeriod", alignmentPeriod)

@ -173,7 +173,7 @@ func TestStackdriver(t *testing.T) {
Convey("and query has aggregation mean set", func() {
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
"metricType": "a/metric/type",
"primaryAggregation": "REDUCE_MEAN",
"crossSeriesReducer": "REDUCE_SUM",
"view": "FULL",
})
@ -182,11 +182,11 @@ func TestStackdriver(t *testing.T) {
So(len(queries), ShouldEqual, 1)
So(queries[0].RefID, ShouldEqual, "A")
So(queries[0].Target, ShouldEqual, "aggregation.alignmentPeriod=%2B60s&aggregation.crossSeriesReducer=REDUCE_MEAN&aggregation.perSeriesAligner=ALIGN_MEAN&filter=metric.type%3D%22a%2Fmetric%2Ftype%22&interval.endTime=2018-03-15T13%3A34%3A00Z&interval.startTime=2018-03-15T13%3A00%3A00Z&view=FULL")
So(queries[0].Target, ShouldEqual, "aggregation.alignmentPeriod=%2B60s&aggregation.crossSeriesReducer=REDUCE_SUM&aggregation.perSeriesAligner=ALIGN_MEAN&filter=metric.type%3D%22a%2Fmetric%2Ftype%22&interval.endTime=2018-03-15T13%3A34%3A00Z&interval.startTime=2018-03-15T13%3A00%3A00Z&view=FULL")
So(len(queries[0].Params), ShouldEqual, 7)
So(queries[0].Params["interval.startTime"][0], ShouldEqual, "2018-03-15T13:00:00Z")
So(queries[0].Params["interval.endTime"][0], ShouldEqual, "2018-03-15T13:34:00Z")
So(queries[0].Params["aggregation.crossSeriesReducer"][0], ShouldEqual, "REDUCE_MEAN")
So(queries[0].Params["aggregation.crossSeriesReducer"][0], ShouldEqual, "REDUCE_SUM")
So(queries[0].Params["aggregation.perSeriesAligner"][0], ShouldEqual, "ALIGN_MEAN")
So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, "+60s")
So(queries[0].Params["filter"][0], ShouldEqual, "metric.type=\"a/metric/type\"")
@ -196,7 +196,7 @@ func TestStackdriver(t *testing.T) {
Convey("and query has group bys", func() {
tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
"metricType": "a/metric/type",
"primaryAggregation": "REDUCE_NONE",
"crossSeriesReducer": "REDUCE_NONE",
"groupBys": []interface{}{"metric.label.group1", "metric.label.group2"},
"view": "FULL",
})

@ -12,6 +12,7 @@ import (
"strings"
)
// GetRandomString generate random string by specify chars.
// source: https://github.com/gogits/gogs/blob/9ee80e3e5426821f03a4e99fad34418f5c736413/modules/base/tool.go#L58
func GetRandomString(n int, alphabets ...byte) string {
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
@ -27,18 +28,21 @@ func GetRandomString(n int, alphabets ...byte) string {
return string(bytes)
}
// EncodePassword encodes a password using PBKDF2.
func EncodePassword(password string, salt string) string {
newPasswd := PBKDF2([]byte(password), []byte(salt), 10000, 50, sha256.New)
return hex.EncodeToString(newPasswd)
}
// Encode string to md5 hex value.
// EncodeMd5 encodes a string to md5 hex value.
func EncodeMd5(str string) string {
m := md5.New()
m.Write([]byte(str))
return hex.EncodeToString(m.Sum(nil))
}
// PBKDF2 implements Password-Based Key Derivation Function 2), aimed to reduce
// the vulnerability of encrypted keys to brute force attacks.
// http://code.google.com/p/go/source/browse/pbkdf2/pbkdf2.go?repo=crypto
func PBKDF2(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
prf := hmac.New(h, password)
@ -77,11 +81,13 @@ func PBKDF2(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte
return dk[:keyLen]
}
// GetBasicAuthHeader returns a base64 encoded string from user and password.
func GetBasicAuthHeader(user string, password string) string {
var userAndPass = user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userAndPass))
}
// DecodeBasicAuthHeader decodes user and password from a basic auth header.
func DecodeBasicAuthHeader(header string) (string, string, error) {
var code string
parts := strings.SplitN(header, " ", 2)
@ -102,6 +108,7 @@ func DecodeBasicAuthHeader(header string) (string, string, error) {
return userAndPass[0], userAndPass[1], nil
}
// RandomHex returns a random string from a n seed.
func RandomHex(n int) (string, error) {
bytes := make([]byte, n)
if _, err := rand.Read(bytes); err != nil {

@ -11,6 +11,7 @@ import (
const saltLength = 8
// Decrypt decrypts a payload with a given secret.
func Decrypt(payload []byte, secret string) ([]byte, error) {
salt := payload[:saltLength]
key := encryptionKeyToBytes(secret, string(salt))
@ -36,6 +37,7 @@ func Decrypt(payload []byte, secret string) ([]byte, error) {
return payloadDst, nil
}
// Encrypt encrypts a payload with a given secret.
func Encrypt(payload []byte, secret string) ([]byte, error) {
salt := GetRandomString(saltLength)

@ -8,8 +8,8 @@ import (
"path/filepath"
)
//WalkSkipDir is the Error returned when we want to skip descending into a directory
var WalkSkipDir = errors.New("skip this directory")
//ErrWalkSkipDir is the Error returned when we want to skip descending into a directory
var ErrWalkSkipDir = errors.New("skip this directory")
//WalkFunc is a callback function called for each path as a directory is walked
//If resolvedPath != "", then we are following symbolic links.
@ -50,7 +50,7 @@ func walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollow
}
err := walkFn(resolvedPath, info, nil)
if err != nil {
if info.IsDir() && err == WalkSkipDir {
if info.IsDir() && err == ErrWalkSkipDir {
err = nil
}
return err

@ -4,7 +4,8 @@ import (
"net"
)
func SplitIpPort(ipStr string, portDefault string) (ip string, port string, err error) {
// SplitIPPort splits the ip string and port.
func SplitIPPort(ipStr string, portDefault string) (ip string, port string, err error) {
ipAddr := net.ParseIP(ipStr)
if ipAddr == nil {

@ -6,10 +6,10 @@ import (
. "github.com/smartystreets/goconvey/convey"
)
func TestSplitIpPort(t *testing.T) {
func TestSplitIPPort(t *testing.T) {
Convey("When parsing an IPv4 without explicit port", t, func() {
ip, port, err := SplitIpPort("1.2.3.4", "5678")
ip, port, err := SplitIPPort("1.2.3.4", "5678")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "1.2.3.4")
@ -17,7 +17,7 @@ func TestSplitIpPort(t *testing.T) {
})
Convey("When parsing an IPv6 without explicit port", t, func() {
ip, port, err := SplitIpPort("::1", "5678")
ip, port, err := SplitIPPort("::1", "5678")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "::1")
@ -25,7 +25,7 @@ func TestSplitIpPort(t *testing.T) {
})
Convey("When parsing an IPv4 with explicit port", t, func() {
ip, port, err := SplitIpPort("1.2.3.4:56", "78")
ip, port, err := SplitIPPort("1.2.3.4:56", "78")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "1.2.3.4")
@ -33,7 +33,7 @@ func TestSplitIpPort(t *testing.T) {
})
Convey("When parsing an IPv6 with explicit port", t, func() {
ip, port, err := SplitIpPort("[::1]:56", "78")
ip, port, err := SplitIPPort("[::1]:56", "78")
So(err, ShouldEqual, nil)
So(ip, ShouldEqual, "::1")

@ -1,3 +1,4 @@
package util
// DynMap defines a dynamic map interface.
type DynMap map[string]interface{}

@ -19,7 +19,7 @@ func Md5Sum(reader io.Reader) (string, error) {
return returnMD5String, nil
}
// Md5Sum calculates the md5sum of a string
// Md5SumString calculates the md5sum of a string
func Md5SumString(input string) (string, error) {
buffer := strings.NewReader(input)
return Md5Sum(buffer)

@ -8,19 +8,19 @@ import (
var allowedChars = shortid.DefaultABC
var validUidPattern = regexp.MustCompile(`^[a-zA-Z0-9\-\_]*$`).MatchString
var validUIDPattern = regexp.MustCompile(`^[a-zA-Z0-9\-\_]*$`).MatchString
func init() {
gen, _ := shortid.New(1, allowedChars, 1)
shortid.SetDefault(gen)
}
// IsValidShortUid checks if short unique identifier contains valid characters
func IsValidShortUid(uid string) bool {
return validUidPattern(uid)
// IsValidShortUID checks if short unique identifier contains valid characters
func IsValidShortUID(uid string) bool {
return validUIDPattern(uid)
}
// GenerateShortUid generates a short unique identifier.
func GenerateShortUid() string {
// GenerateShortUID generates a short unique identifier.
func GenerateShortUID() string {
return shortid.MustGenerate()
}

@ -4,7 +4,7 @@ import "testing"
func TestAllowedCharMatchesUidPattern(t *testing.T) {
for _, c := range allowedChars {
if !IsValidShortUid(string(c)) {
if !IsValidShortUID(string(c)) {
t.Fatalf("charset for creating new shortids contains chars not present in uid pattern")
}
}

@ -7,10 +7,12 @@ import (
"time"
)
// StringsFallback2 returns the first of two not empty strings.
func StringsFallback2(val1 string, val2 string) string {
return stringsFallback(val1, val2)
}
// StringsFallback3 returns the first of three not empty strings.
func StringsFallback3(val1 string, val2 string, val3 string) string {
return stringsFallback(val1, val2, val3)
}
@ -24,6 +26,7 @@ func stringsFallback(vals ...string) string {
return ""
}
// SplitString splits a string by commas or empty spaces.
func SplitString(str string) []string {
if len(str) == 0 {
return []string{}
@ -32,6 +35,7 @@ func SplitString(str string) []string {
return regexp.MustCompile("[, ]+").Split(str, -1)
}
// GetAgeString returns a string representing certain time from years to minutes.
func GetAgeString(t time.Time) string {
if t.IsZero() {
return "?"

@ -5,22 +5,26 @@ import (
"strings"
)
type UrlQueryReader struct {
// URLQueryReader is a URL query type.
type URLQueryReader struct {
values url.Values
}
func NewUrlQueryReader(urlInfo *url.URL) (*UrlQueryReader, error) {
// NewURLQueryReader parses a raw query and returns it as a URLQueryReader type.
func NewURLQueryReader(urlInfo *url.URL) (*URLQueryReader, error) {
u, err := url.ParseQuery(urlInfo.RawQuery)
if err != nil {
return nil, err
}
return &UrlQueryReader{
return &URLQueryReader{
values: u,
}, nil
}
func (r *UrlQueryReader) Get(name string, def string) string {
// Get parse parameters from an URL. If the parameter does not exist, it returns
// the default value.
func (r *URLQueryReader) Get(name string, def string) string {
val := r.values[name]
if len(val) == 0 {
return def
@ -29,7 +33,8 @@ func (r *UrlQueryReader) Get(name string, def string) string {
return val[0]
}
func JoinUrlFragments(a, b string) string {
// JoinURLFragments joins two URL fragments into only one URL string.
func JoinURLFragments(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")

@ -1,60 +1,60 @@
package util
import (
"net/url"
"testing"
. "github.com/smartystreets/goconvey/convey"
"net/url"
)
func TestUrl(t *testing.T) {
Convey("When joining two urls where right hand side is empty", t, func() {
result := JoinUrlFragments("http://localhost:8080", "")
result := JoinURLFragments("http://localhost:8080", "")
So(result, ShouldEqual, "http://localhost:8080")
})
Convey("When joining two urls where right hand side is empty and lefthand side has a trailing slash", t, func() {
result := JoinUrlFragments("http://localhost:8080/", "")
result := JoinURLFragments("http://localhost:8080/", "")
So(result, ShouldEqual, "http://localhost:8080/")
})
Convey("When joining two urls where neither has a trailing slash", t, func() {
result := JoinUrlFragments("http://localhost:8080", "api")
result := JoinURLFragments("http://localhost:8080", "api")
So(result, ShouldEqual, "http://localhost:8080/api")
})
Convey("When joining two urls where lefthand side has a trailing slash", t, func() {
result := JoinUrlFragments("http://localhost:8080/", "api")
result := JoinURLFragments("http://localhost:8080/", "api")
So(result, ShouldEqual, "http://localhost:8080/api")
})
Convey("When joining two urls where righthand side has preceding slash", t, func() {
result := JoinUrlFragments("http://localhost:8080", "/api")
result := JoinURLFragments("http://localhost:8080", "/api")
So(result, ShouldEqual, "http://localhost:8080/api")
})
Convey("When joining two urls where righthand side has trailing slash", t, func() {
result := JoinUrlFragments("http://localhost:8080", "api/")
result := JoinURLFragments("http://localhost:8080", "api/")
So(result, ShouldEqual, "http://localhost:8080/api/")
})
Convey("When joining two urls where lefthand side has a trailing slash and righthand side has preceding slash", t, func() {
result := JoinUrlFragments("http://localhost:8080/", "/api/")
result := JoinURLFragments("http://localhost:8080/", "/api/")
So(result, ShouldEqual, "http://localhost:8080/api/")
})
}
func TestNewUrlQueryReader(t *testing.T) {
func TestNewURLQueryReader(t *testing.T) {
u, _ := url.Parse("http://www.abc.com/foo?bar=baz&bar2=baz2")
uqr, _ := NewUrlQueryReader(u)
uqr, _ := NewURLQueryReader(u)
Convey("when trying to retrieve the first query value", t, func() {
result := uqr.Get("bar", "foodef")

@ -13,6 +13,7 @@ var (
regexEmail = regexp.MustCompile(emailRegexPattern)
)
// IsEmail checks if a string is a valid email address.
func IsEmail(str string) bool {
return regexEmail.MatchString(strings.ToLower(str))
}

@ -9,9 +9,9 @@ import PageHeader from '../PageHeader/PageHeader';
import Footer from '../Footer/Footer';
import PageContents from './PageContents';
import { CustomScrollbar } from '@grafana/ui';
import { isEqual } from 'lodash';
interface Props {
title?: string;
children: JSX.Element[] | JSX.Element;
navModel: NavModel;
}
@ -28,7 +28,7 @@ class Page extends Component<Props> {
}
componentDidUpdate(prevProps: Props) {
if (prevProps.title !== this.props.title) {
if (!isEqual(prevProps.navModel, this.props.navModel)) {
this.updateTitle();
}
}

@ -80,7 +80,7 @@ const Navigation = ({ main }: { main: NavModelItem }) => {
};
export default class PageHeader extends React.Component<Props, any> {
constructor(props) {
constructor(props: Props) {
super(props);
}

@ -4,7 +4,7 @@ interface Props {
pageName?: string;
}
const PageLoader: FC<Props> = ({ pageName }) => {
const PageLoader: FC<Props> = ({ pageName = '' }) => {
const loadingText = `Loading ${pageName}...`;
return (
<div className="page-loader-wrapper">

@ -1,106 +1,20 @@
import $ from 'jquery';
import angular from 'angular';
export class Profiler {
panelsRendered: number;
enabled: boolean;
panelsInitCount: any;
timings: any;
digestCounter: any;
$rootScope: any;
scopeCount: any;
window: any;
init(config, $rootScope) {
this.enabled = config.buildInfo.env === 'development';
this.timings = {};
this.timings.appStart = { loadStart: new Date().getTime() };
this.$rootScope = $rootScope;
this.window = window;
if (!this.enabled) {
return;
}
$rootScope.$watch(
() => {
this.digestCounter++;
return false;
},
() => {}
);
$rootScope.onAppEvent('refresh', this.refresh.bind(this), $rootScope);
$rootScope.onAppEvent('dashboard-fetch-end', this.dashboardFetched.bind(this), $rootScope);
$rootScope.onAppEvent('dashboard-initialized', this.dashboardInitialized.bind(this), $rootScope);
$rootScope.onAppEvent('panel-initialized', this.panelInitialized.bind(this), $rootScope);
}
refresh() {
this.timings.query = 0;
this.timings.render = 0;
setTimeout(() => {
console.log('panel count: ' + this.panelsInitCount);
console.log('total query: ' + this.timings.query);
console.log('total render: ' + this.timings.render);
console.log('avg render: ' + this.timings.render / this.panelsInitCount);
}, 5000);
}
dashboardFetched() {
this.timings.dashboardLoadStart = new Date().getTime();
this.panelsInitCount = 0;
this.digestCounter = 0;
this.panelsInitCount = 0;
this.panelsRendered = 0;
this.timings.query = 0;
this.timings.render = 0;
}
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);
const timeTaken = this.timings.lastPanelInitializedAt - this.timings.dashboardLoadStart;
console.log('Dashboard::Performance All panels initialized in ' + timeTaken + ' ms');
// measure digest performance
const rootDigestStart = window.performance.now();
for (let i = 0; i < 30; i++) {
this.$rootScope.$apply();
}
console.log('Dashboard::Performance Root Digest ' + (window.performance.now() - rootDigestStart) / 30);
}, 3000);
}
getTotalWatcherCount() {
let count = 0;
let scopes = 0;
const root = $(document.getElementsByTagName('body'));
const f = element => {
if (element.data().hasOwnProperty('$scope')) {
scopes++;
angular.forEach(element.data().$scope.$$watchers, () => {
count++;
});
}
angular.forEach(element.children(), childElement => {
f($(childElement));
});
};
f(root);
this.scopeCount = scopes;
return count;
}
renderingCompleted(panelId, panelTimings) {
renderingCompleted(panelId) {
// add render counter to root scope
// used by phantomjs render.js to know when panel has rendered
this.panelsRendered = (this.panelsRendered || 0) + 1;
@ -108,21 +22,6 @@ export class Profiler {
// this window variable is used by backend rendering tools to know
// all panels have completed rendering
this.window.panelsRendered = this.panelsRendered;
if (this.enabled) {
panelTimings.renderEnd = new Date().getTime();
this.timings.query += panelTimings.queryEnd - panelTimings.queryStart;
this.timings.render += panelTimings.renderEnd - panelTimings.renderStart;
}
}
panelInitialized() {
if (!this.enabled) {
return;
}
this.panelsInitCount++;
this.timings.lastPanelInitializedAt = new Date().getTime();
}
}

@ -0,0 +1,83 @@
import {
actionCreatorFactory,
resetAllActionCreatorTypes,
noPayloadActionCreatorFactory,
} from './actionCreatorFactory';
interface Dummy {
n: number;
s: string;
o: {
n: number;
s: string;
b: boolean;
};
b: boolean;
}
const setup = (payload?: Dummy) => {
resetAllActionCreatorTypes();
const actionCreator = actionCreatorFactory<Dummy>('dummy').create();
const noPayloadactionCreator = noPayloadActionCreatorFactory('NoPayload').create();
const result = actionCreator(payload);
const noPayloadResult = noPayloadactionCreator();
return { actionCreator, noPayloadactionCreator, result, noPayloadResult };
};
describe('actionCreatorFactory', () => {
describe('when calling create', () => {
it('then it should create correct type string', () => {
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } };
const { actionCreator, result } = setup(payload);
expect(actionCreator.type).toEqual('dummy');
expect(result.type).toEqual('dummy');
});
it('then it should create correct payload', () => {
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } };
const { result } = setup(payload);
expect(result.payload).toEqual(payload);
});
});
describe('when calling create with existing type', () => {
it('then it should throw error', () => {
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } };
setup(payload);
expect(() => {
noPayloadActionCreatorFactory('DuMmY').create();
}).toThrow();
});
});
});
describe('noPayloadActionCreatorFactory', () => {
describe('when calling create', () => {
it('then it should create correct type string', () => {
const { noPayloadResult, noPayloadactionCreator } = setup();
expect(noPayloadactionCreator.type).toEqual('NoPayload');
expect(noPayloadResult.type).toEqual('NoPayload');
});
it('then it should create correct payload', () => {
const { noPayloadResult } = setup();
expect(noPayloadResult.payload).toBeUndefined();
});
});
describe('when calling create with existing type', () => {
it('then it should throw error', () => {
setup();
expect(() => {
actionCreatorFactory<Dummy>('nOpAyLoAd').create();
}).toThrow();
});
});
});

@ -0,0 +1,57 @@
import { Action } from 'redux';
const allActionCreators: string[] = [];
export interface ActionOf<Payload> extends Action {
readonly type: string;
readonly payload: Payload;
}
export interface ActionCreator<Payload> {
readonly type: string;
(payload: Payload): ActionOf<Payload>;
}
export interface NoPayloadActionCreator {
readonly type: string;
(): ActionOf<undefined>;
}
export interface ActionCreatorFactory<Payload> {
create: () => ActionCreator<Payload>;
}
export interface NoPayloadActionCreatorFactory {
create: () => NoPayloadActionCreator;
}
export const actionCreatorFactory = <Payload>(type: string): ActionCreatorFactory<Payload> => {
const create = (): ActionCreator<Payload> => {
return Object.assign((payload: Payload): ActionOf<Payload> => ({ type, payload }), { type });
};
if (allActionCreators.some(t => (t && type ? t.toLocaleUpperCase() === type.toLocaleUpperCase() : false))) {
throw new Error(`There is already an actionCreator defined with the type ${type}`);
}
allActionCreators.push(type);
return { create };
};
export const noPayloadActionCreatorFactory = (type: string): NoPayloadActionCreatorFactory => {
const create = (): NoPayloadActionCreator => {
return Object.assign((): ActionOf<undefined> => ({ type, payload: undefined }), { type });
};
if (allActionCreators.some(t => (t && type ? t.toLocaleUpperCase() === type.toLocaleUpperCase() : false))) {
throw new Error(`There is already an actionCreator defined with the type ${type}`);
}
allActionCreators.push(type);
return { create };
};
// Should only be used by tests
export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0);

@ -0,0 +1,4 @@
import { actionCreatorFactory } from './actionCreatorFactory';
import { reducerFactory } from './reducerFactory';
export { actionCreatorFactory, reducerFactory };

@ -0,0 +1,97 @@
import { reducerFactory } from './reducerFactory';
import { actionCreatorFactory, ActionOf } from './actionCreatorFactory';
interface DummyReducerState {
n: number;
s: string;
b: boolean;
o: {
n: number;
s: string;
b: boolean;
};
}
const dummyReducerIntialState: DummyReducerState = {
n: 1,
s: 'One',
b: true,
o: {
n: 2,
s: 'two',
b: false,
},
};
const dummyActionCreator = actionCreatorFactory<DummyReducerState>('dummy').create();
const dummyReducer = reducerFactory(dummyReducerIntialState)
.addMapper({
filter: dummyActionCreator,
mapper: (state, action) => ({ ...state, ...action.payload }),
})
.create();
describe('reducerFactory', () => {
describe('given it is created with a defined handler', () => {
describe('when reducer is called with no state', () => {
describe('and with an action that the handler can not handle', () => {
it('then the resulting state should be intial state', () => {
const result = dummyReducer(undefined as DummyReducerState, {} as ActionOf<any>);
expect(result).toEqual(dummyReducerIntialState);
});
});
describe('and with an action that the handler can handle', () => {
it('then the resulting state should correct', () => {
const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } };
const result = dummyReducer(undefined as DummyReducerState, dummyActionCreator(payload));
expect(result).toEqual(payload);
});
});
});
describe('when reducer is called with a state', () => {
describe('and with an action that the handler can not handle', () => {
it('then the resulting state should be intial state', () => {
const result = dummyReducer(dummyReducerIntialState, {} as ActionOf<any>);
expect(result).toEqual(dummyReducerIntialState);
});
});
describe('and with an action that the handler can handle', () => {
it('then the resulting state should correct', () => {
const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } };
const result = dummyReducer(dummyReducerIntialState, dummyActionCreator(payload));
expect(result).toEqual(payload);
});
});
});
});
describe('given a handler is added', () => {
describe('when a handler with the same creator is added', () => {
it('then is should throw', () => {
const faultyReducer = reducerFactory(dummyReducerIntialState).addMapper({
filter: dummyActionCreator,
mapper: (state, action) => {
return { ...state, ...action.payload };
},
});
expect(() => {
faultyReducer.addMapper({
filter: dummyActionCreator,
mapper: state => {
return state;
},
});
}).toThrow();
});
});
});
});

@ -0,0 +1,45 @@
import { ActionOf, ActionCreator } from './actionCreatorFactory';
import { Reducer } from 'redux';
export type Mapper<State, Payload> = (state: State, action: ActionOf<Payload>) => State;
export interface MapperConfig<State, Payload> {
filter: ActionCreator<Payload>;
mapper: Mapper<State, Payload>;
}
export interface AddMapper<State> {
addMapper: <Payload>(config: MapperConfig<State, Payload>) => CreateReducer<State>;
}
export interface CreateReducer<State> extends AddMapper<State> {
create: () => Reducer<State, ActionOf<any>>;
}
export const reducerFactory = <State>(initialState: State): AddMapper<State> => {
const allMappers: { [key: string]: Mapper<State, any> } = {};
const addMapper = <Payload>(config: MapperConfig<State, Payload>): CreateReducer<State> => {
if (allMappers[config.filter.type]) {
throw new Error(`There is already a mapper defined with the type ${config.filter.type}`);
}
allMappers[config.filter.type] = config.mapper;
return instance;
};
const create = (): Reducer<State, ActionOf<any>> => (state: State = initialState, action: ActionOf<any>): State => {
const mapper = allMappers[action.type];
if (mapper) {
return mapper(state, action);
}
return state;
};
const instance: CreateReducer<State> = { addMapper, create };
return instance;
};

@ -1,7 +1,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';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
export class BackendSrv {
private inFlightRequests = {};

@ -1,6 +1,7 @@
import kbn from 'app/core/utils/kbn';
import { getFlotTickDecimals } from 'app/core/utils/ticks';
import _ from 'lodash';
import { getValueFormat } from '@grafana/ui';
function matchSeriesOverride(aliasOrRegex, seriesAlias) {
if (!aliasOrRegex) {
@ -31,13 +32,13 @@ export function updateLegendValues(data: TimeSeries[], panel, height) {
const yaxes = panel.yaxes;
const seriesYAxis = series.yaxis || 1;
const axis = yaxes[seriesYAxis - 1];
const formater = kbn.valueFormats[axis.format];
const formatter = getValueFormat(axis.format);
// decimal override
if (_.isNumber(panel.decimals)) {
series.updateLegendValues(formater, panel.decimals, null);
series.updateLegendValues(formatter, panel.decimals, null);
} else if (_.isNumber(axis.decimals)) {
series.updateLegendValues(formater, axis.decimals + 1, null);
series.updateLegendValues(formatter, axis.decimals + 1, null);
} else {
// auto decimals
// legend and tooltip gets one more decimal precision
@ -45,7 +46,7 @@ export function updateLegendValues(data: TimeSeries[], panel, height) {
const { datamin, datamax } = getDataMinMax(data);
const { tickDecimals, scaledDecimals } = getFlotTickDecimals(datamin, datamax, axis, height);
const tickDecimalsPlusOne = (tickDecimals || -1) + 1;
series.updateLegendValues(formater, tickDecimalsPlusOne, scaledDecimals + 2);
series.updateLegendValues(formatter, tickDecimalsPlusOne, scaledDecimals + 2);
}
}
}
@ -105,7 +106,7 @@ export default class TimeSeries {
this.aliasEscaped = _.escape(opts.alias);
this.color = opts.color;
this.bars = { fillColor: opts.color };
this.valueFormater = kbn.valueFormats.none;
this.valueFormater = getValueFormat('none');
this.stats = {};
this.legend = true;
this.unit = opts.unit;

@ -0,0 +1,22 @@
// https://github.com/facebook/react/issues/5465
export interface CancelablePromise<T> {
promise: Promise<T>;
cancel: () => void;
}
export const makePromiseCancelable = <T>(promise: Promise<T>): CancelablePromise<T> => {
let hasCanceled_ = false;
const wrappedPromise = new Promise<T>((resolve, reject) => {
promise.then(val => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)));
promise.catch(error => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error)));
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};

@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { NavModel, StoreState } from 'app/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { getServerStats, ServerStat } from './state/apis';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import Page from 'app/core/components/Page/Page';
interface Props {
navModel: NavModel;
@ -13,21 +13,24 @@ interface Props {
interface State {
stats: ServerStat[];
isLoading: boolean;
}
export class ServerStats extends PureComponent<Props, State> {
constructor(props) {
constructor(props: Props) {
super(props);
this.state = {
stats: [],
isLoading: false
};
}
async componentDidMount() {
try {
this.setState({ isLoading: true });
const stats = await this.props.getServerStats();
this.setState({ stats });
this.setState({ stats, isLoading: false });
} catch (error) {
console.error(error);
}
@ -35,12 +38,11 @@ export class ServerStats extends PureComponent<Props, State> {
render() {
const { navModel } = this.props;
const { stats } = this.state;
const { stats, isLoading } = this.state;
return (
<div>
<PageHeader model={navModel} />
<div className="page-container page-body">
<Page navModel={navModel}>
<Page.Contents isLoading={isLoading}>
<table className="filter-table form-inline">
<thead>
<tr>
@ -50,8 +52,8 @@ export class ServerStats extends PureComponent<Props, State> {
</thead>
<tbody>{stats.map(StatItem)}</tbody>
</table>
</div>
</div>
</Page.Contents>
</Page>
);
}
}

@ -1,118 +1,258 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ServerStats Should render table with stats 1`] = `
<div>
<div
className="page-scrollbar-wrapper"
>
<div
className="page-header-canvas"
className="custom-scrollbars"
style={
Object {
"height": "auto",
"maxHeight": "100%",
"minHeight": "100%",
"overflow": "hidden",
"position": "relative",
"width": "100%",
}
}
>
<div
className="page-container"
className="view"
style={
Object {
"WebkitOverflowScrolling": "touch",
"bottom": undefined,
"left": undefined,
"marginBottom": 0,
"marginRight": 0,
"maxHeight": "calc(100% + 0px)",
"minHeight": "calc(100% + 0px)",
"overflow": "scroll",
"position": "relative",
"right": undefined,
"top": undefined,
}
}
>
<div
className="page-header"
className="page-scrollbar-content"
>
<div
className="page-header__inner"
className="page-header-canvas"
>
<span
className="page-header__logo"
>
<i
className="page-header__icon fa fa-fw fa-warning"
/>
</span>
<div
className="page-header__info-block"
className="page-container"
>
<h1
className="page-header__title"
>
Admin
</h1>
<div
className="page-header__sub-title"
className="page-header"
>
subTitle
<div
className="page-header__inner"
>
<span
className="page-header__logo"
>
<i
className="page-header__icon fa fa-fw fa-warning"
/>
</span>
<div
className="page-header__info-block"
>
<h1
className="page-header__title"
>
Admin
</h1>
<div
className="page-header__sub-title"
>
subTitle
</div>
</div>
</div>
<nav>
<div
className="gf-form-select-wrapper width-20 page-header__select-nav"
>
<label
className="gf-form-select-icon icon"
htmlFor="page-header-select-nav"
/>
<select
className="gf-select-nav gf-form-input"
id="page-header-select-nav"
onChange={[Function]}
value="Admin"
>
<option
value="Admin"
>
Admin
</option>
</select>
</div>
<ul
className="gf-tabs page-header__tabs"
>
<li
className="gf-tabs-item"
>
<a
className="gf-tabs-link active"
href="Admin"
>
<i
className="icon"
/>
Admin
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<nav>
<div
className="page-container page-body"
>
<table
className="filter-table form-inline"
>
<thead>
<tr>
<th>
Name
</th>
<th>
Value
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Total dashboards
</td>
<td>
10
</td>
</tr>
<tr>
<td>
Total Users
</td>
<td>
1
</td>
</tr>
</tbody>
</table>
</div>
<footer
className="footer"
>
<div
className="gf-form-select-wrapper width-20 page-header__select-nav"
className="text-center"
>
<label
className="gf-form-select-icon icon"
htmlFor="page-header-select-nav"
/>
<select
className="gf-select-nav gf-form-input"
id="page-header-select-nav"
onChange={[Function]}
value="Admin"
>
<option
value="Admin"
>
Admin
</option>
</select>
<ul>
<li>
<a
href="http://docs.grafana.org"
target="_blank"
>
<i
className="fa fa-file-code-o"
/>
Docs
</a>
</li>
<li>
<a
href="https://grafana.com/services/support"
target="_blank"
>
<i
className="fa fa-support"
/>
Support Plans
</a>
</li>
<li>
<a
href="https://community.grafana.com/"
target="_blank"
>
<i
className="fa fa-comments-o"
/>
Community
</a>
</li>
<li>
<a
href="https://grafana.com"
target="_blank"
>
Grafana
</a>
<span>
v
v1.0
(commit:
1
)
</span>
</li>
</ul>
</div>
<ul
className="gf-tabs page-header__tabs"
>
<li
className="gf-tabs-item"
>
<a
className="gf-tabs-link active"
href="Admin"
>
<i
className="icon"
/>
Admin
</a>
</li>
</ul>
</nav>
</footer>
</div>
</div>
</div>
<div
className="page-container page-body"
>
<table
className="filter-table form-inline"
<div
className="track-horizontal"
style={
Object {
"display": "none",
"height": 6,
"position": "absolute",
}
}
>
<thead>
<tr>
<th>
Name
</th>
<th>
Value
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Total dashboards
</td>
<td>
10
</td>
</tr>
<tr>
<td>
Total Users
</td>
<td>
1
</td>
</tr>
</tbody>
</table>
<div
className="thumb-horizontal"
style={
Object {
"display": "block",
"height": "100%",
"position": "relative",
}
}
/>
</div>
<div
className="track-vertical"
style={
Object {
"display": "none",
"position": "absolute",
"width": 6,
}
}
>
<div
className="thumb-vertical"
style={
Object {
"display": "block",
"position": "relative",
"width": "100%",
}
}
/>
</div>
</div>
</div>
`;

@ -18,6 +18,7 @@ const setup = (propOverrides?: object) => {
togglePauseAlertRule: jest.fn(),
stateFilter: '',
search: '',
isLoading: false
};
Object.assign(props, propOverrides);
@ -121,7 +122,7 @@ describe('Functions', () => {
describe('State filter changed', () => {
it('should update location', () => {
const { instance } = setup();
const mockEvent = { target: { value: 'alerting' } };
const mockEvent = { target: { value: 'alerting' } } as React.ChangeEvent<HTMLSelectElement>;
instance.onStateFilterChanged(mockEvent);
@ -146,7 +147,7 @@ describe('Functions', () => {
describe('Search query change', () => {
it('should set search query', () => {
const { instance } = setup();
const mockEvent = { target: { value: 'dashboard' } };
const mockEvent = { target: { value: 'dashboard' } } as React.ChangeEvent<HTMLInputElement>;
instance.onSearchQueryChange(mockEvent);

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import Page from 'app/core/components/Page/Page';
import AlertRuleItem from './AlertRuleItem';
import appEvents from 'app/core/app_events';
import { updateLocation } from 'app/core/actions';
@ -19,6 +19,7 @@ export interface Props {
togglePauseAlertRule: typeof togglePauseAlertRule;
stateFilter: string;
search: string;
isLoading: boolean;
}
export class AlertRuleList extends PureComponent<Props, any> {
@ -54,9 +55,9 @@ export class AlertRuleList extends PureComponent<Props, any> {
return 'all';
}
onStateFilterChanged = event => {
onStateFilterChanged = (evt: React.ChangeEvent<HTMLSelectElement>) => {
this.props.updateLocation({
query: { state: event.target.value },
query: { state: evt.target.value },
});
};
@ -68,8 +69,8 @@ export class AlertRuleList extends PureComponent<Props, any> {
});
};
onSearchQueryChange = event => {
const { value } = event.target;
onSearchQueryChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const { value } = evt.target;
this.props.setSearchQuery(value);
};
@ -77,7 +78,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' });
};
alertStateFilterOption = ({ text, value }) => {
alertStateFilterOption = ({ text, value }: { text: string; value: string; }) => {
return (
<option key={value} value={value}>
{text}
@ -86,12 +87,11 @@ export class AlertRuleList extends PureComponent<Props, any> {
};
render() {
const { navModel, alertRules, search } = this.props;
const { navModel, alertRules, search, isLoading } = this.props;
return (
<div>
<PageHeader model={navModel} />
<div className="page-container page-body">
<Page navModel={navModel}>
<Page.Contents isLoading={isLoading}>
<div className="page-action-bar">
<div className="gf-form gf-form--grow">
<label className="gf-form--has-input-icon gf-form--grow">
@ -131,8 +131,8 @@ export class AlertRuleList extends PureComponent<Props, any> {
))}
</ol>
</section>
</div>
</div>
</Page.Contents>
</Page>
);
}
}
@ -142,6 +142,7 @@ const mapStateToProps = (state: StoreState) => ({
alertRules: getAlertRuleItems(state.alertRules),
stateFilter: state.location.query.state,
search: getSearchQuery(state.alertRules),
isLoading: state.alertRules.isLoading
});
const mapDispatchToProps = {

@ -12,8 +12,8 @@ import StateHistory from './StateHistory';
import 'app/features/alerting/AlertTabCtrl';
// Types
import { DashboardModel } from '../dashboard/dashboard_model';
import { PanelModel } from '../dashboard/panel_model';
import { DashboardModel } from '../dashboard/state/DashboardModel';
import { PanelModel } from '../dashboard/state/PanelModel';
import { TestRuleResult } from './TestRuleResult';
interface Props {

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import alertDef from './state/alertDef';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { DashboardModel } from '../dashboard/dashboard_model';
import { DashboardModel } from '../dashboard/state/DashboardModel';
import appEvents from '../../core/app_events';
interface Props {

@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DashboardModel } from '../dashboard/dashboard_model';
import { DashboardModel } from '../dashboard/state/DashboardModel';
import { Props, TestRuleResult } from './TestRuleResult';
jest.mock('app/core/services/backend_srv', () => ({

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { DashboardModel } from '../dashboard/dashboard_model';
import { DashboardModel } from '../dashboard/state/DashboardModel';
import { LoadingPlaceholder } from '@grafana/ui/src';
export interface Props {

@ -1,12 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render alert rules 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={Object {}}
>
<PageContents
isLoading={false}
>
<div
className="page-action-bar"
@ -151,17 +150,16 @@ exports[`Render should render alert rules 1`] = `
/>
</ol>
</section>
</div>
</div>
</PageContents>
</Page>
`;
exports[`Render should render component 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={Object {}}
>
<PageContents
isLoading={false}
>
<div
className="page-action-bar"
@ -263,6 +261,6 @@ exports[`Render should render component 1`] = `
className="alert-rule-list"
/>
</section>
</div>
</div>
</PageContents>
</Page>
`;

@ -4,11 +4,16 @@ import { ThunkAction } from 'redux-thunk';
export enum ActionTypes {
LoadAlertRules = 'LOAD_ALERT_RULES',
LoadedAlertRules = 'LOADED_ALERT_RULES',
SetSearchQuery = 'SET_ALERT_SEARCH_QUERY',
}
export interface LoadAlertRulesAction {
type: ActionTypes.LoadAlertRules;
}
export interface LoadedAlertRulesAction {
type: ActionTypes.LoadedAlertRules;
payload: AlertRuleDTO[];
}
@ -17,8 +22,12 @@ export interface SetSearchQueryAction {
payload: string;
}
export const loadAlertRules = (rules: AlertRuleDTO[]): LoadAlertRulesAction => ({
export const loadAlertRules = (): LoadAlertRulesAction => ({
type: ActionTypes.LoadAlertRules,
});
export const loadedAlertRules = (rules: AlertRuleDTO[]): LoadedAlertRulesAction => ({
type: ActionTypes.LoadedAlertRules,
payload: rules,
});
@ -27,14 +36,15 @@ export const setSearchQuery = (query: string): SetSearchQueryAction => ({
payload: query,
});
export type Action = LoadAlertRulesAction | SetSearchQueryAction;
export type Action = LoadAlertRulesAction | LoadedAlertRulesAction | SetSearchQueryAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
return async dispatch => {
const rules = await getBackendSrv().get('/api/alerts', options);
dispatch(loadAlertRules(rules));
dispatch(loadAlertRules());
const rules: AlertRuleDTO[] = await getBackendSrv().get('/api/alerts', options);
dispatch(loadedAlertRules(rules));
};
}

@ -80,7 +80,7 @@ describe('Alert rules', () => {
it('should set alert rules', () => {
const action: Action = {
type: ActionTypes.LoadAlertRules,
type: ActionTypes.LoadedAlertRules,
payload: payload,
};

@ -3,7 +3,7 @@ import { AlertRuleDTO, AlertRule, AlertRulesState } from 'app/types';
import { Action, ActionTypes } from './actions';
import alertDef from './alertDef';
export const initialState: AlertRulesState = { items: [], searchQuery: '' };
export const initialState: AlertRulesState = { items: [], searchQuery: '', isLoading: false };
function convertToAlertRule(rule, state): AlertRule {
const stateModel = alertDef.getStateDisplayModel(state);
@ -29,17 +29,21 @@ function convertToAlertRule(rule, state): AlertRule {
export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => {
switch (action.type) {
case ActionTypes.LoadAlertRules: {
return { ...state, isLoading: true };
}
case ActionTypes.LoadedAlertRules: {
const alertRules: AlertRuleDTO[] = action.payload;
const alertRulesViewModel: AlertRule[] = alertRules.map(rule => {
return convertToAlertRule(rule, rule.state);
});
return { items: alertRulesViewModel, searchQuery: state.searchQuery };
return { ...state, items: alertRulesViewModel, isLoading: false };
}
case ActionTypes.SetSearchQuery:
return { items: state.items, searchQuery: action.payload };
return { ...state, searchQuery: action.payload };
}
return state;

@ -10,7 +10,7 @@ import coreModule from 'app/core/core_module';
import { makeRegions, dedupAnnotations } from './events_processing';
// Types
import { DashboardModel } from '../dashboard/dashboard_model';
import { DashboardModel } from '../dashboard/state/DashboardModel';
export class AnnotationsSrv {
globalAnnotationsPromise: any;

@ -1,5 +1,3 @@
import '../annotations_srv';
import 'app/features/dashboard/time_srv';
import { AnnotationsSrv } from '../annotations_srv';
describe('AnnotationsSrv', () => {

@ -1,8 +1,8 @@
import React from 'react';
import _ from 'lodash';
import config from 'app/core/config';
import { PanelModel } from '../../panel_model';
import { DashboardModel } from '../../dashboard_model';
import { PanelModel } from '../../state/PanelModel';
import { DashboardModel } from '../../state/DashboardModel';
import store from 'app/core/store';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { updateLocation } from 'app/core/actions';

@ -7,7 +7,7 @@ jest.mock('app/core/store', () => {
import _ from 'lodash';
import config from 'app/core/config';
import { DashboardExporter } from './DashboardExporter';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
describe('given dashboard with repeated panels', () => {
let dash, exported;

@ -1,6 +1,6 @@
import config from 'app/core/config';
import _ from 'lodash';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
export class DashboardExporter {
constructor(private datasourceSrv) {}

@ -1,7 +1,7 @@
import moment from 'moment';
import angular from 'angular';
import { appEvents, NavModel } from 'app/core/core';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
export class DashNavCtrl {
dashboard: DashboardModel;

@ -37,7 +37,7 @@
<i class="fa fa-link"></i>
</a>
<button class="btn navbar-button navbar-button--settings" ng-click="ctrl.toggleSettings()" bs-tooltip="'Settings'" data-placement="bottom" ng-show="ctrl.dashboard.meta.showSettings">
<button class="btn navbar-button navbar-button--settings" ng-click="ctrl.toggleSettings()" bs-tooltip="'Dashboard Settings'" data-placement="bottom" ng-show="ctrl.dashboard.meta.showSettings">
<i class="fa fa-cog"></i>
</button>
</div>

@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DashboardRow } from '../dashgrid/DashboardRow';
import { PanelModel } from '../panel_model';
import { DashboardRow } from './DashboardRow';
import { PanelModel } from '../../state/PanelModel';
describe('DashboardRow', () => {
let wrapper, panel, dashboardMock;

@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../../state/PanelModel';
import { DashboardModel } from '../../state/DashboardModel';
import templateSrv from 'app/features/templating/template_srv';
import appEvents from 'app/core/app_events';
@ -18,13 +18,18 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
collapsed: this.props.panel.collapsed,
};
this.toggle = this.toggle.bind(this);
this.openSettings = this.openSettings.bind(this);
this.delete = this.delete.bind(this);
this.update = this.update.bind(this);
appEvents.on('template-variable-value-updated', this.onVariableUpdated);
}
toggle() {
componentWillUnmount() {
appEvents.off('template-variable-value-updated', this.onVariableUpdated);
}
onVariableUpdated = () => {
this.forceUpdate();
}
onToggle = () => {
this.props.dashboard.toggleRow(this.props.panel);
this.setState(prevState => {
@ -32,23 +37,23 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
});
}
update() {
onUpdate = () => {
this.props.dashboard.processRepeats();
this.forceUpdate();
}
openSettings() {
onOpenSettings = () => {
appEvents.emit('show-modal', {
templateHtml: `<row-options row="model.row" on-updated="model.onUpdated()" dismiss="dismiss()"></row-options>`,
modalClass: 'modal--narrow',
model: {
row: this.props.panel,
onUpdated: this.update.bind(this),
onUpdated: this.onUpdate,
},
});
}
delete() {
onDelete = () => {
appEvents.emit('confirm-modal', {
title: 'Delete Row',
text: 'Are you sure you want to remove this row and all its panels?',
@ -81,7 +86,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
return (
<div className={classes}>
<a className="dashboard-row__title pointer" onClick={this.toggle}>
<a className="dashboard-row__title pointer" onClick={this.onToggle}>
<i className={chevronClass} />
{title}
<span className="dashboard-row__panel_count">
@ -90,16 +95,16 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
</a>
{canEdit && (
<div className="dashboard-row__actions">
<a className="pointer" onClick={this.openSettings}>
<a className="pointer" onClick={this.onOpenSettings}>
<i className="fa fa-cog" />
</a>
<a className="pointer" onClick={this.delete}>
<a className="pointer" onClick={this.onDelete}>
<i className="fa fa-trash" />
</a>
</div>
)}
{this.state.collapsed === true && (
<div className="dashboard-row__toggle-target" onClick={this.toggle}>
<div className="dashboard-row__toggle-target" onClick={this.onToggle}>
&nbsp;
</div>
)}

@ -0,0 +1 @@
export { DashboardRow } from './DashboardRow';

@ -1,5 +1,5 @@
import { coreModule, appEvents, contextSrv } from 'app/core/core';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
import $ from 'jquery';
import _ from 'lodash';
import angular from 'angular';

@ -24,7 +24,7 @@ export class RowOptionsCtrl {
export function rowOptionsDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/features/dashboard/partials/row_options.html',
templateUrl: 'public/app/features/dashboard/components/RowOptions/template.html',
controller: RowOptionsCtrl,
bindToController: true,
controllerAs: 'ctrl',

@ -3,7 +3,7 @@ import angular from 'angular';
import moment from 'moment';
import locationUtil from 'app/core/utils/location_util';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './HistorySrv';
export class HistoryListCtrl {

@ -1,6 +1,6 @@
import { versions, restore } from './__mocks__/history';
import { HistorySrv } from './HistorySrv';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
jest.mock('app/core/store');
describe('historySrv', () => {

@ -1,6 +1,6 @@
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
export interface HistoryListOpts {
limit: number;

@ -5,10 +5,10 @@ import coreModule from 'app/core/core_module';
import { removePanel } from 'app/features/dashboard/utils/panel';
// Services
import { AnnotationsSrv } from '../annotations/annotations_srv';
import { AnnotationsSrv } from '../../annotations/annotations_srv';
// Types
import { DashboardModel } from './dashboard_model';
import { DashboardModel } from '../state/DashboardModel';
export class DashboardCtrl {
dashboard: DashboardModel;

@ -0,0 +1,123 @@
// Libraries
import React, { Component } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
// Utils & Services
import appEvents from 'app/core/app_events';
import locationUtil from 'app/core/utils/location_util';
import { getBackendSrv } from 'app/core/services/backend_srv';
// Components
import { DashboardPanel } from '../dashgrid/DashboardPanel';
// Redux
import { updateLocation } from 'app/core/actions';
// Types
import { StoreState } from 'app/types';
import { PanelModel, DashboardModel } from 'app/features/dashboard/state';
interface Props {
panelId: string;
urlUid?: string;
urlSlug?: string;
urlType?: string;
$scope: any;
$injector: any;
updateLocation: typeof updateLocation;
}
interface State {
panel: PanelModel | null;
dashboard: DashboardModel | null;
notFound: boolean;
}
export class SoloPanelPage extends Component<Props, State> {
state: State = {
panel: null,
dashboard: null,
notFound: false,
};
componentDidMount() {
const { $injector, $scope, urlUid, urlType, urlSlug } = this.props;
// handle old urls with no uid
if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) {
this.redirectToNewUrl();
return;
}
const dashboardLoaderSrv = $injector.get('dashboardLoaderSrv');
// subscribe to event to know when dashboard controller is done with inititalization
appEvents.on('dashboard-initialized', this.onDashoardInitialized);
dashboardLoaderSrv.loadDashboard(urlType, urlSlug, urlUid).then(result => {
result.meta.soloMode = true;
$scope.initDashboard(result, $scope);
});
}
redirectToNewUrl() {
getBackendSrv().getDashboardBySlug(this.props.urlSlug).then(res => {
if (res) {
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
this.props.updateLocation(url);
}
});
}
onDashoardInitialized = () => {
const { $scope, panelId } = this.props;
const dashboard: DashboardModel = $scope.dashboard;
const panel = dashboard.getPanelById(parseInt(panelId, 10));
if (!panel) {
this.setState({ notFound: true });
return;
}
this.setState({ dashboard, panel });
};
render() {
const { panelId } = this.props;
const { notFound, panel, dashboard } = this.state;
if (notFound) {
return (
<div className="alert alert-error">
Panel with id { panelId } not found
</div>
);
}
if (!panel) {
return <div>Loading & initializing dashboard</div>;
}
return (
<div className="panel-solo">
<DashboardPanel dashboard={dashboard} panel={panel} isEditing={false} isFullscreen={false} />
</div>
);
}
}
const mapStateToProps = (state: StoreState) => ({
urlUid: state.location.routeParams.uid,
urlSlug: state.location.routeParams.slug,
urlType: state.location.routeParams.type,
panelId: state.location.query.panelId
});
const mapDispatchToProps = {
updateLocation
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SoloPanelPage));

@ -1,16 +1,30 @@
import React from 'react';
import { hot } from 'react-hot-loader';
import ReactGridLayout from 'react-grid-layout';
import ReactGridLayout, { ItemCallback } from 'react-grid-layout';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
import { DashboardPanel } from './DashboardPanel';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../panel_model';
import { DashboardModel, PanelModel } from '../state';
import classNames from 'classnames';
import sizeMe from 'react-sizeme';
let lastGridWidth = 1200;
let ignoreNextWidthChange = false;
interface GridWrapperProps {
size: { width: number; };
layout: ReactGridLayout.Layout[];
onLayoutChange: (layout: ReactGridLayout.Layout[]) => void;
children: JSX.Element | JSX.Element[];
onDragStop: ItemCallback;
onResize: ItemCallback;
onResizeStop: ItemCallback;
onWidthChange: () => void;
className: string;
isResizable?: boolean;
isDraggable?: boolean;
isFullscreen?: boolean;
}
function GridWrapper({
size,
layout,
@ -24,7 +38,7 @@ function GridWrapper({
isResizable,
isDraggable,
isFullscreen,
}) {
}: GridWrapperProps) {
const width = size.width > 0 ? size.width : lastGridWidth;
// logic to ignore width changes (optimization)
@ -43,7 +57,6 @@ function GridWrapper({
className={className}
isDraggable={isDraggable}
isResizable={isResizable}
measureBeforeMount={false}
containerPadding={[0, 0]}
useCSSTransforms={false}
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
@ -71,22 +84,17 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
gridToPanelMap: any;
panelMap: { [id: string]: PanelModel };
constructor(props) {
constructor(props: DashboardGridProps) {
super(props);
this.onLayoutChange = this.onLayoutChange.bind(this);
this.onResize = this.onResize.bind(this);
this.onResizeStop = this.onResizeStop.bind(this);
this.onDragStop = this.onDragStop.bind(this);
this.onWidthChange = this.onWidthChange.bind(this);
// subscribe to dashboard events
const dashboard = this.props.dashboard;
dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this));
dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
dashboard.on('panel-added', this.triggerForceUpdate);
dashboard.on('panel-removed', this.triggerForceUpdate);
dashboard.on('repeats-processed', this.triggerForceUpdate);
dashboard.on('view-mode-changed', this.onViewModeChanged);
dashboard.on('row-collapsed', this.triggerForceUpdate);
dashboard.on('row-expanded', this.triggerForceUpdate);
}
buildLayout() {
@ -123,7 +131,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
return layout;
}
onLayoutChange(newLayout) {
onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => {
for (const newPos of newLayout) {
this.panelMap[newPos.i].updateGridPos(newPos);
}
@ -131,22 +139,22 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
this.props.dashboard.sortPanelsByGridPos();
}
triggerForceUpdate() {
triggerForceUpdate = () => {
this.forceUpdate();
}
onWidthChange() {
onWidthChange = () => {
for (const panel of this.props.dashboard.panels) {
panel.resizeDone();
}
}
onViewModeChanged(payload) {
onViewModeChanged = () => {
ignoreNextWidthChange = true;
this.forceUpdate();
}
updateGridPos(item, layout) {
updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
this.panelMap[item.i].updateGridPos(item);
// react-grid-layout has a bug (#670), and onLayoutChange() is only called when the component is mounted.
@ -154,16 +162,17 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
this.onLayoutChange(layout);
}
onResize(layout, oldItem, newItem) {
onResize: ItemCallback = (layout, oldItem, newItem) => {
console.log();
this.panelMap[newItem.i].updateGridPos(newItem);
}
onResizeStop(layout, oldItem, newItem) {
onResizeStop: ItemCallback = (layout, oldItem, newItem) => {
this.updateGridPos(newItem, layout);
this.panelMap[newItem.i].resizeDone();
}
onDragStop(layout, oldItem, newItem) {
onDragStop: ItemCallback = (layout, oldItem, newItem) => {
this.updateGridPos(newItem, layout);
}

@ -7,12 +7,11 @@ import { importPluginModule } from 'app/features/plugins/plugin_loader';
import { AddPanelWidget } from '../components/AddPanelWidget';
import { getPanelPluginNotFound } from './PanelPluginNotFound';
import { DashboardRow } from './DashboardRow';
import { DashboardRow } from '../components/DashboardRow';
import { PanelChrome } from './PanelChrome';
import { PanelEditor } from '../panel_editor/PanelEditor';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { PanelModel, DashboardModel } from '../state';
import { PanelPlugin } from 'app/types';
import { PanelResizer } from './PanelResizer';

@ -1,24 +1,28 @@
// Library
import React, { Component } from 'react';
import { Tooltip } from '@grafana/ui';
import { Themes } from '@grafana/ui/src/components/Tooltip/Tooltip';
import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';
// Services
import { getDatasourceSrv, DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv';
// Utils
import kbn from 'app/core/utils/kbn';
// Types
import { TimeRange, TimeSeries, LoadingState, DataQueryResponse, DataQueryOptions } from '@grafana/ui/src/types';
import {
DataQueryOptions,
DataQueryResponse,
LoadingState,
PanelData,
TableData,
TimeRange,
TimeSeries,
} from '@grafana/ui';
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
interface RenderProps {
loading: LoadingState;
timeSeries: TimeSeries[];
panelData: PanelData;
}
export interface Props {
@ -33,6 +37,7 @@ export interface Props {
minInterval?: string;
maxDataPoints?: number;
children: (r: RenderProps) => JSX.Element;
onDataResponse?: (data: DataQueryResponse) => void;
}
export interface State {
@ -86,7 +91,17 @@ export class DataPanel extends Component<Props, State> {
}
private issueQueries = async () => {
const { isVisible, queries, datasource, panelId, dashboardId, timeRange, widthPixels, maxDataPoints } = this.props;
const {
isVisible,
queries,
datasource,
panelId,
dashboardId,
timeRange,
widthPixels,
maxDataPoints,
onDataResponse,
} = this.props;
if (!isVisible) {
return;
@ -120,14 +135,16 @@ export class DataPanel extends Component<Props, State> {
cacheTimeout: null,
};
console.log('Issuing DataPanel query', queryOptions);
const resp = await ds.query(queryOptions);
console.log('Issuing DataPanel query Resp', resp);
if (this.isUnmounted) {
return;
}
if (onDataResponse) {
onDataResponse(resp);
}
this.setState({
loading: LoadingState.Done,
response: resp,
@ -149,11 +166,27 @@ export class DataPanel extends Component<Props, State> {
}
};
getPanelData = () => {
const { response } = this.state;
if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') {
return {
tableData: response.data[0] as TableData,
timeSeries: null,
};
}
return {
timeSeries: response.data as TimeSeries[],
tableData: null,
};
};
render() {
const { queries } = this.props;
const { response, loading, isFirstLoad } = this.state;
const { loading, isFirstLoad } = this.state;
const timeSeries = response.data;
const panelData = this.getPanelData();
if (isFirstLoad && loading === LoadingState.Loading) {
return this.renderLoadingStates();
@ -179,8 +212,8 @@ export class DataPanel extends Component<Props, State> {
return (
<>
{this.props.children({
timeSeries,
loading,
panelData,
})}
</>
);
@ -200,7 +233,7 @@ export class DataPanel extends Component<Props, State> {
);
} else if (loading === LoadingState.Error) {
return (
<Tooltip content={errorMessage} placement="bottom-start" theme={Themes.Error}>
<Tooltip content={errorMessage} placement="bottom-start" theme="error">
<div className="panel-info-corner panel-info-corner--error">
<i className="fa" />
<span className="panel-info-corner-inner" />

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save