Compare commits

...

35 Commits

Author SHA1 Message Date
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
38 changed files with 629 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.1",
"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 255", new(RawSqlMigration).
Sqlite("SELECT 0 WHERE 0;").
Postgres("ALTER TABLE user_auth ALTER COLUMN auth_id TYPE VARCHAR(255);").
Mysql("ALTER TABLE user_auth MODIFY auth_id VARCHAR(255);"))
}

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() {

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

@@ -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,8 @@ div.flot-text {
padding: $panel-padding;
height: calc(100% - 27px);
position: relative;
overflow: hidden;
// Fixes scrolling on mobile devices
overflow: auto;
}
.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