diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8144956773b..69cea87dccd 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -35,7 +35,7 @@ jobs:
- run: cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h 127.0.0.1 -P 3306 -u root -prootpass
- run:
name: mysql integration tests
- command: 'GRAFANA_TEST_DB=mysql go test ./pkg/services/sqlstore/... ./pkg/tsdb/mysql/... '
+ command: './scripts/circle-test-mysql.sh'
postgres-integration-test:
docker:
@@ -54,7 +54,7 @@ jobs:
- run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql'
- run:
name: postgres integration tests
- command: 'GRAFANA_TEST_DB=postgres go test ./pkg/services/sqlstore/... ./pkg/tsdb/postgres/...'
+ command: './scripts/circle-test-postgres.sh'
codespell:
docker:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d67030873c3..cb2a344b014 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+# 6.1.0 (unreleased)
+
+### Minor
+* **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow)
+
+### Bug Fixes
+* **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
+* **Datasource**: Handles nil jsondata field gracefully [#14239](https://github.com/grafana/grafana/issues/14239)
+* **Gauge**: Interpolate scoped variables in repeated gauges [#15739](https://github.com/grafana/grafana/issues/15739)
+* **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619)
+
+# 6.0.1 (unreleased)
+
+### Bug Fixes
+* **Metrics**: Fixes broken usagestats metrics for /metrics [#15651](https://github.com/grafana/grafana/issues/15651)
+* **Dashboard**: Fixes kiosk mode should have &kiosk appended to the url [#15765](https://github.com/grafana/grafana/issues/15765)
+* **Dashboard**: Fixes kiosk=tv mode with autofitpanels should respect header [#15650](https://github.com/grafana/grafana/issues/15650)
+
# 6.0.0 stable (2019-02-25)
### Bug Fixes
diff --git a/devenv/docker/blocks/influxdb/docker-compose.yaml b/devenv/docker/blocks/influxdb/docker-compose.yaml
index e3ca81ced22..f34a7ef4978 100644
--- a/devenv/docker/blocks/influxdb/docker-compose.yaml
+++ b/devenv/docker/blocks/influxdb/docker-compose.yaml
@@ -1,5 +1,3 @@
-version: '2'
-services:
influxdb:
image: influxdb:latest
container_name: influxdb
diff --git a/docs/sources/guides/whats-new-in-v6-0.md b/docs/sources/guides/whats-new-in-v6-0.md
index 7c61df1c3c4..1506cee6dbd 100644
--- a/docs/sources/guides/whats-new-in-v6-0.md
+++ b/docs/sources/guides/whats-new-in-v6-0.md
@@ -12,7 +12,7 @@ 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.
+This update to Grafana introduces a new way of exploring your data, support for log data, and tons of other features.
The main highlights are:
@@ -25,24 +25,24 @@ The main highlights are:
- [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.
-- [Removal of user session storage]({{< relref "#easier-to-deploy-improved-security" >}}) makes Grafana easier to deploy & improves security.
+- [Removal of user session storage]({{< relref "#easier-to-deploy-improved-security" >}}) makes Grafana easier to deploy and improves security.
## 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.
+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 and 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
+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
+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.
@@ -78,8 +78,8 @@ for other log sources to Explore and the next planned integration is Elasticsear
## 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
+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! Check out the
video below to see the new Panel Editor in action.
@@ -94,7 +94,7 @@ video below to see the new Panel Editor in action.
### 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
+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.
@@ -105,7 +105,7 @@ continue to refine and start using in other panels.
### 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's code base and ecosystem. Starting in v6.0 **Panels** and **Data
+is part of the future-proofing of Grafana'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 soon.
@@ -120,7 +120,7 @@ To get started read the guide: [Using Google Stackdriver in Grafana](/features/d
## 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 is able to get alerting support, in the 6.0 release alerting is supported for the Azure Monitor service, with the rest to follow.
+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 is able to get alerting support, in the 6.0 release alerting is supported for the Azure Monitor service, with the rest to follow.
The Azure Monitor datasource integrates four Azure services with Grafana - Azure Monitor, Azure Log Analytics, Azure Application Insights and Azure Application Insights Analytics.
@@ -128,15 +128,15 @@ Please read [Using Azure Monitor in Grafana documentation](/features/datasources
## 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.
+Grafana now has 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. This is the 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 exist in the instance.
## Easier to deploy & improved security
-Grafana 6.0 removes the need of configuring and setup of additional storage for [user sessions](/tutorials/ha_setup/#user-sessions). This should make it easier to deploy and operate Grafana in a
-high availability setup and/or if you're using a stateless user session storage like Redis, Memcache, Postgres or MySQL.
+Grafana 6.0 removes the need to configure and set up additional storage for [user sessions](/tutorials/ha_setup/#user-sessions). This should make it easier to deploy and operate Grafana in a
+high availability setup and/or if you're using a stateless user session store like Redis, Memcache, Postgres or MySQL.
-Instead of user sessions a solution based on short-lived tokens that are rotated frequently have been implemented. This also replaces the old "remember me cookie"
-solution, which allowed a user to be logged in between browser sessions, and which have been subject to several security holes throughout the years.
+Instead of user sessions, we've implemented a solution based on short-lived tokens that are rotated frequently. This also replaces the old "remember me cookie"
+solution, which allowed a user to be logged in between browser sessions and which have been subject to several security holes throughout the years.
Read more about the short-lived token solution and how to configure it [here](/auth/overview/#login-and-short-lived-tokens).
> Please note that due to these changes, all users will be required to login upon next visit after upgrade.
@@ -146,15 +146,15 @@ Besides these changes we have also made security improvements regarding Cross-Si
* Cookies are per default using the [SameSite](/installation/configuration/#cookie-samesite) attribute to protect against CSRF attacks
* Script tags in text panels are per default [disabled](/installation/configuration/#disable-sanitize-html) to protect against XSS attacks
-> If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions setup and configured
-but our goal is to remove this requirements in a near future.
+> If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions set up and configured
+but our goal is to remove this requirement in the near future.
## 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
+helps making colors more consistent across dashboards. We hope to do more in this color picker in the future, like showing
colors used in the dashboard.
Named colors also enables Grafana to adapt colors to the current theme.
@@ -163,7 +163,7 @@ Named colors also enables Grafana to adapt colors to the current theme.
## 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.
+- 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`
diff --git a/docs/versions.json b/docs/versions.json
index b1e447d5aa0..d1eab9afa51 100644
--- a/docs/versions.json
+++ b/docs/versions.json
@@ -1,5 +1,6 @@
[
- { "version": "v5.4", "path": "/", "archived": false, "current": true },
+ { "version": "v6.0", "path": "/", "archived": false, "current": true },
+ { "version": "v5.4", "path": "/v5.4", "archived": true },
{ "version": "v5.3", "path": "/v5.3", "archived": true },
{ "version": "v5.2", "path": "/v5.2", "archived": true },
{ "version": "v5.1", "path": "/v5.1", "archived": true },
diff --git a/package.json b/package.json
index 9e0b88804fd..04a6967fdb9 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
- "version": "6.0.0-pre3",
+ "version": "6.1.0-pre",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@@ -162,7 +162,7 @@
"license": "Apache-2.0",
"dependencies": {
"@babel/polyfill": "^7.0.0",
- "@torkelo/react-select": "2.1.1",
+ "@torkelo/react-select": "2.4.1",
"@types/reselect": "^2.2.0",
"angular": "1.6.6",
"angular-bindonce": "0.3.1",
diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
index d52d2fea80d..1bc1fd42792 100644
--- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
+++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
@@ -1,4 +1,4 @@
-import React, { PureComponent } from 'react';
+import React, { Component } from 'react';
import isNil from 'lodash/isNil';
import classNames from 'classnames';
import Scrollbars from 'react-custom-scrollbars';
@@ -20,7 +20,7 @@ interface Props {
/**
* Wraps component into component from `react-custom-scrollbars`
*/
-export class CustomScrollbar extends PureComponent {
+export class CustomScrollbar extends Component {
static defaultProps: Partial = {
autoHide: false,
autoHideTimeout: 200,
@@ -42,11 +42,7 @@ export class CustomScrollbar extends PureComponent {
const ref = this.ref.current;
if (ref && !isNil(this.props.scrollTop)) {
- if (this.props.scrollTop > 10000) {
- ref.scrollToBottom();
- } else {
- ref.scrollTop(this.props.scrollTop);
- }
+ ref.scrollTop(this.props.scrollTop);
}
}
diff --git a/public/app/core/components/Select/UnitPicker.tsx b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx
similarity index 87%
rename from public/app/core/components/Select/UnitPicker.tsx
rename to packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx
index f9dbc0ae421..3a34df02a39 100644
--- a/public/app/core/components/Select/UnitPicker.tsx
+++ b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx
@@ -1,6 +1,8 @@
import React, { PureComponent } from 'react';
-import { getValueFormats } from '@grafana/ui';
-import { Select } from '@grafana/ui';
+
+import { Select } from '..';
+
+import { getValueFormats } from '../../utils';
interface Props {
onChange: (item: any) => void;
@@ -8,7 +10,7 @@ interface Props {
width?: number;
}
-export default class UnitPicker extends PureComponent {
+export class UnitPicker extends PureComponent {
static defaultProps = {
width: 12,
};
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index 642f6102a12..fdf39d77d65 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -27,3 +27,4 @@ export { Gauge } from './Gauge/Gauge';
export { Switch } from './Switch/Switch';
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
export { Piechart, PiechartDataPoint, PiechartType } from './Piechart/Piechart';
+export { UnitPicker } from './UnitPicker/UnitPicker';
diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts
index a34f39b59c6..79c5b22488d 100644
--- a/packages/grafana-ui/src/types/datasource.ts
+++ b/packages/grafana-ui/src/types/datasource.ts
@@ -39,6 +39,16 @@ export interface DataQueryError {
statusText?: string;
}
+export interface ScopedVar {
+ text: any;
+ value: any;
+ [key: string]: any;
+}
+
+export interface ScopedVars {
+ [key: string]: ScopedVar;
+}
+
export interface DataQueryOptions {
timezone: string;
range: TimeRange;
@@ -50,7 +60,7 @@ export interface DataQueryOptions {
interval: string;
intervalMs: number;
maxDataPoints: number;
- scopedVars: object;
+ scopedVars: ScopedVars;
}
export interface QueryFix {
diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts
index 2da48b0fec6..ae205100c13 100644
--- a/packages/grafana-ui/src/types/panel.ts
+++ b/packages/grafana-ui/src/types/panel.ts
@@ -12,7 +12,7 @@ export interface PanelProps {
renderCounter: number;
width: number;
height: number;
- onInterpolate: InterpolateFunction;
+ replaceVariables: InterpolateFunction;
}
export interface PanelData {
@@ -22,7 +22,7 @@ export interface PanelData {
export interface PanelEditorProps {
options: T;
- onChange: (options: T) => void;
+ onOptionsChange: (options: T) => void;
}
export class ReactPanelPlugin {
diff --git a/packages/grafana-ui/src/utils/valueFormats/categories.ts b/packages/grafana-ui/src/utils/valueFormats/categories.ts
index e127285b473..806da582bb3 100644
--- a/packages/grafana-ui/src/utils/valueFormats/categories.ts
+++ b/packages/grafana-ui/src/utils/valueFormats/categories.ts
@@ -125,7 +125,7 @@ export const getCategories = (): ValueFormatCategory[] => [
{
name: 'Data (Metric)',
formats: [
- { name: 'bits', id: 'decbits', fn: decimalSIPrefix('d') },
+ { name: 'bits', id: 'decbits', fn: decimalSIPrefix('b') },
{ name: 'bytes', id: 'decbytes', fn: decimalSIPrefix('B') },
{ name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) },
{ name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) },
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 6da127fb550..81ea83eae61 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -33,17 +33,17 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/profile/", reqSignedIn, hs.Index)
r.Get("/profile/password", reqSignedIn, hs.Index)
r.Get("/profile/switch-org/:id", reqSignedIn, hs.ChangeActiveOrgAndRedirectToHome)
- r.Get("/org/", reqSignedIn, hs.Index)
- r.Get("/org/new", reqSignedIn, hs.Index)
- r.Get("/datasources/", reqSignedIn, hs.Index)
- r.Get("/datasources/new", reqSignedIn, hs.Index)
- r.Get("/datasources/edit/*", reqSignedIn, hs.Index)
- r.Get("/org/users", reqSignedIn, hs.Index)
- r.Get("/org/users/new", reqSignedIn, hs.Index)
- r.Get("/org/users/invite", reqSignedIn, hs.Index)
- r.Get("/org/teams", reqSignedIn, hs.Index)
- r.Get("/org/teams/*", reqSignedIn, hs.Index)
- r.Get("/org/apikeys/", reqSignedIn, hs.Index)
+ r.Get("/org/", reqOrgAdmin, hs.Index)
+ r.Get("/org/new", reqGrafanaAdmin, hs.Index)
+ r.Get("/datasources/", reqOrgAdmin, hs.Index)
+ r.Get("/datasources/new", reqOrgAdmin, hs.Index)
+ r.Get("/datasources/edit/*", reqOrgAdmin, hs.Index)
+ r.Get("/org/users", reqOrgAdmin, hs.Index)
+ r.Get("/org/users/new", reqOrgAdmin, hs.Index)
+ r.Get("/org/users/invite", reqOrgAdmin, hs.Index)
+ r.Get("/org/teams", reqOrgAdmin, hs.Index)
+ r.Get("/org/teams/*", reqOrgAdmin, hs.Index)
+ r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
r.Get("/admin", reqGrafanaAdmin, hs.Index)
@@ -73,12 +73,12 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/dashboards/", reqSignedIn, hs.Index)
r.Get("/dashboards/*", reqSignedIn, hs.Index)
- r.Get("/explore", reqEditorRole, hs.Index)
+ r.Get("/explore", reqSignedIn, middleware.EnsureEditorOrViewerCanEdit, hs.Index)
r.Get("/playlists/", reqSignedIn, hs.Index)
r.Get("/playlists/*", reqSignedIn, hs.Index)
- r.Get("/alerting/", reqSignedIn, hs.Index)
- r.Get("/alerting/*", reqSignedIn, hs.Index)
+ r.Get("/alerting/", reqEditorRole, hs.Index)
+ r.Get("/alerting/*", reqEditorRole, hs.Index)
// sign up
r.Get("/signup", hs.Index)
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 248ebf63f0f..904a885b171 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -307,33 +307,26 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
}
}
- if c.OrgRole == m.ROLE_ADMIN && c.IsGrafanaAdmin {
- cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
- Divider: true, HideFromTabs: true, Id: "admin-divider", Text: "Text",
- })
- }
-
- if c.IsGrafanaAdmin {
- cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
- Text: "Server Admin",
- HideFromTabs: true,
- SubTitle: "Manage all users & orgs",
- Id: "admin",
- Icon: "gicon gicon-shield",
- Url: setting.AppSubUrl + "/admin/users",
- Children: []*dtos.NavLink{
- {Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
- {Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
- {Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
- {Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
- {Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"},
- },
- })
- }
-
data.NavTree = append(data.NavTree, cfgNode)
}
+ if c.IsGrafanaAdmin {
+ data.NavTree = append(data.NavTree, &dtos.NavLink{
+ Text: "Server Admin",
+ SubTitle: "Manage all users & orgs",
+ HideFromTabs: true,
+ Id: "admin",
+ Icon: "gicon gicon-shield",
+ Url: setting.AppSubUrl + "/admin/users",
+ Children: []*dtos.NavLink{
+ {Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
+ {Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
+ {Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
+ {Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
+ },
+ })
+ }
+
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Help",
SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go
index 86067cd7721..4b731db5d3d 100644
--- a/pkg/api/org_invite.go
+++ b/pkg/api/org_invite.go
@@ -27,6 +27,10 @@ func GetPendingOrgInvites(c *m.ReqContext) Response {
}
func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
+ if setting.DisableLoginForm {
+ return Error(400, "Cannot invite when login is disabled.", nil)
+ }
+
if !inviteDto.Role.IsValid() {
return Error(400, "Invalid role specified", nil)
}
@@ -37,10 +41,6 @@ func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
if err != m.ErrUserNotFound {
return Error(500, "Failed to query db for existing user check", err)
}
-
- if setting.DisableLoginForm {
- return Error(401, "User could not be found", nil)
- }
} else {
return inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
}
diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go
index 5ca136c32c4..2757f245060 100644
--- a/pkg/api/playlist_play.go
+++ b/pkg/api/playlist_play.go
@@ -52,8 +52,10 @@ func populateDashboardsByTag(orgID int64, signedInUser *m.SignedInUser, dashboar
for _, item := range searchQuery.Result {
result = append(result, dtos.PlaylistDashboard{
Id: item.Id,
+ Slug: item.Slug,
Title: item.Title,
Uri: item.Uri,
+ Url: m.GetDashboardUrl(item.Uid, item.Slug),
Order: dashboardTagOrder[tag],
})
}
diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go
index b454b362d90..53218147ae0 100644
--- a/pkg/cmd/grafana-server/server.go
+++ b/pkg/cmd/grafana-server/server.go
@@ -31,6 +31,7 @@ import (
_ "github.com/grafana/grafana/pkg/infra/metrics"
_ "github.com/grafana/grafana/pkg/infra/serverlock"
_ "github.com/grafana/grafana/pkg/infra/tracing"
+ _ "github.com/grafana/grafana/pkg/infra/usagestats"
_ "github.com/grafana/grafana/pkg/plugins"
_ "github.com/grafana/grafana/pkg/services/alerting"
_ "github.com/grafana/grafana/pkg/services/auth"
diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go
index 27248342c8d..e06409211eb 100644
--- a/pkg/middleware/auth.go
+++ b/pkg/middleware/auth.go
@@ -4,7 +4,7 @@ import (
"net/url"
"strings"
- "gopkg.in/macaron.v1"
+ macaron "gopkg.in/macaron.v1"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
@@ -52,6 +52,12 @@ func notAuthorized(c *m.ReqContext) {
c.Redirect(setting.AppSubUrl + "/login")
}
+func EnsureEditorOrViewerCanEdit(c *m.ReqContext) {
+ if !c.SignedInUser.HasRole(m.ROLE_EDITOR) && !setting.ViewersCanEdit {
+ accessForbidden(c)
+ }
+}
+
func RoleAuth(roles ...m.RoleType) macaron.Handler {
return func(c *m.ReqContext) {
ok := false
diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go
index 2da09672f13..475cb4a3777 100644
--- a/pkg/services/search/models.go
+++ b/pkg/services/search/models.go
@@ -17,6 +17,7 @@ type Hit struct {
Title string `json:"title"`
Uri string `json:"uri"`
Url string `json:"url"`
+ Slug string `json:"slug"`
Type HitType `json:"type"`
Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"`
diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go
index 1b69cce8c99..36fd7c5b023 100644
--- a/pkg/services/sqlstore/datasource.go
+++ b/pkg/services/sqlstore/datasource.go
@@ -3,6 +3,8 @@ package sqlstore
import (
"time"
+ "github.com/grafana/grafana/pkg/components/simplejson"
+
"github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/bus"
@@ -95,6 +97,10 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error {
return m.ErrDataSourceNameExists
}
+ if cmd.JsonData == nil {
+ cmd.JsonData = simplejson.New()
+ }
+
ds := &m.DataSource{
OrgId: cmd.OrgId,
Name: cmd.Name,
@@ -142,6 +148,10 @@ func updateIsDefaultFlag(ds *m.DataSource, sess *DBSession) error {
func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
return inTransaction(func(sess *DBSession) error {
+ if cmd.JsonData == nil {
+ cmd.JsonData = simplejson.New()
+ }
+
ds := &m.DataSource{
Id: cmd.Id,
OrgId: cmd.OrgId,
@@ -164,11 +174,6 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
Version: cmd.Version + 1,
}
- sess.UseBool("is_default")
- sess.UseBool("basic_auth")
- sess.UseBool("with_credentials")
- sess.UseBool("read_only")
-
var updateSession *xorm.Session
if cmd.Version != 0 {
// the reason we allow cmd.version > db.version is make it possible for people to force
@@ -180,7 +185,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
updateSession = sess.Where("id=? and org_id=?", ds.Id, ds.OrgId)
}
- affected, err := updateSession.Update(ds)
+ affected, err := updateSession.AllCols().Omit("created").Update(ds)
if err != nil {
return err
}
diff --git a/pkg/services/sqlstore/migrations/datasource_mig.go b/pkg/services/sqlstore/migrations/datasource_mig.go
index fc617be72a1..54d86d34dba 100644
--- a/pkg/services/sqlstore/migrations/datasource_mig.go
+++ b/pkg/services/sqlstore/migrations/datasource_mig.go
@@ -130,4 +130,7 @@ func addDataSourceMigration(mg *Migrator) {
const migrateLoggingToLoki = `UPDATE data_source SET type = 'loki' WHERE type = 'logging'`
mg.AddMigration("Migrate logging ds to loki ds", NewRawSqlMigration(migrateLoggingToLoki))
+
+ const setEmptyJSONWhereNullJSON = `UPDATE data_source SET json_data = '{}' WHERE json_data is null`
+ mg.AddMigration("Update json_data with nulls", NewRawSqlMigration(setEmptyJSONWhereNullJSON))
}
diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go
index cb95d39d82d..83fafbd87b5 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query.go
@@ -101,7 +101,7 @@ func init() {
"AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
- "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
+ "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "MaximumUsedTransactionIDs", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/Route53": {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"},
diff --git a/public/app/core/specs/file_export.test.ts b/public/app/core/specs/file_export.test.ts
index 98f5a5be742..9e2ff0a7ce1 100644
--- a/public/app/core/specs/file_export.test.ts
+++ b/public/app/core/specs/file_export.test.ts
@@ -60,6 +60,16 @@ describe('file_export', () => {
expect(text).toBe(expectedText);
});
+
+ it('should not modify series.datapoints', () => {
+ const expectedSeries1DataPoints = ctx.seriesList[0].datapoints.slice();
+ const expectedSeries2DataPoints = ctx.seriesList[1].datapoints.slice();
+
+ fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat);
+
+ expect(expectedSeries1DataPoints).toEqual(ctx.seriesList[0].datapoints);
+ expect(expectedSeries2DataPoints).toEqual(ctx.seriesList[1].datapoints);
+ });
});
describe('when exporting table data to csv', () => {
diff --git a/public/app/core/utils/errors.test.ts b/public/app/core/utils/errors.test.ts
new file mode 100644
index 00000000000..a5783fe7204
--- /dev/null
+++ b/public/app/core/utils/errors.test.ts
@@ -0,0 +1,55 @@
+import { getMessageFromError } from 'app/core/utils/errors';
+
+describe('errors functions', () => {
+ let message;
+
+ describe('when getMessageFromError gets an error string', () => {
+ beforeEach(() => {
+ message = getMessageFromError('error string');
+ });
+
+ it('should return the string', () => {
+ expect(message).toBe('error string');
+ });
+ });
+
+ describe('when getMessageFromError gets an error object with message field', () => {
+ beforeEach(() => {
+ message = getMessageFromError({ message: 'error string' });
+ });
+
+ it('should return the message text', () => {
+ expect(message).toBe('error string');
+ });
+ });
+
+ describe('when getMessageFromError gets an error object with data.message field', () => {
+ beforeEach(() => {
+ message = getMessageFromError({ data: { message: 'error string' } });
+ });
+
+ it('should return the message text', () => {
+ expect(message).toBe('error string');
+ });
+ });
+
+ describe('when getMessageFromError gets an error object with statusText field', () => {
+ beforeEach(() => {
+ message = getMessageFromError({ statusText: 'error string' });
+ });
+
+ it('should return the statusText text', () => {
+ expect(message).toBe('error string');
+ });
+ });
+
+ describe('when getMessageFromError gets an error object', () => {
+ beforeEach(() => {
+ message = getMessageFromError({ customError: 'error string' });
+ });
+
+ it('should return the stringified error', () => {
+ expect(message).toBe('{"customError":"error string"}');
+ });
+ });
+});
diff --git a/public/app/core/utils/errors.ts b/public/app/core/utils/errors.ts
index 3f6f1cfbc8d..afdf5270ade 100644
--- a/public/app/core/utils/errors.ts
+++ b/public/app/core/utils/errors.ts
@@ -13,5 +13,5 @@ export function getMessageFromError(err: any): string | null {
}
}
- return null;
+ return err;
}
diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts
index 1f999da72a5..c59d646839f 100644
--- a/public/app/core/utils/file_export.ts
+++ b/public/app/core/utils/file_export.ts
@@ -88,18 +88,18 @@ export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAU
)
);
// process data
- seriesList = mergeSeriesByTime(seriesList);
+ const extendedDatapointsList = mergeSeriesByTime(seriesList);
// make text
- for (let i = 0; i < seriesList[0].datapoints.length; i += 1) {
- const timestamp = moment(seriesList[0].datapoints[i][POINT_TIME_INDEX]).format(dateTimeFormat);
+ for (let i = 0; i < extendedDatapointsList[0].length; i += 1) {
+ const timestamp = moment(extendedDatapointsList[0][i][POINT_TIME_INDEX]).format(dateTimeFormat);
text += formatRow(
[timestamp].concat(
- seriesList.map(series => {
- return series.datapoints[i][POINT_VALUE_INDEX];
+ extendedDatapointsList.map(datapoints => {
+ return datapoints[i][POINT_VALUE_INDEX];
})
),
- i < seriesList[0].datapoints.length - 1
+ i < extendedDatapointsList[0].length - 1
);
}
@@ -120,22 +120,23 @@ function mergeSeriesByTime(seriesList) {
}
timestamps = sortedUniq(timestamps.sort());
+ const result = [];
for (let i = 0; i < seriesList.length; i++) {
const seriesPoints = seriesList[i].datapoints;
const seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]);
- const extendedSeries = [];
- let pointIndex;
+ const extendedDatapoints = [];
for (let j = 0; j < timestamps.length; j++) {
- pointIndex = sortedIndexOf(seriesTimestamps, timestamps[j]);
+ const timestamp = timestamps[j];
+ const pointIndex = sortedIndexOf(seriesTimestamps, timestamp);
if (pointIndex !== -1) {
- extendedSeries.push(seriesPoints[pointIndex]);
+ extendedDatapoints.push(seriesPoints[pointIndex]);
} else {
- extendedSeries.push([null, timestamps[j]]);
+ extendedDatapoints.push([null, timestamp]);
}
}
- seriesList[i].datapoints = extendedSeries;
+ result.push(extendedDatapoints);
}
- return seriesList;
+ return result;
}
export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
diff --git a/public/app/features/admin/AdminEditOrgCtrl.ts b/public/app/features/admin/AdminEditOrgCtrl.ts
index 60514a86392..4ce0e2366f6 100644
--- a/public/app/features/admin/AdminEditOrgCtrl.ts
+++ b/public/app/features/admin/AdminEditOrgCtrl.ts
@@ -2,7 +2,7 @@ export default class AdminEditOrgCtrl {
/** @ngInject */
constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
$scope.init = () => {
- $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
+ $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
if ($routeParams.id) {
$scope.getOrg($routeParams.id);
diff --git a/public/app/features/admin/AdminEditUserCtrl.ts b/public/app/features/admin/AdminEditUserCtrl.ts
index bf72c1746aa..a5dcae52d50 100644
--- a/public/app/features/admin/AdminEditUserCtrl.ts
+++ b/public/app/features/admin/AdminEditUserCtrl.ts
@@ -6,7 +6,7 @@ export default class AdminEditUserCtrl {
$scope.user = {};
$scope.newOrg = { name: '', role: 'Editor' };
$scope.permissions = {};
- $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-users', 1);
+ $scope.navModel = navModelSrv.getNav('admin', 'global-users', 0);
$scope.init = () => {
if ($routeParams.id) {
diff --git a/public/app/features/admin/AdminListOrgsCtrl.ts b/public/app/features/admin/AdminListOrgsCtrl.ts
index 8783efc182c..d6ee4b33dbb 100644
--- a/public/app/features/admin/AdminListOrgsCtrl.ts
+++ b/public/app/features/admin/AdminListOrgsCtrl.ts
@@ -2,7 +2,7 @@ export default class AdminListOrgsCtrl {
/** @ngInject */
constructor($scope, backendSrv, navModelSrv) {
$scope.init = () => {
- $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
+ $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
$scope.getOrgs();
};
diff --git a/public/app/features/admin/AdminListUsersCtrl.ts b/public/app/features/admin/AdminListUsersCtrl.ts
index 1b5b27a4d66..5b5321d13ce 100644
--- a/public/app/features/admin/AdminListUsersCtrl.ts
+++ b/public/app/features/admin/AdminListUsersCtrl.ts
@@ -10,7 +10,7 @@ export default class AdminListUsersCtrl {
/** @ngInject */
constructor(private $scope, private backendSrv, navModelSrv) {
- this.navModel = navModelSrv.getNav('cfg', 'admin', 'global-users', 1);
+ this.navModel = navModelSrv.getNav('admin', 'global-users', 0);
this.query = '';
this.getUsers();
}
diff --git a/public/app/features/admin/StyleGuideCtrl.ts b/public/app/features/admin/StyleGuideCtrl.ts
index e38e1a3344d..6548aa09198 100644
--- a/public/app/features/admin/StyleGuideCtrl.ts
+++ b/public/app/features/admin/StyleGuideCtrl.ts
@@ -9,7 +9,7 @@ export default class StyleGuideCtrl {
/** @ngInject */
constructor(private $routeParams, private backendSrv, navModelSrv) {
- this.navModel = navModelSrv.getNav('cfg', 'admin', 'styleguide', 1);
+ this.navModel = navModelSrv.getNav('admin', 'styleguide', 0);
this.theme = config.bootData.user.lightTheme ? 'light' : 'dark';
}
diff --git a/public/app/features/admin/index.ts b/public/app/features/admin/index.ts
index 7d06155b6f8..fecc04bc410 100644
--- a/public/app/features/admin/index.ts
+++ b/public/app/features/admin/index.ts
@@ -11,7 +11,7 @@ class AdminSettingsCtrl {
/** @ngInject */
constructor($scope, backendSrv, navModelSrv) {
- this.navModel = navModelSrv.getNav('cfg', 'admin', 'server-settings', 1);
+ this.navModel = navModelSrv.getNav('admin', 'server-settings', 0);
backendSrv.get('/api/admin/settings').then(settings => {
$scope.settings = settings;
@@ -24,7 +24,7 @@ class AdminHomeCtrl {
/** @ngInject */
constructor(navModelSrv) {
- this.navModel = navModelSrv.getNav('cfg', 'admin', 1);
+ this.navModel = navModelSrv.getNav('admin', 0);
}
}
diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx
index 453c5d1f9ac..d2f22d7d010 100644
--- a/public/app/features/dashboard/components/DashNav/DashNav.tsx
+++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx
@@ -8,6 +8,7 @@ import { appEvents } from 'app/core/app_events';
import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
// Components
+import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper';
import { DashNavButton } from './DashNavButton';
import { Tooltip } from '@grafana/ui';
@@ -173,26 +174,28 @@ export class DashNav extends PureComponent {
{this.renderDashboardTitleSearchButton()}
{this.playlistSrv.isPlaying && (
-