Compare commits

...

43 Commits

Author SHA1 Message Date
Daniel Lee
0871432854 Merge pull request #11955 from grafana/v5.1.3-patch-release
V5.1.3 patch release
2018-05-16 17:39:55 +02:00
Daniel Lee
ab0dc3f086 v5.1.3 2018-05-16 17:27:50 +02:00
Daniel Lee
871db9d5d4 legend: fixes Firefox/baron scroll bug
Compensates for Firefox scrollbar calculation error in the baron framework.
Offsetwidth and clientwidth are used to find the width of the scrollbar. In
the legend these differ by 9px and cause the scroll div to grow by 9px for
every refresh. This fix compensates with a negative margin-right in that
case.

Fixes #11830

(cherry picked from commit 50026fad2e)
2018-05-16 17:25:51 +02:00
Daniel Lee
18379c30bf scroll: temporary fix for double scrollbar issue
If #11939 is not merged in the patch release, then
this is a temporary fix for 5.1.3. It sets overflow to
hidden for larger screens and keeps the overflow set to
auto for mobiles and tablets.

Fixes #11937

(cherry picked from commit 567fec402e)
2018-05-16 17:25:40 +02:00
Marcus Efraimsson
c3c690e211 Merge pull request #11897 from grafana/cp-5.1.2
Cherry-picks for v5.1.2
2018-05-09 11:00:18 +02:00
Marcus Efraimsson
92a8b0871e 5.1.2 release 2018-05-09 10:50:04 +02:00
Dan Cech
a41e346a0b allow analytics.js to be cached, enable anonymizeIP setting
(cherry picked from commit ab589da534)
2018-05-09 10:47:29 +02:00
Marcus Efraimsson
57f2d98418 decrease length of auth_id column in user_auth table
certain mysql versions don't support having indices with a greater varchar length
than 190.

(cherry picked from commit 8ed79d614b)
2018-05-09 10:47:07 +02:00
Daniel Lee
dc11bf7029 Merge pull request #11844 from grafana/cp-5.1.1
Cherry-picks for v5.1.1
2018-05-07 13:50:10 +02:00
Marcus Efraimsson
1f9995d4d3 5.1.1 release 2018-05-07 09:31:55 +02:00
Alexander Zobnin
fe108270bb scroll: remove firefox scrollbars
(cherry picked from commit 83d599670d)
2018-05-07 09:29:49 +02:00
Alexander Zobnin
3daaf37d6a scroll: fix scrolling on mobile Chrome (#11710)
(cherry picked from commit 64283408ee)
2018-05-07 09:29:40 +02:00
Marcus Efraimsson
e4d9c886d7 increase length of auth_id column in user_auth table
(cherry picked from commit 3d9b7a5892)
2018-05-07 09:28:46 +02:00
Marcus Efraimsson
e8fa60793d revert renaming of unit key ppm
#11211 removed the unit key ppm in favor of conppm. A change which is not forward compatible.
This commit revert the unit key back to ppm.
Also adds some better error description if trying to use a unit which don't exists.
Fixes #11743

(cherry picked from commit 138c8c348e)
2018-05-07 09:28:06 +02:00
Daniel Lee
81ce890890 Merge pull request #11769 from grafana/v5.1.0-appveyor-fix
V5.1.0 appveyor fix
2018-04-27 17:57:56 +02:00
Daniel Lee
eabd3ba9c1 appveyor: uppercase the C drive in go path
Fixes #11758

(cherry picked from commit 7e2fb5e92e)
2018-04-27 17:44:03 +02:00
Daniel Lee
844bdc53a2 Merge pull request #11733 from grafana/cp-5.1.0
Cherry-picks for v5.1.0
2018-04-26 17:09:23 +02:00
Marcus Efraimsson
ebffcc21cf 5.1.0 release 2018-04-26 16:34:28 +02:00
Marcus Efraimsson
61de54be5a fix so that google analytics script are cached
(cherry picked from commit ddeba41638)
2018-04-26 16:32:35 +02:00
Marcus Efraimsson
08963b1414 prometheus: convert metric find query tests to jest
(cherry picked from commit f112e38266)
2018-04-26 16:31:26 +02:00
Marcus Efraimsson
70f4797a03 prometheus: fix variable query to fallback correctly to series query
Using a query of for example up or up{job=job1}

(cherry picked from commit 6687409efb)
2018-04-26 16:31:13 +02:00
David Kaltschmidt
9df8c4fe86 force GET for metadataRequests, w/ test
(cherry picked from commit 707700ac7d)
2018-04-26 16:30:48 +02:00
David Kaltschmidt
9149a0c655 Renamed helperRequest and removed positional args
From review feedback:

* s/helper/metadata
* combined positional args to _request into options dict
* metadataRequest reuses _request()
* moved consumption of this.httpMethod into _request, can be overwritten
 in options due to spread-after

(cherry picked from commit 006286ac05)
2018-04-26 16:30:38 +02:00
David Kaltschmidt
cb83ec8945 Add silent option to backend requests
* When set to `true`, the `silent` option for backend_srv requests
 suppresses all event emitters that are triggered when the response is
received.
* Added `helperRequest()` to the Prometheus datasource to support
 requests that are not triggered by the user, e.g., for tab completion.
`helperRequest()` sets the `silent` option.
* Migrated all non-timeseries queries of the Prometheus datasource to
 use `helperRequest()`.

Fixes #11673

(cherry picked from commit 53817b7429)
2018-04-26 16:30:26 +02:00
Patrick O'Carroll
cadca93d93 removed height 100% from panel-container to fix ie11 panel edit mode
(cherry picked from commit 6836268f3e)
2018-04-26 15:00:48 +02:00
Patrick O'Carroll
fa1c1274db replaced border hack carot with fontawesome carot fixes #11677
(cherry picked from commit 99aa9a46bc)
2018-04-26 15:00:29 +02:00
Marcus Efraimsson
b58dc6cd49 mssql: fix value columns conversion to float when using timeseries query
(cherry picked from commit 1452634a2a)
2018-04-26 15:00:05 +02:00
Marcus Efraimsson
66938e80c9 postgres: fix value columns conversion to float when using timeseries query
(cherry picked from commit cf43007531)
2018-04-26 14:59:55 +02:00
Marcus Efraimsson
ac22f85d37 mysql: fix value columns conversion to float when using timeseries query
(cherry picked from commit 346577b664)
2018-04-26 14:59:46 +02:00
Marcus Efraimsson
3147bcccdd sql datasource: extract common logic for converting value column to float
(cherry picked from commit 76bd2aea44)
2018-04-26 14:59:37 +02:00
Patrick O'Carroll
cb8d436ea3 changed test name and dashboardMock code
(cherry picked from commit 38a4a2dc60)
2018-04-26 14:59:09 +02:00
Patrick O'Carroll
8e7147a7da fixed test
(cherry picked from commit 1446f54447)
2018-04-26 14:58:59 +02:00
Patrick O'Carroll
f72c4bc0e0 removed import config
(cherry picked from commit 45e6d9fcc4)
2018-04-26 14:58:46 +02:00
Patrick O'Carroll
06d52adf4e fixed so user who can edit dashboard can edit row, fixes #11466
(cherry picked from commit 3eaaa5d32d)
2018-04-26 14:58:34 +02:00
Leonard Gram
790fd99676 Fixes signing of packages.
Signing was failing as the builds
were expected to run as ubuntu but
is run as root.

Closes #11686

(cherry picked from commit 3a48ea8dde)
2018-04-26 14:57:57 +02:00
Marcus Efraimsson
3908571baf db: fix failing user auth tests for postgres
(cherry picked from commit d14ac54af6)
2018-04-26 14:52:52 +02:00
Patrick O'Carroll
0e8f05e6d5 added pointer to show more, reset values on new query
(cherry picked from commit a40314022b)
2018-04-26 14:52:20 +02:00
Patrick O'Carroll
111839bdcc added button to show more preview values for variables, button runs a function that increases options limit, fixes #11508
(cherry picked from commit c2cc77fa08)
2018-04-26 14:51:49 +02:00
Marcus Efraimsson
a46d0204d9 use inherited property from api when rendering permissions
(cherry picked from commit 079346917f)
2018-04-26 14:51:27 +02:00
Marcus Efraimsson
3e93fd1372 return inherited property for permissions
(cherry picked from commit d86ed679b1)
2018-04-26 14:51:13 +02:00
bergquist
a07f525686 build: fixes build script for releases 2018-04-20 12:47:56 +02:00
Daniel Lee
6a2d9e21dc Merge pull request #11640 from grafana/11637-dropdown-styling
removed padding and moved carrot
(cherry picked from commit c2064781a0)
2018-04-20 11:22:54 +02:00
bergquist
9dfc53d5a4 release 5.1.0-beta1 2018-04-20 11:01:55 +02:00
39 changed files with 642 additions and 335 deletions

View File

@@ -117,7 +117,7 @@ jobs:
- image: circleci/python:2.7-stretch
steps:
- attach_workspace:
at: dist
at: .
- run:
name: install awscli
command: 'sudo pip install awscli'

View File

@@ -6,7 +6,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
environment:
nodejs_version: "6"
GOPATH: c:\gopath
GOPATH: C:\gopath
GOVERSION: 1.10
install:

View File

@@ -4,7 +4,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "5.1.0-pre1",
"version": "5.1.3",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"

View File

@@ -69,6 +69,7 @@ type DashboardAclInfoDTO struct {
Slug string `json:"slug"`
IsFolder bool `json:"isFolder"`
Url string `json:"url"`
Inherited bool `json:"inherited"`
}
func (dto *DashboardAclInfoDTO) hasSameRoleAs(other *DashboardAclInfoDTO) bool {

View File

@@ -154,12 +154,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss
// validate overridden permissions to be higher
for _, a := range acl {
for _, existingPerm := range existingPermissions {
// handle default permissions
if existingPerm.DashboardId == -1 {
existingPerm.DashboardId = g.dashId
}
if a.DashboardId == existingPerm.DashboardId {
if !existingPerm.Inherited {
continue
}
@@ -187,13 +182,6 @@ func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
return nil, err
}
for _, a := range query.Result {
// handle default permissions
if a.DashboardId == -1 {
a.DashboardId = g.dashId
}
}
g.acl = query.Result
return g.acl, nil
}

View File

@@ -217,13 +217,13 @@ func (sc *scenarioContext) parentFolderPermissionScenario(pt permissionType, per
switch pt {
case USER:
folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, UserId: userID, Permission: permission}}
folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, UserId: userID, Permission: permission, Inherited: true}}
case TEAM:
folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, TeamId: teamID, Permission: permission}}
folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, TeamId: teamID, Permission: permission, Inherited: true}}
case EDITOR:
folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &editorRole, Permission: permission}}
folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &editorRole, Permission: permission, Inherited: true}}
case VIEWER:
folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &viewerRole, Permission: permission}}
folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &viewerRole, Permission: permission, Inherited: true}}
}
permissionScenario(fmt.Sprintf("and parent folder has %s with permission to %s", pt.String(), permission.String()), childDashboardID, sc, folderPermissionList, func(sc *scenarioContext) {

View File

@@ -67,7 +67,8 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
'' as title,
'' as slug,
'' as uid,` +
falseStr + ` AS is_folder
falseStr + ` AS is_folder,` +
falseStr + ` AS inherited
FROM dashboard_acl as da
WHERE da.dashboard_id = -1`
query.Result = make([]*m.DashboardAclInfoDTO, 0)
@@ -94,7 +95,8 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
d.title,
d.slug,
d.uid,
d.is_folder
d.is_folder,
CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + dialect.BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited
FROM dashboard as d
LEFT JOIN dashboard folder on folder.id = d.folder_id
LEFT JOIN dashboard_acl AS da ON

View File

@@ -26,6 +26,22 @@ func TestDashboardAclDataAccess(t *testing.T) {
})
Convey("Given dashboard folder with default permissions", func() {
Convey("When reading folder acl should include default acl", func() {
query := m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err := GetDashboardAclInfoList(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeFalse)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeFalse)
})
Convey("When reading dashboard acl should include acl for parent folder", func() {
query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1}
@@ -36,8 +52,10 @@ func TestDashboardAclDataAccess(t *testing.T) {
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeTrue)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeTrue)
})
})
@@ -94,7 +112,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(query.Result[0].Inherited, ShouldBeTrue)
So(query.Result[1].DashboardId, ShouldEqual, childDash.Id)
So(query.Result[1].Inherited, ShouldBeFalse)
})
})
})
@@ -118,9 +138,12 @@ func TestDashboardAclDataAccess(t *testing.T) {
So(len(query.Result), ShouldEqual, 3)
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeTrue)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeTrue)
So(query.Result[2].DashboardId, ShouldEqual, childDash.Id)
So(query.Result[2].Inherited, ShouldBeFalse)
})
})
@@ -209,8 +232,10 @@ func TestDashboardAclDataAccess(t *testing.T) {
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeFalse)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeFalse)
})
})
})

View File

@@ -21,4 +21,9 @@ func addUserAuthMigrations(mg *Migrator) {
mg.AddMigration("create user auth table", NewAddTableMigration(userAuthV1))
// add indices
addTableIndicesMigrations(mg, "v1", userAuthV1)
mg.AddMigration("alter user_auth.auth_id to length 190", new(RawSqlMigration).
Sqlite("SELECT 0 WHERE 0;").
Postgres("ALTER TABLE user_auth ALTER COLUMN auth_id TYPE VARCHAR(190);").
Mysql("ALTER TABLE user_auth MODIFY auth_id VARCHAR(190);"))
}

View File

@@ -32,7 +32,7 @@ func TestUserAuth(t *testing.T) {
So(err, ShouldBeNil)
_, err = x.Exec("DELETE FROM org WHERE 1=1")
So(err, ShouldBeNil)
_, err = x.Exec("DELETE FROM user WHERE 1=1")
_, err = x.Exec("DELETE FROM " + dialect.Quote("user") + " WHERE 1=1")
So(err, ShouldBeNil)
_, err = x.Exec("DELETE FROM user_auth WHERE 1=1")
So(err, ShouldBeNil)
@@ -117,7 +117,7 @@ func TestUserAuth(t *testing.T) {
So(query.Result.Login, ShouldEqual, "loginuser1")
// remove user
_, err = x.Exec("DELETE FROM user WHERE id=?", query.Result.Id)
_, err = x.Exec("DELETE FROM "+dialect.Quote("user")+" WHERE id=?", query.Result.Id)
So(err, ShouldBeNil)
// get via user_auth for deleted user

View File

@@ -256,16 +256,10 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
continue
}
switch columnValue := values[i].(type) {
case int64:
value = null.FloatFrom(float64(columnValue))
case float64:
value = null.FloatFrom(columnValue)
case nil:
value.Valid = false
default:
return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue)
if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
return err
}
if metricIndex == -1 {
metric = col
}

View File

@@ -374,12 +374,12 @@ func TestMSSQL(t *testing.T) {
_, err = sess.InsertMulti(series)
So(err, ShouldBeNil)
Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT TOP 1 timeInt64 as time, valueOne FROM metric_values ORDER BY time`,
"rawSql": `SELECT TOP 1 timeInt64 as time, timeInt64 FROM metric_values ORDER BY time`,
"format": "time_series",
}),
RefId: "A",
@@ -396,12 +396,12 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT TOP 1 timeInt64Nullable as time, valueOne FROM metric_values ORDER BY time`,
"rawSql": `SELECT TOP 1 timeInt64Nullable as time, timeInt64Nullable FROM metric_values ORDER BY time`,
"format": "time_series",
}),
RefId: "A",
@@ -418,12 +418,12 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float64) as time column and value column (float64) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT TOP 1 timeFloat64 as time, valueOne FROM metric_values ORDER BY time`,
"rawSql": `SELECT TOP 1 timeFloat64 as time, timeFloat64 FROM metric_values ORDER BY time`,
"format": "time_series",
}),
RefId: "A",
@@ -440,12 +440,12 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT TOP 1 timeFloat64Nullable as time, valueOne FROM metric_values ORDER BY time`,
"rawSql": `SELECT TOP 1 timeFloat64Nullable as time, timeFloat64Nullable FROM metric_values ORDER BY time`,
"format": "time_series",
}),
RefId: "A",
@@ -462,12 +462,12 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int32) as time column and value column (int32) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT TOP 1 timeInt32 as time, valueOne FROM metric_values ORDER BY time`,
"rawSql": `SELECT TOP 1 timeInt32 as time, timeInt32 FROM metric_values ORDER BY time`,
"format": "time_series",
}),
RefId: "A",
@@ -484,12 +484,12 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT TOP 1 timeInt32Nullable as time, valueOne FROM metric_values ORDER BY time`,
"rawSql": `SELECT TOP 1 timeInt32Nullable as time, timeInt32Nullable FROM metric_values ORDER BY time`,
"format": "time_series",
}),
RefId: "A",
@@ -506,12 +506,12 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float32) as time column and value column (float32) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT TOP 1 timeFloat32 as time, valueOne FROM metric_values ORDER BY time`,
"rawSql": `SELECT TOP 1 timeFloat32 as time, timeFloat32 FROM metric_values ORDER BY time`,
"format": "time_series",
}),
RefId: "A",
@@ -528,12 +528,12 @@ func TestMSSQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
})
Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT TOP 1 timeFloat32Nullable as time, valueOne FROM metric_values ORDER BY time`,
"rawSql": `SELECT TOP 1 timeFloat32Nullable as time, timeFloat32Nullable FROM metric_values ORDER BY time`,
"format": "time_series",
}),
RefId: "A",

View File

@@ -265,16 +265,10 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
continue
}
switch columnValue := values[i].(type) {
case int64:
value = null.FloatFrom(float64(columnValue))
case float64:
value = null.FloatFrom(columnValue)
case nil:
value.Valid = false
default:
return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue)
if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
return err
}
if metricIndex == -1 {
metric = col
}

View File

@@ -420,12 +420,12 @@ func TestMySQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT timeInt64 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT timeInt64 as time, timeInt64 FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -442,12 +442,12 @@ func TestMySQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT timeInt64Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT timeInt64Nullable as time, timeInt64Nullable FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -464,12 +464,12 @@ func TestMySQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float64) as time column and value column (float64) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT timeFloat64 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT timeFloat64 as time, timeFloat64 FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -486,12 +486,12 @@ func TestMySQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT timeFloat64Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT timeFloat64Nullable as time, timeFloat64Nullable FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -508,12 +508,12 @@ func TestMySQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() {
FocusConvey("When doing a metric query using epoch (int32) as time column and value column (int32) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT timeInt32 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT timeInt32 as time, timeInt32 FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -530,12 +530,12 @@ func TestMySQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT timeInt32Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT timeInt32Nullable as time, timeInt32Nullable FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -552,12 +552,12 @@ func TestMySQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float32) as time column and value column (float32) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT timeFloat32 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT timeFloat32 as time, timeFloat32 FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -574,12 +574,12 @@ func TestMySQL(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
})
Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT timeFloat32Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT timeFloat32Nullable as time, timeFloat32Nullable FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",

View File

@@ -245,16 +245,10 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
continue
}
switch columnValue := values[i].(type) {
case int64:
value = null.FloatFrom(float64(columnValue))
case float64:
value = null.FloatFrom(columnValue)
case nil:
value.Valid = false
default:
return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue)
if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil {
return err
}
if metricIndex == -1 {
metric = col
}

View File

@@ -353,12 +353,12 @@ func TestPostgres(t *testing.T) {
_, err = sess.InsertMulti(series)
So(err, ShouldBeNil)
Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT "timeInt64" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT "timeInt64" as time, "timeInt64" FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -375,12 +375,12 @@ func TestPostgres(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT "timeInt64Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT "timeInt64Nullable" as time, "timeInt64Nullable" FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -397,12 +397,12 @@ func TestPostgres(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float64) as time column and value column (float64) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT "timeFloat64" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT "timeFloat64" as time, "timeFloat64" FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -419,12 +419,12 @@ func TestPostgres(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT "timeFloat64Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT "timeFloat64Nullable" as time, "timeFloat64Nullable" FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -441,12 +441,12 @@ func TestPostgres(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int32) as time column and value column (int32) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT "timeInt32" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT "timeInt32" as time, "timeInt32" FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -463,12 +463,12 @@ func TestPostgres(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT "timeInt32Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT "timeInt32Nullable" as time, "timeInt32Nullable" FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -485,12 +485,12 @@ func TestPostgres(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
})
Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float32) as time column and value column (float32) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT "timeFloat32" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT "timeFloat32" as time, "timeFloat32" FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",
@@ -507,12 +507,12 @@ func TestPostgres(t *testing.T) {
So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
})
Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() {
Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() {
query := &tsdb.TsdbQuery{
Queries: []*tsdb.Query{
{
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT "timeFloat32Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
"rawSql": `SELECT "timeFloat32Nullable" as time, "timeFloat32Nullable" FROM metric_values ORDER BY time LIMIT 1`,
"format": "time_series",
}),
RefId: "A",

View File

@@ -2,9 +2,12 @@ package tsdb
import (
"context"
"fmt"
"sync"
"time"
"github.com/grafana/grafana/pkg/components/null"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -185,3 +188,109 @@ func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) {
}
}
}
// ConvertSqlValueColumnToFloat converts timeseries value column to float.
func ConvertSqlValueColumnToFloat(columnName string, columnValue interface{}) (null.Float, error) {
var value null.Float
switch typedValue := columnValue.(type) {
case int:
value = null.FloatFrom(float64(typedValue))
case *int:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case int64:
value = null.FloatFrom(float64(typedValue))
case *int64:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case int32:
value = null.FloatFrom(float64(typedValue))
case *int32:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case int16:
value = null.FloatFrom(float64(typedValue))
case *int16:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case int8:
value = null.FloatFrom(float64(typedValue))
case *int8:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case uint:
value = null.FloatFrom(float64(typedValue))
case *uint:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case uint64:
value = null.FloatFrom(float64(typedValue))
case *uint64:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case uint32:
value = null.FloatFrom(float64(typedValue))
case *uint32:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case uint16:
value = null.FloatFrom(float64(typedValue))
case *uint16:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case uint8:
value = null.FloatFrom(float64(typedValue))
case *uint8:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case float64:
value = null.FloatFrom(typedValue)
case *float64:
value = null.FloatFromPtr(typedValue)
case float32:
value = null.FloatFrom(float64(typedValue))
case *float32:
if typedValue == nil {
value.Valid = false
} else {
value = null.FloatFrom(float64(*typedValue))
}
case nil:
value.Valid = false
default:
return null.NewFloat(0, false), fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", columnName, typedValue, typedValue)
}
return value, nil
}

View File

@@ -1,10 +1,11 @@
package tsdb
import (
"fmt"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/null"
. "github.com/smartystreets/goconvey/convey"
)
@@ -156,8 +157,6 @@ func TestSqlEngine(t *testing.T) {
So(fixtures[1].(float64), ShouldEqual, tMilliseconds)
So(fixtures[2].(float64), ShouldEqual, tMilliseconds)
So(fixtures[3].(float64), ShouldEqual, tMilliseconds)
fmt.Println(fixtures[4].(float64))
fmt.Println(tMilliseconds)
So(fixtures[4].(float64), ShouldEqual, tMilliseconds)
So(fixtures[5].(float64), ShouldEqual, tMilliseconds)
So(fixtures[6], ShouldBeNil)
@@ -183,5 +182,101 @@ func TestSqlEngine(t *testing.T) {
So(fixtures[2], ShouldBeNil)
})
})
Convey("Given row with value columns", func() {
intValue := 1
int64Value := int64(1)
int32Value := int32(1)
int16Value := int16(1)
int8Value := int8(1)
float64Value := float64(1)
float32Value := float32(1)
uintValue := uint(1)
uint64Value := uint64(1)
uint32Value := uint32(1)
uint16Value := uint16(1)
uint8Value := uint8(1)
fixtures := make([]interface{}, 24)
fixtures[0] = intValue
fixtures[1] = &intValue
fixtures[2] = int64Value
fixtures[3] = &int64Value
fixtures[4] = int32Value
fixtures[5] = &int32Value
fixtures[6] = int16Value
fixtures[7] = &int16Value
fixtures[8] = int8Value
fixtures[9] = &int8Value
fixtures[10] = float64Value
fixtures[11] = &float64Value
fixtures[12] = float32Value
fixtures[13] = &float32Value
fixtures[14] = uintValue
fixtures[15] = &uintValue
fixtures[16] = uint64Value
fixtures[17] = &uint64Value
fixtures[18] = uint32Value
fixtures[19] = &uint32Value
fixtures[20] = uint16Value
fixtures[21] = &uint16Value
fixtures[22] = uint8Value
fixtures[23] = &uint8Value
var intNilPointer *int
var int64NilPointer *int64
var int32NilPointer *int32
var int16NilPointer *int16
var int8NilPointer *int8
var float64NilPointer *float64
var float32NilPointer *float32
var uintNilPointer *uint
var uint64NilPointer *uint64
var uint32NilPointer *uint32
var uint16NilPointer *uint16
var uint8NilPointer *uint8
nilPointerFixtures := make([]interface{}, 12)
nilPointerFixtures[0] = intNilPointer
nilPointerFixtures[1] = int64NilPointer
nilPointerFixtures[2] = int32NilPointer
nilPointerFixtures[3] = int16NilPointer
nilPointerFixtures[4] = int8NilPointer
nilPointerFixtures[5] = float64NilPointer
nilPointerFixtures[6] = float32NilPointer
nilPointerFixtures[7] = uintNilPointer
nilPointerFixtures[8] = uint64NilPointer
nilPointerFixtures[9] = uint32NilPointer
nilPointerFixtures[10] = uint16NilPointer
nilPointerFixtures[11] = uint8NilPointer
Convey("When converting values to float should return expected value", func() {
for _, f := range fixtures {
value, _ := ConvertSqlValueColumnToFloat("col", f)
if !value.Valid {
t.Fatalf("Failed to convert %T value, expected a valid float value", f)
}
if value.Float64 != null.FloatFrom(1).Float64 {
t.Fatalf("Failed to convert %T value, expected a float value of 1.000, but got %v", f, value)
}
}
})
Convey("When converting nil pointer values to float should return expected value", func() {
for _, f := range nilPointerFixtures {
value, err := ConvertSqlValueColumnToFloat("col", f)
if err != nil {
t.Fatalf("Failed to convert %T value, expected a non nil error, but got %v", f, err)
}
if value.Valid {
t.Fatalf("Failed to convert %T value, expected an invalid float value", f)
}
}
})
})
})
}

View File

@@ -41,7 +41,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde
permissionChanged(itemIndex, permissionOption.value, permissionOption.label);
};
const inheritedFromRoot = item.dashboardId === -1 && folderInfo && folderInfo.id === 0;
const inheritedFromRoot = item.dashboardId === -1 && !item.inherited;
return (
<tr className={setClassNameHelper(item.inherited)}>

View File

@@ -7,7 +7,11 @@ export class Analytics {
constructor(private $rootScope, private $location) {}
gaInit() {
$.getScript('https://www.google-analytics.com/analytics.js'); // jQuery shortcut
$.ajax({
url: 'https://www.google-analytics.com/analytics.js',
dataType: 'script',
cache: true,
});
var ga = ((<any>window).ga =
(<any>window).ga ||
function() {
@@ -15,6 +19,7 @@ export class Analytics {
});
ga.l = +new Date();
ga('create', (<any>config).googleAnalyticsId, 'auto');
ga('set', 'anonymizeIp', true);
return ga;
}

View File

@@ -170,7 +170,9 @@ export class BackendSrv {
return this.$http(options)
.then(response => {
appEvents.emit('ds-request-response', response);
if (!options.silent) {
appEvents.emit('ds-request-response', response);
}
return response;
})
.catch(err => {
@@ -201,8 +203,9 @@ export class BackendSrv {
if (err.data && !err.data.message && _.isString(err.data.error)) {
err.data.message = err.data.error;
}
appEvents.emit('ds-request-error', err);
if (!options.silent) {
appEvents.emit('ds-request-error', err);
}
throw err;
})
.finally(() => {

View File

@@ -595,7 +595,7 @@ kbn.valueFormats.radr = kbn.formatBuilders.decimalSIPrefix('R');
kbn.valueFormats.radsvh = kbn.formatBuilders.decimalSIPrefix('Sv/h');
// Concentration
kbn.valueFormats.conppm = kbn.formatBuilders.fixedUnit('ppm');
kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm');
kbn.valueFormats.conppb = kbn.formatBuilders.fixedUnit('ppb');
kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m3');
kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm3');
@@ -1099,7 +1099,7 @@ kbn.getUnitFormats = function() {
{
text: 'concentration',
submenu: [
{ text: 'parts-per-million (ppm)', value: 'conppm' },
{ text: 'parts-per-million (ppm)', value: 'ppm' },
{ text: 'parts-per-billion (ppb)', value: 'conppb' },
{ text: 'nanogram per cubic metre (ng/m3)', value: 'conngm3' },
{ text: 'nanogram per normal cubic metre (ng/Nm3)', value: 'conngNm3' },

View File

@@ -4,7 +4,6 @@ import { PanelModel } from '../panel_model';
import { PanelContainer } from './PanelContainer';
import templateSrv from 'app/features/templating/template_srv';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
export interface DashboardRowProps {
panel: PanelModel;
@@ -95,7 +94,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
{title}
<span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span>
</a>
{config.bootData.user.orgRole !== 'Viewer' && (
{this.dashboard.meta.canEdit === true && (
<div className="dashboard-row__actions">
<a className="pointer" onClick={this.openSettings}>
<i className="fa fa-cog" />

View File

@@ -2,17 +2,15 @@ import React from 'react';
import { shallow } from 'enzyme';
import { DashboardRow } from '../dashgrid/DashboardRow';
import { PanelModel } from '../panel_model';
import config from '../../../core/config';
describe('DashboardRow', () => {
let wrapper, panel, getPanelContainer, dashboardMock;
beforeEach(() => {
dashboardMock = { toggleRow: jest.fn() };
config.bootData = {
user: {
orgRole: 'Admin',
dashboardMock = {
toggleRow: jest.fn(),
meta: {
canEdit: true,
},
};
@@ -41,8 +39,8 @@ describe('DashboardRow', () => {
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(2);
});
it('should have zero actions as viewer', () => {
config.bootData.user.orgRole = 'Viewer';
it('should have zero actions when cannot edit', () => {
dashboardMock.meta.canEdit = false;
panel = new PanelModel({ collapsed: false });
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);

View File

@@ -10,6 +10,7 @@ export class VariableEditorCtrl {
$scope.ctrl = {};
$scope.namePattern = /^(?!__).*$/;
$scope._ = _;
$scope.optionsLimit = 20;
$scope.refreshOptions = [
{ value: 0, text: 'Never' },
@@ -96,6 +97,7 @@ export class VariableEditorCtrl {
};
$scope.runQuery = function() {
$scope.optionsLimit = 20;
return variableSrv.updateOptions($scope.current).catch(err => {
if (err.data && err.data.message) {
err.message = err.data.message;
@@ -165,6 +167,10 @@ export class VariableEditorCtrl {
$scope.removeVariable = function(variable) {
variableSrv.removeVariable(variable);
};
$scope.showMoreOptions = function() {
$scope.optionsLimit += 20;
};
}
}

View File

@@ -280,11 +280,14 @@
</div>
<div class="gf-form-group" ng-show="current.options.length">
<h5>Preview of values (shows max 20)</h5>
<h5>Preview of values</h5>
<div class="gf-form-inline">
<div class="gf-form" ng-repeat="option in current.options | limitTo: 20">
<span class="gf-form-label">{{option.text}}</span>
</div>
<div class="gf-form" ng-repeat="option in current.options | limitTo: optionsLimit">
<span class="gf-form-label">{{option.text}}</span>
</div>
<div class="gf-form" ng-if= "current.options.length > optionsLimit">
<a class="gf-form-label btn-secondary" ng-click="showMoreOptions()">Show more</a>
</div>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import kbn from 'app/core/utils/kbn';
import * as dateMath from 'app/core/utils/datemath';
import PrometheusMetricFindQuery from './metric_find_query';
import { ResultTransformer } from './result_transformer';
import { BackendSrv } from 'app/core/services/backend_srv';
export function prometheusRegularEscape(value) {
return value.replace(/'/g, "\\\\'");
@@ -29,7 +30,7 @@ export class PrometheusDatasource {
resultTransformer: ResultTransformer;
/** @ngInject */
constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) {
constructor(instanceSettings, private $q, private backendSrv: BackendSrv, private templateSrv, private timeSrv) {
this.type = 'prometheus';
this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
this.name = instanceSettings.name;
@@ -43,13 +44,13 @@ export class PrometheusDatasource {
this.resultTransformer = new ResultTransformer(templateSrv);
}
_request(method, url, data?, requestId?) {
_request(url, data?, options?: any) {
var options: any = {
url: this.url + url,
method: method,
requestId: requestId,
method: this.httpMethod,
...options,
};
if (method === 'GET') {
if (options.method === 'GET') {
if (!_.isEmpty(data)) {
options.url =
options.url +
@@ -81,6 +82,11 @@ export class PrometheusDatasource {
return this.backendSrv.datasourceRequest(options);
}
// Use this for tab completion features, wont publish response to other components
metadataRequest(url) {
return this._request(url, null, { method: 'GET', silent: true });
}
interpolateQueryExpr(value, variable, defaultFormatFn) {
// if no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) {
@@ -206,7 +212,7 @@ export class PrometheusDatasource {
end: end,
step: query.step,
};
return this._request(this.httpMethod, url, data, query.requestId);
return this._request(url, data, { requestId: query.requestId });
}
performInstantQuery(query, time) {
@@ -215,7 +221,7 @@ export class PrometheusDatasource {
query: query.expr,
time: time,
};
return this._request(this.httpMethod, url, data, query.requestId);
return this._request(url, data, { requestId: query.requestId });
}
performSuggestQuery(query, cache = false) {
@@ -229,7 +235,7 @@ export class PrometheusDatasource {
);
}
return this._request('GET', url).then(result => {
return this.metadataRequest(url).then(result => {
this.metricsNameCache = {
data: result.data.data,
expire: Date.now() + 60 * 1000,
@@ -323,4 +329,8 @@ export class PrometheusDatasource {
}
return Math.ceil(date.valueOf() / 1000);
}
getOriginalMetricName(labelData) {
return this.resultTransformer.getOriginalMetricName(labelData);
}
}

View File

@@ -46,7 +46,7 @@ export default class PrometheusMetricFindQuery {
// return label values globally
url = '/api/v1/label/' + label + '/values';
return this.datasource._request('GET', url).then(function(result) {
return this.datasource.metadataRequest(url).then(function(result) {
return _.map(result.data.data, function(value) {
return { text: value };
});
@@ -56,7 +56,7 @@ export default class PrometheusMetricFindQuery {
var end = this.datasource.getPrometheusTime(this.range.to, true);
url = '/api/v1/series?match[]=' + encodeURIComponent(metric) + '&start=' + start + '&end=' + end;
return this.datasource._request('GET', url).then(function(result) {
return this.datasource.metadataRequest(url).then(function(result) {
var _labels = _.map(result.data.data, function(metric) {
return metric[label] || '';
}).filter(function(label) {
@@ -76,7 +76,7 @@ export default class PrometheusMetricFindQuery {
metricNameQuery(metricFilterPattern) {
var url = '/api/v1/label/__name__/values';
return this.datasource._request('GET', url).then(function(result) {
return this.datasource.metadataRequest(url).then(function(result) {
return _.chain(result.data.data)
.filter(function(metricName) {
var r = new RegExp(metricFilterPattern);
@@ -120,8 +120,8 @@ export default class PrometheusMetricFindQuery {
var url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end;
var self = this;
return this.datasource._request('GET', url).then(function(result) {
return _.map(result.data.data, function(metric) {
return this.datasource.metadataRequest(url).then(function(result) {
return _.map(result.data.data, metric => {
return {
text: self.datasource.getOriginalMetricName(metric),
expandable: true,

View File

@@ -14,6 +14,7 @@ describe('PrometheusDatasource', () => {
};
ctx.backendSrvMock = {};
ctx.templateSrvMock = {
replace: a => a,
};
@@ -23,6 +24,45 @@ describe('PrometheusDatasource', () => {
ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
});
describe('Datasource metadata requests', () => {
it('should perform a GET request with the default config', () => {
ctx.backendSrvMock.datasourceRequest = jest.fn();
ctx.ds.metadataRequest('/foo');
expect(ctx.backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
expect(ctx.backendSrvMock.datasourceRequest.mock.calls[0][0].method).toBe('GET');
});
it('should still perform a GET request with the DS HTTP method set to POST', () => {
ctx.backendSrvMock.datasourceRequest = jest.fn();
const postSettings = _.cloneDeep(instanceSettings);
postSettings.jsonData.httpMethod = 'POST';
const ds = new PrometheusDatasource(postSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
ds.metadataRequest('/foo');
expect(ctx.backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
expect(ctx.backendSrvMock.datasourceRequest.mock.calls[0][0].method).toBe('GET');
});
});
describe('When performing performSuggestQuery', () => {
it('should cache response', async () => {
ctx.backendSrvMock.datasourceRequest.mockReturnValue(
Promise.resolve({
status: 'success',
data: { data: ['value1', 'value2', 'value3'] },
})
);
let results = await ctx.ds.performSuggestQuery('value', true);
expect(results).toHaveLength(3);
ctx.backendSrvMock.datasourceRequest.mockReset();
results = await ctx.ds.performSuggestQuery('value', true);
expect(results).toHaveLength(3);
});
});
describe('When converting prometheus histogram to heatmap format', () => {
beforeEach(() => {
ctx.query = {

View File

@@ -0,0 +1,205 @@
import moment from 'moment';
import { PrometheusDatasource } from '../datasource';
import PrometheusMetricFindQuery from '../metric_find_query';
import q from 'q';
describe('PrometheusMetricFindQuery', function() {
let instanceSettings = {
url: 'proxied',
directUrl: 'direct',
user: 'test',
password: 'mupp',
jsonData: { httpMethod: 'GET' },
};
const raw = {
from: moment.utc('2018-04-25 10:00'),
to: moment.utc('2018-04-25 11:00'),
};
let ctx: any = {
backendSrvMock: {
datasourceRequest: jest.fn(() => Promise.resolve({})),
},
templateSrvMock: {
replace: a => a,
},
timeSrvMock: {
timeRange: () => ({
from: raw.from,
to: raw.to,
raw: raw,
}),
},
};
ctx.setupMetricFindQuery = (data: any) => {
ctx.backendSrvMock.datasourceRequest.mockReturnValue(Promise.resolve({ status: 'success', data: data.response }));
return new PrometheusMetricFindQuery(ctx.ds, data.query, ctx.timeSrvMock);
};
beforeEach(() => {
ctx.backendSrvMock.datasourceRequest.mockReset();
ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
});
describe('When performing metricFindQuery', () => {
it('label_values(resource) should generate label search query', async () => {
const query = ctx.setupMetricFindQuery({
query: 'label_values(resource)',
response: {
data: ['value1', 'value2', 'value3'],
},
});
const results = await query.process();
expect(results).toHaveLength(3);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({
method: 'GET',
url: 'proxied/api/v1/label/resource/values',
silent: true,
});
});
it('label_values(metric, resource) should generate series query with correct time', async () => {
const query = ctx.setupMetricFindQuery({
query: 'label_values(metric, resource)',
response: {
data: [
{ __name__: 'metric', resource: 'value1' },
{ __name__: 'metric', resource: 'value2' },
{ __name__: 'metric', resource: 'value3' },
],
},
});
const results = await query.process();
expect(results).toHaveLength(3);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({
method: 'GET',
url: `proxied/api/v1/series?match[]=metric&start=${raw.from.unix()}&end=${raw.to.unix()}`,
silent: true,
});
});
it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query with correct time', async () => {
const query = ctx.setupMetricFindQuery({
query: 'label_values(metric{label1="foo", label2="bar", label3="baz"}, resource)',
response: {
data: [
{ __name__: 'metric', resource: 'value1' },
{ __name__: 'metric', resource: 'value2' },
{ __name__: 'metric', resource: 'value3' },
],
},
});
const results = await query.process();
expect(results).toHaveLength(3);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({
method: 'GET',
url: `proxied/api/v1/series?match[]=${encodeURIComponent(
'metric{label1="foo", label2="bar", label3="baz"}'
)}&start=${raw.from.unix()}&end=${raw.to.unix()}`,
silent: true,
});
});
it('label_values(metric, resource) result should not contain empty string', async () => {
const query = ctx.setupMetricFindQuery({
query: 'label_values(metric, resource)',
response: {
data: [
{ __name__: 'metric', resource: 'value1' },
{ __name__: 'metric', resource: 'value2' },
{ __name__: 'metric', resource: '' },
],
},
});
const results = await query.process();
expect(results).toHaveLength(2);
expect(results[0].text).toBe('value1');
expect(results[1].text).toBe('value2');
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({
method: 'GET',
url: `proxied/api/v1/series?match[]=metric&start=${raw.from.unix()}&end=${raw.to.unix()}`,
silent: true,
});
});
it('metrics(metric.*) should generate metric name query', async () => {
const query = ctx.setupMetricFindQuery({
query: 'metrics(metric.*)',
response: {
data: ['metric1', 'metric2', 'metric3', 'nomatch'],
},
});
const results = await query.process();
expect(results).toHaveLength(3);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({
method: 'GET',
url: 'proxied/api/v1/label/__name__/values',
silent: true,
});
});
it('query_result(metric) should generate metric name query', async () => {
const query = ctx.setupMetricFindQuery({
query: 'query_result(metric)',
response: {
data: {
resultType: 'vector',
result: [
{
metric: { __name__: 'metric', job: 'testjob' },
value: [1443454528.0, '3846'],
},
],
},
},
});
const results = await query.process();
expect(results).toHaveLength(1);
expect(results[0].text).toBe('metric{job="testjob"} 3846 1443454528000');
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({
method: 'GET',
url: `proxied/api/v1/query?query=metric&time=${raw.to.unix()}`,
requestId: undefined,
});
});
it('up{job="job1"} should fallback using generate series query', async () => {
const query = ctx.setupMetricFindQuery({
query: 'up{job="job1"}',
response: {
data: [
{ __name__: 'up', instance: '127.0.0.1:1234', job: 'job1' },
{ __name__: 'up', instance: '127.0.0.1:5678', job: 'job1' },
{ __name__: 'up', instance: '127.0.0.1:9102', job: 'job1' },
],
},
});
const results = await query.process();
expect(results).toHaveLength(3);
expect(results[0].text).toBe('up{instance="127.0.0.1:1234",job="job1"}');
expect(results[1].text).toBe('up{instance="127.0.0.1:5678",job="job1"}');
expect(results[2].text).toBe('up{instance="127.0.0.1:9102",job="job1"}');
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1);
expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({
method: 'GET',
url: `proxied/api/v1/series?match[]=${encodeURIComponent(
'up{job="job1"}'
)}&start=${raw.from.unix()}&end=${raw.to.unix()}`,
silent: true,
});
});
});
});

View File

@@ -1,181 +0,0 @@
import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
import moment from 'moment';
import helpers from 'test/specs/helpers';
import { PrometheusDatasource } from '../datasource';
import PrometheusMetricFindQuery from '../metric_find_query';
describe('PrometheusMetricFindQuery', function() {
var ctx = new helpers.ServiceTestContext();
var instanceSettings = {
url: 'proxied',
directUrl: 'direct',
user: 'test',
password: 'mupp',
jsonData: { httpMethod: 'GET' },
};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(
angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
ctx.$q = $q;
ctx.$httpBackend = $httpBackend;
ctx.$rootScope = $rootScope;
ctx.ds = $injector.instantiate(PrometheusDatasource, {
instanceSettings: instanceSettings,
});
$httpBackend.when('GET', /\.html$/).respond('');
})
);
describe('When performing metricFindQuery', function() {
var results;
var response;
it('label_values(resource) should generate label search query', function() {
response = {
status: 'success',
data: ['value1', 'value2', 'value3'],
};
ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/resource/values').respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(resource)', ctx.timeSrv);
pm.process().then(function(data) {
results = data;
});
ctx.$httpBackend.flush();
ctx.$rootScope.$apply();
expect(results.length).to.be(3);
});
it('label_values(metric, resource) should generate series query', function() {
response = {
status: 'success',
data: [
{ __name__: 'metric', resource: 'value1' },
{ __name__: 'metric', resource: 'value2' },
{ __name__: 'metric', resource: 'value3' },
],
};
ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv);
pm.process().then(function(data) {
results = data;
});
ctx.$httpBackend.flush();
ctx.$rootScope.$apply();
expect(results.length).to.be(3);
});
it('label_values(metric, resource) should pass correct time', function() {
ctx.timeSrv.setTime({
from: moment.utc('2011-01-01'),
to: moment.utc('2015-01-01'),
});
ctx.$httpBackend
.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=1293840000&end=1420070400/)
.respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv);
pm.process().then(function(data) {
results = data;
});
ctx.$httpBackend.flush();
ctx.$rootScope.$apply();
});
it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query', function() {
response = {
status: 'success',
data: [
{ __name__: 'metric', resource: 'value1' },
{ __name__: 'metric', resource: 'value2' },
{ __name__: 'metric', resource: 'value3' },
],
};
ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv);
pm.process().then(function(data) {
results = data;
});
ctx.$httpBackend.flush();
ctx.$rootScope.$apply();
expect(results.length).to.be(3);
});
it('label_values(metric, resource) result should not contain empty string', function() {
response = {
status: 'success',
data: [
{ __name__: 'metric', resource: 'value1' },
{ __name__: 'metric', resource: 'value2' },
{ __name__: 'metric', resource: '' },
],
};
ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv);
pm.process().then(function(data) {
results = data;
});
ctx.$httpBackend.flush();
ctx.$rootScope.$apply();
expect(results.length).to.be(2);
expect(results[0].text).to.be('value1');
expect(results[1].text).to.be('value2');
});
it('metrics(metric.*) should generate metric name query', function() {
response = {
status: 'success',
data: ['metric1', 'metric2', 'metric3', 'nomatch'],
};
ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'metrics(metric.*)', ctx.timeSrv);
pm.process().then(function(data) {
results = data;
});
ctx.$httpBackend.flush();
ctx.$rootScope.$apply();
expect(results.length).to.be(3);
});
it('query_result(metric) should generate metric name query', function() {
response = {
status: 'success',
data: {
resultType: 'vector',
result: [
{
metric: { __name__: 'metric', job: 'testjob' },
value: [1443454528.0, '3846'],
},
],
},
};
ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/query\?query=metric&time=.*/).respond(response);
var pm = new PrometheusMetricFindQuery(ctx.ds, 'query_result(metric)', ctx.timeSrv);
pm.process().then(function(data) {
results = data;
});
ctx.$httpBackend.flush();
ctx.$rootScope.$apply();
expect(results.length).to.be(1);
expect(results[0].text).to.be('metric{job="testjob"} 3846 1443454528000');
});
});
describe('When performing performSuggestQuery', function() {
var results;
var response;
it('cache response', function() {
response = {
status: 'success',
data: ['value1', 'value2', 'value3'],
};
ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response);
ctx.ds.performSuggestQuery('value', true).then(function(data) {
results = data;
});
ctx.$httpBackend.flush();
ctx.$rootScope.$apply();
expect(results.length).to.be(3);
ctx.ds.performSuggestQuery('value', true).then(function(data) {
// get from cache, no need to flush
results = data;
expect(results.length).to.be(3);
});
});
});
});

View File

@@ -634,6 +634,9 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
function configureAxisMode(axis, format) {
axis.tickFormatter = function(val, axis) {
if (!kbn.valueFormats[format]) {
throw new Error(`Unit '${format}' is not supported`);
}
return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
};
}

View File

@@ -287,6 +287,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
destroyScrollbar();
legendScrollbar = baron(scrollbarParams);
}
// #11830 - compensates for Firefox scrollbar calculation error in the baron framework
scroller[0].style.marginRight = '-' + (scroller[0].offsetWidth - scroller[0].clientWidth) + 'px';
legendScrollbar.scroll();
}

View File

@@ -16,6 +16,7 @@ describe('PermissionsStore', () => {
permissionName: 'View',
teamId: 1,
team: 'MyTestTeam',
inherited: true,
},
{
id: 5,

View File

@@ -224,8 +224,6 @@ const prepareServerResponse = (response, dashboardId: number, isFolder: boolean,
};
const prepareItem = (item, dashboardId: number, isFolder: boolean, isInRoot: boolean) => {
item.inherited = !isFolder && !isInRoot && dashboardId !== item.dashboardId;
item.sortRank = 0;
if (item.userId > 0) {
item.name = item.userLogin;

View File

@@ -256,17 +256,15 @@
// Caret to indicate there is a submenu
.dropdown-submenu > a::after {
display: block;
content: ' ';
float: right;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 5px 0 5px 5px;
border-left-color: $text-color-weak;
margin-top: 5px;
margin-right: -4px;
position: absolute;
top: 35%;
right: $input-padding-x;
background-color: transparent;
color: $text-color-weak;
font: normal normal normal $font-size-sm/1 FontAwesome;
content: '\f0da';
pointer-events: none;
font-size: 11px;
}
.dropdown-submenu:hover > a::after {
border-left-color: $dropdownLinkColorHover;

View File

@@ -341,19 +341,19 @@ $input-border: 1px solid $input-border-color;
margin-right: $gf-form-margin;
position: relative;
background-color: $input-bg;
padding-right: $input-padding-x;
border: $input-border;
border-radius: $input-border-radius;
&::after {
position: absolute;
top: 35%;
right: $input-padding-x/2;
right: $input-padding-x;
background-color: transparent;
color: $input-color;
font: normal normal normal $font-size-sm/1 FontAwesome;
content: '\f0d7';
pointer-events: none;
font-size: 11px;
}
.gf-form-input {

View File

@@ -33,7 +33,6 @@ div.flot-text {
border: $panel-border;
position: relative;
border-radius: 3px;
height: 100%;
&.panel-transparent {
background-color: transparent;
@@ -45,7 +44,16 @@ div.flot-text {
padding: $panel-padding;
height: calc(100% - 27px);
position: relative;
overflow: hidden;
// Fixes scrolling on mobile devices
overflow: auto;
}
// For larger screens, set back to hidden to avoid double scroll bars
@include media-breakpoint-up(md) {
.panel-content {
overflow: hidden;
}
}
.panel-title-container {

View File

@@ -1,4 +1,4 @@
%_signature gpg
%_gpg_path /home/ubuntu/.gnupg
%_gpg_path /root/.gnupg
%_gpg_name Grafana
%_gpgbin /usr/bin/gpg