Merge branch 'master' into docs-5.1

This commit is contained in:
Marcus Efraimsson
2018-04-24 16:17:15 +02:00
46 changed files with 217 additions and 117 deletions

View File

@@ -20,18 +20,20 @@ jobs:
gometalinter:
docker:
- image: circleci/golang:1.10
environment:
# we need CGO because of go-sqlite3
CGO_ENABLED: 1
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
- run: 'go get -u gopkg.in/alecthomas/gometalinter.v2'
- run: 'go get -u github.com/gordonklaus/ineffassign'
- run: 'go get -u github.com/opennota/check/cmd/structcheck'
- run: 'go get -u github.com/mdempsky/unconvert'
- run: 'go get -u github.com/opennota/check/cmd/varcheck'
- run:
name: install gometalinter tool
command: 'go get -u github.com/alecthomas/gometalinter'
- run:
name: install linters
command: 'gometalinter --install'
- run:
name: run some linters
command: 'gometalinter --vendor --deadline 6m --disable-all --enable=structcheck --enable=unconvert --enable=varcheck ./pkg/...'
name: run linters
command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
test-frontend:
docker:
@@ -139,6 +141,10 @@ workflows:
filters:
tags:
only: /.*/
- gometalinter:
filters:
tags:
only: /.*/
- build:
filters:
tags:

View File

@@ -56,10 +56,10 @@ To simplify syntax and to allow for dynamic parts, like date range filters, the
Macro example | Description
------------ | -------------
*$__time(dateColumn)* | Will be replaced by an expression to rename the column to `time`. For example, *dateColumn as time*
*$__timeEpoch(dateColumn)* | Will be replaced by an expression to rename the column to `time` and converting the value to unix timestamp. For example, *extract(epoch from dateColumn) as time*
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-05-10T10:06:23Z' AND '2017-05-10T10:06:23Z'*
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-05-10T10:06:23Z'*
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-05-10T10:06:23Z'*
*$__timeSec(dateColumn)* | Will be replaced by an expression to rename the column to `time` and converting the value to unix timestamp. For example, *extract(epoch from dateColumn) as time*
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'*
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time*
*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example).
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*

View File

@@ -36,6 +36,8 @@ Query Parameters:
- `alertId`: number. Optional. Find annotations for a specified alert.
- `dashboardId`: number. Optional. Find annotations that are scoped to a specific dashboard
- `panelId`: number. Optional. Find annotations that are scoped to a specific panel
- `userId`: number. Optional. Find annotations created by a specific user
- `type`: string. Optional. `alert`|`annotation` Return alerts or user created annotations
- `tags`: string. Optional. Use this to filter global annotations. Global annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. To do an "AND" filtering with multiple tags, specify the tags parameter multiple times e.g. `tags=tag1&tags=tag2`.
**Example Response**:

View File

@@ -1,4 +1,4 @@
{
"stable": "5.0.0",
"testing": "5.0.0"
"stable": "5.0.4",
"testing": "5.0.4"
}

View File

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

View File

@@ -2,7 +2,6 @@ package api
import (
"strings"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -15,9 +14,10 @@ import (
func GetAnnotations(c *m.ReqContext) Response {
query := &annotations.ItemQuery{
From: c.QueryInt64("from") / 1000,
To: c.QueryInt64("to") / 1000,
From: c.QueryInt64("from"),
To: c.QueryInt64("to"),
OrgId: c.OrgId,
UserId: c.QueryInt64("userId"),
AlertId: c.QueryInt64("alertId"),
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
@@ -37,7 +37,7 @@ func GetAnnotations(c *m.ReqContext) Response {
if item.Email != "" {
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
}
item.Time = item.Time * 1000
item.Time = item.Time
}
return JSON(200, items)
@@ -68,16 +68,12 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
UserId: c.UserId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
Epoch: cmd.Time / 1000,
Epoch: cmd.Time,
Text: cmd.Text,
Data: cmd.Data,
Tags: cmd.Tags,
}
if item.Epoch == 0 {
item.Epoch = time.Now().Unix()
}
if err := repo.Save(&item); err != nil {
return Error(500, "Failed to save annotation", err)
}
@@ -97,7 +93,7 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
}
item.Id = 0
item.Epoch = cmd.TimeEnd / 1000
item.Epoch = cmd.TimeEnd
if err := repo.Save(&item); err != nil {
return Error(500, "Failed save annotation for region end time", err)
@@ -132,9 +128,6 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
return Error(500, "Failed to save Graphite annotation", err)
}
if cmd.When == 0 {
cmd.When = time.Now().Unix()
}
text := formatGraphiteAnnotation(cmd.What, cmd.Data)
// Support tags in prior to Graphite 0.10.0 format (string of tags separated by space)
@@ -163,7 +156,7 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
Epoch: cmd.When,
Epoch: cmd.When * 1000,
Text: text,
Tags: tagsArray,
}
@@ -191,7 +184,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
OrgId: c.OrgId,
UserId: c.UserId,
Id: annotationID,
Epoch: cmd.Time / 1000,
Epoch: cmd.Time,
Text: cmd.Text,
Tags: cmd.Tags,
}
@@ -203,7 +196,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
if cmd.IsRegion {
itemRight := item
itemRight.RegionId = item.Id
itemRight.Epoch = cmd.TimeEnd / 1000
itemRight.Epoch = cmd.TimeEnd
// We don't know id of region right event, so set it to 0 and find then using query like
// ... WHERE region_id = <item.RegionId> AND id != <item.RegionId> ...

View File

@@ -100,9 +100,9 @@ func main() {
}
func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) {
var code int
signalChan := make(chan os.Signal, 1)
ignoreChan := make(chan os.Signal, 1)
code := 0
signal.Notify(ignoreChan, syscall.SIGHUP)
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)

View File

@@ -141,5 +141,9 @@ func getDiff(baseData, newData *simplejson.Json) (interface{}, diff.Diff, error)
left := make(map[string]interface{})
err = json.Unmarshal(leftBytes, &left)
if err != nil {
return nil, nil, err
}
return left, jsonDiff, nil
}

View File

@@ -60,6 +60,7 @@ func TestFirst(t *testing.T) {
}`
j, err := NewObjectFromBytes([]byte(testJSON))
assert.True(err == nil, "failed to create new object from bytes")
a, err := j.GetObject("address")
assert.True(a != nil && err == nil, "failed to create json from string")
@@ -108,6 +109,7 @@ func TestFirst(t *testing.T) {
//log.Println("address: ", address)
s, err = address.GetString("street")
assert.True(s == "Street 42" && err == nil, "street mismatching")
addressAsString, err := j.GetString("address")
assert.True(addressAsString == "" && err != nil, "address should not be an string")
@@ -148,6 +150,7 @@ func TestFirst(t *testing.T) {
//assert.True(element.IsObject() == true, "first fail")
element, err := elementValue.Object()
assert.True(err == nil, "create element fail")
s, err = element.GetString("street")
assert.True(s == "Street 42" && err == nil, "second fail")
@@ -232,6 +235,7 @@ func TestSecond(t *testing.T) {
assert.True(fromName == "Tom Brady" && err == nil, "fromName mismatch")
actions, err := dataItem.GetObjectArray("actions")
assert.True(err == nil, "get object from array failed")
for index, action := range actions {

View File

@@ -13,6 +13,7 @@ func TestUploadToAzureBlob(t *testing.T) {
err := setting.NewConfigContext(&setting.CommandLineArgs{
HomePath: "../../../",
})
So(err, ShouldBeNil)
uploader, _ := NewImageUploader()

View File

@@ -19,6 +19,7 @@ func TestImageUploaderFactory(t *testing.T) {
Convey("with bucket url https://foo.bar.baz.s3-us-east-2.amazonaws.com", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
So(err, ShouldBeNil)
s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
@@ -37,6 +38,7 @@ func TestImageUploaderFactory(t *testing.T) {
Convey("with bucket url https://s3.amazonaws.com/mybucket", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
So(err, ShouldBeNil)
s3sec.NewKey("bucket_url", "https://s3.amazonaws.com/my.bucket.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
@@ -55,15 +57,15 @@ func TestImageUploaderFactory(t *testing.T) {
Convey("with bucket url https://s3-us-west-2.amazonaws.com/mybucket", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
So(err, ShouldBeNil)
s3sec.NewKey("bucket_url", "https://s3-us-west-2.amazonaws.com/my.bucket.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*S3Uploader)
original, ok := uploader.(*S3Uploader)
So(ok, ShouldBeTrue)
So(original.region, ShouldEqual, "us-west-2")
So(original.bucket, ShouldEqual, "my.bucket.com")
@@ -82,6 +84,7 @@ func TestImageUploaderFactory(t *testing.T) {
setting.ImageUploadProvider = "webdav"
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
So(err, ShouldBeNil)
webdavSec.NewKey("url", "webdavUrl")
webdavSec.NewKey("username", "username")
webdavSec.NewKey("password", "password")
@@ -107,14 +110,14 @@ func TestImageUploaderFactory(t *testing.T) {
setting.ImageUploadProvider = "gcs"
gcpSec, err := setting.Cfg.GetSection("external_image_storage.gcs")
So(err, ShouldBeNil)
gcpSec.NewKey("key_file", "/etc/secrets/project-79a52befa3f6.json")
gcpSec.NewKey("bucket", "project-grafana-east")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*GCSUploader)
original, ok := uploader.(*GCSUploader)
So(ok, ShouldBeTrue)
So(original.keyFile, ShouldEqual, "/etc/secrets/project-79a52befa3f6.json")
So(original.bucket, ShouldEqual, "project-grafana-east")
@@ -128,15 +131,15 @@ func TestImageUploaderFactory(t *testing.T) {
Convey("with container name", func() {
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
So(err, ShouldBeNil)
azureBlobSec.NewKey("account_name", "account_name")
azureBlobSec.NewKey("account_key", "account_key")
azureBlobSec.NewKey("container_name", "container_name")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*AzureBlobUploader)
original, ok := uploader.(*AzureBlobUploader)
So(ok, ShouldBeTrue)
So(original.account_name, ShouldEqual, "account_name")
So(original.account_key, ShouldEqual, "account_key")

View File

@@ -41,14 +41,20 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
url.Path = path.Join(url.Path, filename)
imgData, err := ioutil.ReadFile(pa)
if err != nil {
return "", err
}
req, err := http.NewRequest("PUT", url.String(), bytes.NewReader(imgData))
if err != nil {
return "", err
}
if u.username != "" {
req.SetBasicAuth(u.username, u.password)
}
res, err := netClient.Do(req)
if err != nil {
return "", err
}

View File

@@ -32,7 +32,9 @@ func TestLogFile(t *testing.T) {
Convey("Logging should add lines", func() {
err := fileLogWrite.WriteLine("test1\n")
So(err, ShouldBeNil)
err = fileLogWrite.WriteLine("test2\n")
So(err, ShouldBeNil)
err = fileLogWrite.WriteLine("test3\n")
So(err, ShouldBeNil)
So(fileLogWrite.maxlines_curlines, ShouldEqual, 3)

View File

@@ -302,9 +302,11 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
var groupSearchResult *ldap.SearchResult
for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
filter_replace := getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
var filter_replace string
if a.server.GroupSearchFilterUserAttribute == "" {
filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
} else {
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
}
filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
@@ -346,6 +348,9 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
}
func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
if name == "DN" {
return result.Entries[0].DN
}
for _, attr := range result.Entries[n].Attributes {
if attr.Name == name {
if len(attr.Values) > 0 {

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

@@ -3,13 +3,14 @@ package notifiers
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"os"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"io"
"mime/multipart"
"os"
)
const (
@@ -133,6 +134,9 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
}
ruleUrl, err := evalContext.GetRuleUrl()
if err != nil {
return nil, err
}
metrics := generateMetricsMessage(evalContext)
message := generateImageCaption(evalContext, ruleUrl, metrics)

View File

@@ -83,27 +83,6 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
return nil
}
fields := make([]map[string]interface{}, 0)
fieldLimitCount := 4
for index, evt := range evalContext.EvalMatches {
fields = append(fields, map[string]interface{}{
"title": evt.Metric,
"value": evt.Value,
"short": true,
})
if index > fieldLimitCount {
break
}
}
if evalContext.Error != nil {
fields = append(fields, map[string]interface{}{
"title": "Error message",
"value": evalContext.Error.Error(),
"short": false,
})
}
messageType := evalContext.Rule.State
if evalContext.Rule.State == models.AlertStateAlerting { // translate 'Alerting' to 'CRITICAL' (Victorops analog)
messageType = AlertStateCritical

View File

@@ -77,7 +77,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
Text: "",
NewState: string(evalContext.Rule.State),
PrevState: string(evalContext.PrevAlertState),
Epoch: time.Now().Unix(),
Epoch: time.Now().UnixNano() / int64(time.Millisecond),
Data: annotationData,
}

View File

@@ -13,6 +13,7 @@ type ItemQuery struct {
OrgId int64 `json:"orgId"`
From int64 `json:"from"`
To int64 `json:"to"`
UserId int64 `json:"userId"`
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
@@ -63,6 +64,8 @@ type Item struct {
PrevState string `json:"prevState"`
NewState string `json:"newState"`
Epoch int64 `json:"epoch"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
Tags []string `json:"tags"`
Data *simplejson.Json `json:"data"`
@@ -80,6 +83,8 @@ type ItemDTO struct {
UserId int64 `json:"userId"`
NewState string `json:"newState"`
PrevState string `json:"prevState"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
Time int64 `json:"time"`
Text string `json:"text"`
RegionId int64 `json:"regionId"`

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) {
@@ -649,6 +649,9 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShou
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
if err != nil {
sc.reportFailure(tc, nil, err)
}
sc.updatePermissions = permissionList
ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)

View File

@@ -235,7 +235,6 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, service dashboards.DashboardPr
func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
checkFilepath, err := filepath.EvalSymlinks(path)
if path != checkFilepath {
path = checkFilepath
fi, err := os.Lstat(checkFilepath)
if err != nil {
return nil, err

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
@@ -17,6 +18,12 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
return inTransaction(func(sess *DBSession) error {
tags := models.ParseTagPairs(item.Tags)
item.Tags = models.JoinTagPairs(tags)
item.Created = time.Now().UnixNano() / int64(time.Millisecond)
item.Updated = item.Created
if item.Epoch == 0 {
item.Epoch = item.Created
}
if _, err := sess.Table("annotation").Insert(item); err != nil {
return err
}
@@ -79,6 +86,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
return errors.New("Annotation not found")
}
existing.Updated = time.Now().UnixNano() / int64(time.Millisecond)
existing.Epoch = item.Epoch
existing.Text = item.Text
if item.RegionId != 0 {
@@ -102,7 +110,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
existing.Tags = item.Tags
_, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing)
_, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "updated", "tags").Update(existing)
return err
})
}
@@ -124,6 +132,8 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
annotation.text,
annotation.tags,
annotation.data,
annotation.created,
annotation.updated,
usr.email,
usr.login,
alert.name as alert_name
@@ -161,6 +171,11 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
params = append(params, query.PanelId)
}
if query.UserId != 0 {
sql.WriteString(` AND annotation.user_id = ?`)
params = append(params, query.UserId)
}
if query.From > 0 && query.To > 0 {
sql.WriteString(` AND annotation.epoch BETWEEN ? AND ?`)
params = append(params, query.From, query.To)
@@ -168,6 +183,8 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
if query.Type == "alert" {
sql.WriteString(` AND annotation.alert_id > 0`)
} else if query.Type == "annotation" {
sql.WriteString(` AND annotation.alert_id = 0`)
}
if len(query.Tags) > 0 {

View File

@@ -79,6 +79,12 @@ func TestAnnotations(t *testing.T) {
Convey("Can read tags", func() {
So(items[0].Tags, ShouldResemble, []string{"outage", "error", "type:outage", "server:server-1"})
})
Convey("Has created and updated values", func() {
So(items[0].Created, ShouldBeGreaterThan, 0)
So(items[0].Updated, ShouldBeGreaterThan, 0)
So(items[0].Updated, ShouldEqual, items[0].Created)
})
})
Convey("Can query for annotation by id", func() {
@@ -231,6 +237,10 @@ func TestAnnotations(t *testing.T) {
So(items[0].Tags, ShouldResemble, []string{"newtag1", "newtag2"})
So(items[0].Text, ShouldEqual, "something new")
})
Convey("Updated time has increased", func() {
So(items[0].Updated, ShouldBeGreaterThan, items[0].Created)
})
})
Convey("Can delete annotation", func() {
@@ -246,6 +256,7 @@ func TestAnnotations(t *testing.T) {
annotationId := items[0].Id
err = repo.Delete(&annotations.DeleteParams{Id: annotationId})
So(err, ShouldBeNil)
items, err = repo.Find(query)
So(err, ShouldBeNil)

View File

@@ -77,7 +77,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
}
parentVersion := dash.Version
affectedRows := int64(0)
var affectedRows int64
var err error
if dash.Id == 0 {

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)
})
})
@@ -131,6 +154,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
DashboardId: savedFolder.Id,
Permission: m.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
err = GetDashboardAclInfoList(q1)
@@ -209,8 +233,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

@@ -90,4 +90,29 @@ func addAnnotationMig(mg *Migrator) {
Sqlite(updateTextFieldSql).
Postgres(updateTextFieldSql).
Mysql(updateTextFieldSql))
//
// Add a 'created' & 'updated' column
//
mg.AddMigration("Add created time to annotation table", NewAddColumnMigration(table, &Column{
Name: "created", Type: DB_BigInt, Nullable: true, Default: "0",
}))
mg.AddMigration("Add updated time to annotation table", NewAddColumnMigration(table, &Column{
Name: "updated", Type: DB_BigInt, Nullable: true, Default: "0",
}))
mg.AddMigration("Add index for created in annotation table", NewAddIndexMigration(table, &Index{
Cols: []string{"org_id", "created"}, Type: IndexType,
}))
mg.AddMigration("Add index for updated in annotation table", NewAddIndexMigration(table, &Index{
Cols: []string{"org_id", "updated"}, Type: IndexType,
}))
//
// Convert epoch saved as seconds to miliseconds
//
updateEpochSql := "UPDATE annotation SET epoch = (epoch*1000) where epoch < 9999999999"
mg.AddMigration("Convert existing annotations from seconds to milliseconds", new(RawSqlMigration).
Sqlite(updateEpochSql).
Postgres(updateEpochSql).
Mysql(updateEpochSql))
}

View File

@@ -27,7 +27,7 @@ func TestMigrations(t *testing.T) {
sqlutil.CleanDB(x)
has, err := x.SQL(sql).Get(&r)
_, err = x.SQL(sql).Get(&r)
So(err, ShouldNotBeNil)
mg := NewMigrator(x)
@@ -36,7 +36,7 @@ func TestMigrations(t *testing.T) {
err = mg.Start()
So(err, ShouldBeNil)
has, err = x.SQL(sql).Get(&r)
has, err := x.SQL(sql).Get(&r)
So(err, ShouldBeNil)
So(has, ShouldBeTrue)
expectedMigrations := mg.MigrationsCount() - 2 //we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. until then we have to keep this

View File

@@ -22,6 +22,9 @@ func CreatePlaylist(cmd *m.CreatePlaylistCommand) error {
}
_, err := x.Insert(&playlist)
if err != nil {
return err
}
playlistItems := make([]m.PlaylistItem, 0)
for _, item := range cmd.Items {

View File

@@ -48,6 +48,9 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
var pluginSetting m.PluginSetting
exists, err := sess.Where("org_id=? and plugin_id=?", cmd.OrgId, cmd.PluginId).Get(&pluginSetting)
if err != nil {
return err
}
sess.UseBool("enabled")
sess.UseBool("pinned")
if !exists {

View File

@@ -72,6 +72,9 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error {
var prefs m.Preferences
exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs)
if err != nil {
return err
}
if !exists {
prefs = m.Preferences{

View File

@@ -74,6 +74,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
Convey("Should be able to return all teams a user is member of", func() {
groupId := group2.Result.Id
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]})
So(err, ShouldBeNil)
query := &m.GetTeamsByUserQuery{OrgId: testOrgId, UserId: userIds[0]}
err = GetTeamsByUser(query)
@@ -103,7 +104,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]})
So(err, ShouldBeNil)
err = testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId})
So(err, ShouldBeNil)
err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId})
So(err, ShouldBeNil)

View File

@@ -168,11 +168,9 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
return m.ErrUserNotFound
}
user := new(m.User)
// Try and find the user by login first.
// It's not sufficient to assume that a LoginOrEmail with an "@" is an email.
user = &m.User{Login: query.LoginOrEmail}
user := &m.User{Login: query.LoginOrEmail}
has, err := x.Get(user)
if err != nil {
@@ -202,9 +200,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
return m.ErrUserNotFound
}
user := new(m.User)
user = &m.User{Email: query.Email}
user := &m.User{Email: query.Email}
has, err := x.Get(user)
if err != nil {

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

@@ -271,7 +271,7 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
}
}
period := 300
var period int
if regexp.MustCompile(`^\d+$`).Match([]byte(p)) {
period, err = strconv.Atoi(p)
if err != nil {

View File

@@ -40,6 +40,9 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.Data
}
parsedInterval, err := tsdb.GetIntervalFrom(dsInfo, model, time.Millisecond*1)
if err != nil {
return nil, err
}
return &Query{
Measurement: measurement,

View File

@@ -62,9 +62,8 @@ func (query *Query) renderTags() []string {
}
}
textValue := ""
// quote value unless regex or number
var textValue string
if tag.Operator == "=~" || tag.Operator == "!~" {
textValue = tag.Value
} else if tag.Operator == "<" || tag.Operator == ">" {
@@ -107,7 +106,7 @@ func (query *Query) renderSelectors(queryContext *tsdb.TsdbQuery) string {
}
func (query *Query) renderMeasurement() string {
policy := ""
var policy string
if query.Policy == "" || query.Policy == "default" {
policy = ""
} else {

View File

@@ -83,6 +83,10 @@ func (e *OpenTsdbExecutor) createRequest(dsInfo *models.DataSource, data OpenTsd
u.Path = path.Join(u.Path, "api/query")
postData, err := json.Marshal(data)
if err != nil {
plog.Info("Failed marshalling data", "error", err)
return nil, fmt.Errorf("Failed to create request. error: %v", err)
}
req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(string(postData)))
if err != nil {

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

@@ -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

@@ -453,7 +453,13 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
};
this.testDatasource = function() {
return this.metricFindQuery('*').then(function() {
let query = {
panelId: 3,
rangeRaw: { from: 'now-1h', to: 'now' },
targets: [{ target: 'constantLine(100)' }],
maxDataPoints: 300,
};
return this.query(query).then(function() {
return { status: 'success', message: 'Data source is working' };
});
};

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

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