Merge branch 'master' into 10427_addpanel_filter

This commit is contained in:
Marcus Efraimsson 2018-04-18 11:36:13 +02:00
commit 82054e1a3e
No known key found for this signature in database
GPG Key ID: EBFE0FB04612DD4A
124 changed files with 2317 additions and 1355 deletions

View File

@ -1,6 +1,6 @@
[run]
init_cmds = [
["go", "build", "-o", "./bin/grafana-server", "./pkg/cmd/grafana-server"],
["go", "run", "build.go", "-dev", "build-server"],
["./bin/grafana-server", "cfg:app_mode=development"]
]
watch_all = true
@ -12,6 +12,6 @@ watch_dirs = [
watch_exts = [".go", ".ini", ".toml"]
build_delay = 1500
cmds = [
["go", "build", "-o", "./bin/grafana-server", "./pkg/cmd/grafana-server"],
["go", "run", "build.go", "-dev", "build"],
["./bin/grafana-server", "cfg:app_mode=development"]
]

View File

@ -1,6 +1,22 @@
version: 2
jobs:
codespell:
docker:
- image: circleci/python
steps:
- checkout
- run:
name: install codespell
command: 'sudo pip install codespell'
- run:
# Important: all words have to be in lowercase, and separated by "\n".
name: exclude known exceptions
command: 'echo -e "unknwon" > words_to_ignore.txt'
- run:
name: check documentation spelling errors
command: 'codespell -I ./words_to_ignore.txt docs/'
test-frontend:
docker:
- image: circleci/node:6.11.4
@ -103,6 +119,10 @@ workflows:
version: 2
test-and-build:
jobs:
- codespell:
filters:
tags:
only: /.*/
- build:
filters:
tags:

View File

@ -17,6 +17,8 @@
* **IE11**: IE 11 compatibility [#11165](https://github.com/grafana/grafana/issues/11165)
* **Scrolling**: Better scrolling experience [#11053](https://github.com/grafana/grafana/issues/11053), [#11252](https://github.com/grafana/grafana/issues/11252), [#10836](https://github.com/grafana/grafana/issues/10836), [#11185](https://github.com/grafana/grafana/issues/11185), [#11168](https://github.com/grafana/grafana/issues/11168)
* **Docker**: Improved docker image (breaking changes regarding file ownership) [grafana-docker #141](https://github.com/grafana/grafana-docker/issues/141), thx [@Spindel](https://github.com/Spindel), [@ChristianKniep](https://github.com/ChristianKniep), [@brancz](https://github.com/brancz) and [@jangaraj](https://github.com/jangaraj)
* **Folders**: A folder admin cannot add user/team permissions for folder/its dashboards [#11173](https://github.com/grafana/grafana/issues/11173)
* **Provisioning**: Improved workflow for provisioned dashboards [#10883](https://github.com/grafana/grafana/issues/10883)
### Minor
@ -49,6 +51,10 @@
* **Prometheus**: tooltip for legend format not showing properly [#11516](https://github.com/grafana/grafana/issues/11516), thx [@svenklemm](https://github.com/svenklemm)
* **Playlist**: Empty playlists cannot be deleted [#11133](https://github.com/grafana/grafana/issues/11133), thx [@kichristensen](https://github.com/kichristensen)
* **Switch Orgs**: Alphabetic order in Switch Organization modal [#11556](https://github.com/grafana/grafana/issues/11556)
* **Postgres**: improve `$__timeFilter` macro [#11578](https://github.com/grafana/grafana/issues/11578), thx [@svenklemm](https://github.com/svenklemm)
* **Permission list**: Improved ux [#10747](https://github.com/grafana/grafana/issues/10747)
* **Dashboard**: Sizing and positioning of settings menu icons [#11572](https://github.com/grafana/grafana/pull/11572)
* **Folders**: User with org viewer role should not be able to save/move dashboards in/to general folder [#11553](https://github.com/grafana/grafana/issues/11553)
### Tech
* Migrated JavaScript files to TypeScript

View File

@ -22,6 +22,7 @@ module.exports = function (grunt) {
}
}
config.coverage = grunt.option('coverage');
config.phjs = grunt.option('phjsToRelease');
config.pkg.version = grunt.option('pkgVer') || config.pkg.version;

View File

@ -41,6 +41,7 @@ var (
includeBuildNumber bool = true
buildNumber int = 0
binaries []string = []string{"grafana-server", "grafana-cli"}
isDev bool = false
)
const minGoVersion = 1.8
@ -61,6 +62,7 @@ func main() {
flag.BoolVar(&race, "race", race, "Use race detector")
flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system")
flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
flag.Parse()
readVersionFromPackageJson()
@ -394,7 +396,9 @@ func build(binaryName, pkg string, tags []string) {
binary += ".exe"
}
rmr(binary, binary+".md5")
if !isDev {
rmr(binary, binary+".md5")
}
args := []string{"build", "-ldflags", ldflags()}
if len(tags) > 0 {
args = append(args, "-tags", strings.Join(tags, ","))
@ -405,16 +409,21 @@ func build(binaryName, pkg string, tags []string) {
args = append(args, "-o", binary)
args = append(args, pkg)
setBuildEnv()
runPrint("go", "version")
if !isDev {
setBuildEnv()
runPrint("go", "version")
}
runPrint("go", args...)
// Create an md5 checksum of the binary, to be included in the archive for
// automatic upgrades.
err := md5File(binary)
if err != nil {
log.Fatal(err)
if !isDev {
// Create an md5 checksum of the binary, to be included in the archive for
// automatic upgrades.
err := md5File(binary)
if err != nil {
log.Fatal(err)
}
}
}
@ -435,6 +444,10 @@ func rmr(paths ...string) {
}
func clean() {
if isDev {
return
}
rmr("dist")
rmr("tmp")
rmr(filepath.Join(os.Getenv("GOPATH"), fmt.Sprintf("pkg/%s_%s/github.com/grafana", goos, goarch)))
@ -550,7 +563,7 @@ func shaFilesInDist() {
return nil
}
if strings.Contains(path, ".sha256") == false {
if !strings.Contains(path, ".sha256") {
err := shaFile(path)
if err != nil {
log.Printf("Failed to create sha file. error: %v\n", err)

11
codecov.yml Normal file
View File

@ -0,0 +1,11 @@
coverage:
precision: 2
round: down
range: "50...100"
status:
project: yes
patch: yes
changes: no
comment: off

View File

@ -57,7 +57,7 @@ are supported.
### Min time interval
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formated as a
This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
Identifier | Description
@ -172,4 +172,4 @@ datasources:
jsonData:
interval: Daily
timeField: "@timestamp"
```
```

View File

@ -41,7 +41,7 @@ mode is also more secure as the username & password will never reach the browser
### Min time interval
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formated as a
This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
Identifier | Description
@ -208,4 +208,4 @@ datasources:
user: grafana
password: grafana
url: http://localhost:8086
```
```

View File

@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"defaultRegion": "us-west-1"
},
"secureJsonData": {
"accessKey": "Ol4pIDpeKSA6XikgOl4p",
"secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
"accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
"secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
}
}
```

View File

@ -102,6 +102,7 @@
"watch": "webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js",
"build": "grunt build",
"test": "grunt test",
"test:coverage": "grunt test --coverage=true",
"lint": "tslint -c tslint.json --project tsconfig.json --type-check",
"karma": "grunt karma:dev",
"jest": "jest --notify --watch",

View File

@ -149,8 +149,6 @@ func (hs *HTTPServer) registerRoutes() {
// team (admin permission required)
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
teamsRoute.Get("/:teamId", wrap(GetTeamByID))
teamsRoute.Get("/search", wrap(SearchTeams))
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID))
@ -159,6 +157,12 @@ func (hs *HTTPServer) registerRoutes() {
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
}, reqOrgAdmin)
// team without requirement of user to be org admin
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
teamsRoute.Get("/:teamId", wrap(GetTeamByID))
teamsRoute.Get("/search", wrap(SearchTeams))
})
// org information available to all users.
apiRoute.Group("/org", func(orgRoute RouteRegister) {
orgRoute.Get("/", wrap(GetOrgCurrent))
@ -170,7 +174,6 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
@ -184,6 +187,11 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
}, reqOrgAdmin)
// current org without requirement of user to be org admin
apiRoute.Group("/org", func(orgRoute RouteRegister) {
orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
})
// create new org
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))

View File

@ -258,9 +258,6 @@ func (this *thunderTask) fetch() error {
this.Avatar.data = &bytes.Buffer{}
writer := bufio.NewWriter(this.Avatar.data)
if _, err = io.Copy(writer, resp.Body); err != nil {
return err
}
return nil
_, err = io.Copy(writer, resp.Body)
return err
}

View File

@ -102,6 +102,16 @@ func GetDashboard(c *m.ReqContext) Response {
meta.FolderUrl = query.Result.GetUrl()
}
isDashboardProvisioned := &m.IsDashboardProvisionedQuery{DashboardId: dash.Id}
err = bus.Dispatch(isDashboardProvisioned)
if err != nil {
return Error(500, "Error while checking if dashboard is provisioned", err)
}
if isDashboardProvisioned.Result {
meta.Provisioned = true
}
// make sure db version is in sync with json model version
dash.Data.Set("version", dash.Version)
@ -228,7 +238,8 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
err == m.ErrDashboardWithSameUIDExists ||
err == m.ErrFolderNotFound ||
err == m.ErrDashboardFolderCannotHaveParent ||
err == m.ErrDashboardFolderNameExists {
err == m.ErrDashboardFolderNameExists ||
err == m.ErrDashboardCannotSaveProvisionedDashboard {
return Error(400, err.Error(), nil)
}

View File

@ -29,6 +29,11 @@ func GetDashboardPermissionList(c *m.ReqContext) Response {
}
for _, perm := range acl {
perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
if perm.TeamId > 0 {
perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
}
if perm.Slug != "" {
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}

View File

@ -143,7 +143,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) {
})
})
Convey("When trying to override inherited permissions with lower presedence", func() {
Convey("When trying to override inherited permissions with lower precedence", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,

View File

@ -42,6 +42,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
@ -192,6 +197,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
fakeDash.HasAcl = true
setting.ViewersCanEdit = false
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{fakeDash}
query.Result = dashboards
@ -625,6 +635,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
dashTwo.FolderId = 3
dashTwo.HasAcl = false
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{dashOne, dashTwo}
query.Result = dashboards
@ -720,6 +735,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
{SaveError: m.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: 403},
{SaveError: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardCannotSaveProvisionedDashboard, ExpectedStatusCode: 400},
{SaveError: m.UpdatePluginDashboardError{PluginId: "plug"}, ExpectedStatusCode: 412},
}
@ -750,6 +766,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
query.Result = false
return nil
})
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{
Data: simplejson.NewFromAny(map[string]interface{}{

View File

@ -28,6 +28,7 @@ type DashboardMeta struct {
FolderId int64 `json:"folderId"`
FolderTitle string `json:"folderTitle"`
FolderUrl string `json:"folderUrl"`
Provisioned bool `json:"provisioned"`
}
type DashboardFullWithMeta struct {

View File

@ -33,6 +33,12 @@ func GetFolderPermissionList(c *m.ReqContext) Response {
perm.FolderId = folder.Id
perm.DashboardId = 0
perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
if perm.TeamId > 0 {
perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
}
if perm.Slug != "" {
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}

View File

@ -139,7 +139,7 @@ func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
}
hs.httpSrv.TLSConfig = tlsCfg
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0)
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
}

View File

@ -33,7 +33,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
fileInfo, err := os.Stat(pluginsDir)
if err != nil {
if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil {
return errors.New(fmt.Sprintf("pluginsDir (%s) is not a writable directory", pluginsDir))
return fmt.Errorf("pluginsDir (%s) is not a writable directory", pluginsDir)
}
return nil
}

View File

@ -24,7 +24,7 @@ var validateLsCommand = func(pluginDir string) error {
return fmt.Errorf("error: %s", err)
}
if pluginDirInfo.IsDir() == false {
if !pluginDirInfo.IsDir() {
return errors.New("plugin path is not a directory")
}

View File

@ -53,8 +53,7 @@ func upgradeAllCommand(c CommandLine) error {
for _, p := range pluginsToUpgrade {
logger.Infof("Updating %v \n", p.Id)
var err error
err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
err := s.RemoveInstalledPlugin(pluginsDir, p.Id)
if err != nil {
return err
}

View File

@ -42,7 +42,7 @@ func Init(version string, skipTLSVerify bool) {
}
HttpClient = http.Client{
Timeout: time.Duration(10 * time.Second),
Timeout: 10 * time.Second,
Transport: tr,
}
}

View File

@ -33,7 +33,7 @@ func New(orgId int64, name string) KeyGenResult {
jsonString, _ := json.Marshal(jsonKey)
result.ClientSecret = base64.StdEncoding.EncodeToString([]byte(jsonString))
result.ClientSecret = base64.StdEncoding.EncodeToString(jsonString)
return result
}
@ -44,7 +44,7 @@ func Decode(keyString string) (*ApiKeyJson, error) {
}
var keyObj ApiKeyJson
err = json.Unmarshal([]byte(jsonString), &keyObj)
err = json.Unmarshal(jsonString, &keyObj)
if err != nil {
return nil, ErrInvalidApiKey
}

View File

@ -585,7 +585,6 @@ func (v *Value) Null() error {
switch v.data.(type) {
case nil:
valid = v.exists // Valid only if j also exists, since other values could possibly also be nil
break
}
if valid {
@ -607,7 +606,6 @@ func (v *Value) Array() ([]*Value, error) {
switch v.data.(type) {
case []interface{}:
valid = true
break
}
// Unsure if this is a good way to use slices, it's probably not
@ -638,7 +636,6 @@ func (v *Value) Number() (json.Number, error) {
switch v.data.(type) {
case json.Number:
valid = true
break
}
if valid {
@ -687,7 +684,6 @@ func (v *Value) Boolean() (bool, error) {
switch v.data.(type) {
case bool:
valid = true
break
}
if valid {
@ -709,7 +705,6 @@ func (v *Value) Object() (*Object, error) {
switch v.data.(type) {
case map[string]interface{}:
valid = true
break
}
if valid {
@ -746,7 +741,6 @@ func (v *Value) ObjectArray() ([]*Object, error) {
switch v.data.(type) {
case []interface{}:
valid = true
break
}
// Unsure if this is a good way to use slices, it's probably not
@ -782,7 +776,6 @@ func (v *Value) String() (string, error) {
switch v.data.(type) {
case string:
valid = true
break
}
if valid {

View File

@ -21,7 +21,7 @@ func NewAssert(t *testing.T) *Assert {
}
func (assert *Assert) True(value bool, message string) {
if value == false {
if !value {
log.Panicln("Assert: ", message)
}
}
@ -119,13 +119,13 @@ func TestFirst(t *testing.T) {
assert.True(s == "" && err != nil, "nonexistent string fail")
b, err := j.GetBoolean("true")
assert.True(b == true && err == nil, "bool true test")
assert.True(b && err == nil, "bool true test")
b, err = j.GetBoolean("false")
assert.True(b == false && err == nil, "bool false test")
assert.True(!b && err == nil, "bool false test")
b, err = j.GetBoolean("invalid_field")
assert.True(b == false && err != nil, "bool invalid test")
assert.True(!b && err != nil, "bool invalid test")
list, err := j.GetValueArray("list")
assert.True(list != nil && err == nil, "list should be an array")

View File

@ -225,7 +225,7 @@ func (a *Auth) SignRequest(req *http.Request) {
)
decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
sha256 := hmac.New(sha256.New, []byte(decodedKey))
sha256 := hmac.New(sha256.New, decodedKey)
sha256.Write([]byte(strToSign))
signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))

View File

@ -50,7 +50,7 @@ func (f *Float) UnmarshalJSON(data []byte) error {
}
switch x := v.(type) {
case float64:
f.Float64 = float64(x)
f.Float64 = x
case map[string]interface{}:
err = json.Unmarshal(data, &f.NullFloat64)
case nil:

View File

@ -99,10 +99,7 @@ func (w *FileLogWriter) StartLogger() error {
return err
}
w.mw.SetFd(fd)
if err = w.initFd(); err != nil {
return err
}
return nil
return w.initFd()
}
func (w *FileLogWriter) docheck(size int) {

View File

@ -403,8 +403,7 @@ 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 {
var filter_replace string
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
filter_replace := getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
if a.server.GroupSearchFilterUserAttribute == "" {
filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
}

View File

@ -295,11 +295,7 @@ func writeMetric(buf *bufio.Writer, m model.Metric, mf *dto.MetricFamily) error
}
}
if err = addExtentionConventionForRollups(buf, mf, m); err != nil {
return err
}
return nil
return addExtentionConventionForRollups(buf, mf, m)
}
func addExtentionConventionForRollups(buf *bufio.Writer, mf *dto.MetricFamily, m model.Metric) error {

View File

@ -54,6 +54,7 @@ var (
M_Alerting_Active_Alerts prometheus.Gauge
M_StatTotal_Dashboards prometheus.Gauge
M_StatTotal_Users prometheus.Gauge
M_StatActive_Users prometheus.Gauge
M_StatTotal_Orgs prometheus.Gauge
M_StatTotal_Playlists prometheus.Gauge
M_Grafana_Version *prometheus.GaugeVec
@ -253,6 +254,12 @@ func init() {
Namespace: exporterName,
})
M_StatActive_Users = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "stat_active_users",
Help: "number of active users",
Namespace: exporterName,
})
M_StatTotal_Orgs = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "stat_total_orgs",
Help: "total amount of orgs",
@ -270,7 +277,6 @@ func init() {
Help: "Information about the Grafana",
Namespace: exporterName,
}, []string{"version"})
}
func initMetricVars(settings *MetricSettings) {
@ -305,6 +311,7 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Active_Alerts,
M_StatTotal_Dashboards,
M_StatTotal_Users,
M_StatActive_Users,
M_StatTotal_Orgs,
M_StatTotal_Playlists,
M_Grafana_Version)
@ -315,35 +322,36 @@ func initMetricVars(settings *MetricSettings) {
func instrumentationLoop(settings *MetricSettings) chan struct{} {
M_Instance_Start.Inc()
// set the total stats gauges before we publishing metrics
updateTotalStats()
onceEveryDayTick := time.NewTicker(time.Hour * 24)
secondTicker := time.NewTicker(time.Second * time.Duration(settings.IntervalSeconds))
everyMinuteTicker := time.NewTicker(time.Minute)
defer onceEveryDayTick.Stop()
defer everyMinuteTicker.Stop()
for {
select {
case <-onceEveryDayTick.C:
sendUsageStats()
case <-secondTicker.C:
case <-everyMinuteTicker.C:
updateTotalStats()
}
}
}
var metricPublishCounter int64 = 0
func updateTotalStats() {
metricPublishCounter++
if metricPublishCounter == 1 || metricPublishCounter%10 == 0 {
statsQuery := models.GetSystemStatsQuery{}
if err := bus.Dispatch(&statsQuery); err != nil {
metricsLogger.Error("Failed to get system stats", "error", err)
return
}
M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
statsQuery := models.GetSystemStatsQuery{}
if err := bus.Dispatch(&statsQuery); err != nil {
metricsLogger.Error("Failed to get system stats", "error", err)
return
}
M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
}
func sendUsageStats() {
@ -403,6 +411,6 @@ func sendUsageStats() {
out, _ := json.MarshalIndent(report, "", " ")
data := bytes.NewBuffer(out)
client := http.Client{Timeout: time.Duration(5 * time.Second)}
client := http.Client{Timeout: 5 * time.Second}
go client.Post("https://stats.grafana.org/grafana-usage-report", "application/json", data)
}

View File

@ -56,7 +56,10 @@ type DashboardAclInfoDTO struct {
UserId int64 `json:"userId"`
UserLogin string `json:"userLogin"`
UserEmail string `json:"userEmail"`
UserAvatarUrl string `json:"userAvatarUrl"`
TeamId int64 `json:"teamId"`
TeamEmail string `json:"teamEmail"`
TeamAvatarUrl string `json:"teamAvatarUrl"`
Team string `json:"team"`
Role *RoleType `json:"role,omitempty"`
Permission PermissionType `json:"permission"`

View File

@ -13,26 +13,27 @@ import (
// Typed errors
var (
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardFolderNotFound = errors.New("Folder not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
RootFolderName = "General"
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardFolderNotFound = errors.New("Folder not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
ErrDashboardCannotSaveProvisionedDashboard = errors.New("Cannot save provisioned dashboard")
RootFolderName = "General"
)
type UpdatePluginDashboardError struct {
@ -224,6 +225,10 @@ func GetFolderUrl(folderUid string, slug string) string {
return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug)
}
type ValidateDashboardBeforeSaveResult struct {
IsParentFolderChanged bool
}
//
// COMMANDS
//
@ -268,6 +273,7 @@ type ValidateDashboardBeforeSaveCommand struct {
OrgId int64
Dashboard *Dashboard
Overwrite bool
Result *ValidateDashboardBeforeSaveResult
}
//
@ -317,6 +323,12 @@ type GetDashboardSlugByIdQuery struct {
Result string
}
type IsDashboardProvisionedQuery struct {
DashboardId int64
Result bool
}
type GetProvisionedDashboardDataQuery struct {
Name string

View File

@ -33,7 +33,7 @@ func (ds *DataSource) GetHttpClient() (*http.Client, error) {
}
return &http.Client{
Timeout: time.Duration(30 * time.Second),
Timeout: 30 * time.Second,
Transport: transport,
}, nil
}

View File

@ -48,9 +48,9 @@ func (r *RoleType) UnmarshalJSON(data []byte) error {
*r = RoleType(str)
if (*r).IsValid() == false {
if !(*r).IsValid() {
if (*r) != "" {
return errors.New(fmt.Sprintf("JSON validation error: invalid role value: %s", *r))
return fmt.Errorf("JSON validation error: invalid role value: %s", *r)
}
*r = ROLE_VIEWER

View File

@ -74,7 +74,7 @@ func TestMappingRowValue(t *testing.T) {
boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
haveBool, ok := boolRowValue.(bool)
if !ok || haveBool != true {
if !ok || !haveBool {
t.Fatalf("Expected true, was %v", haveBool)
}

View File

@ -69,7 +69,7 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error {
for _, include := range pb.Includes {
if include.Role == "" {
include.Role = m.RoleType(m.ROLE_VIEWER)
include.Role = m.ROLE_VIEWER
}
}

View File

@ -13,7 +13,7 @@ import (
)
var (
httpClient http.Client = http.Client{Timeout: time.Duration(10 * time.Second)}
httpClient http.Client = http.Client{Timeout: 10 * time.Second}
)
type GrafanaNetPlugin struct {

View File

@ -20,7 +20,7 @@ type AlertEvaluator interface {
type NoValueEvaluator struct{}
func (e *NoValueEvaluator) Eval(reducedValue null.Float) bool {
return reducedValue.Valid == false
return !reducedValue.Valid
}
type ThresholdEvaluator struct {
@ -45,7 +45,7 @@ func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvalua
}
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
if reducedValue.Valid == false {
if !reducedValue.Valid {
return false
}
@ -88,7 +88,7 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
}
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
if reducedValue.Valid == false {
if !reducedValue.Valid {
return false
}

View File

@ -53,7 +53,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(reducedValue)
if reducedValue.Valid == false {
if !reducedValue.Valid {
emptySerieCount++
}

View File

@ -104,7 +104,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
// backward compatibility check, can be removed later
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
if hasEnabled && enabled.MustBool() == false {
if hasEnabled && !enabled.MustBool() {
continue
}

View File

@ -90,7 +90,7 @@ func (this *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
this.log.Error("Failed to send notification to LINE", "error", err, "body", string(body))
this.log.Error("Failed to send notification to LINE", "error", err, "body", body)
return err
}

View File

@ -219,7 +219,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
var cmd *m.SendWebhookSync
if evalContext.ImagePublicUrl == "" && this.UploadImage == true {
if evalContext.ImagePublicUrl == "" && this.UploadImage {
cmd = this.buildMessage(evalContext, true)
} else {
cmd = this.buildMessage(evalContext, false)

View File

@ -55,8 +55,8 @@ func (e ValidationError) Error() string {
}
var (
ValueFormatRegex = regexp.MustCompile("^\\d+")
UnitFormatRegex = regexp.MustCompile("\\w{1}$")
ValueFormatRegex = regexp.MustCompile(`^\d+`)
UnitFormatRegex = regexp.MustCompile(`\w{1}$`)
)
var unitMultiplier = map[string]int{

View File

@ -15,7 +15,7 @@ type SchedulerImpl struct {
func NewScheduler() Scheduler {
return &SchedulerImpl{
jobs: make(map[int64]*Job, 0),
jobs: make(map[int64]*Job),
log: log.New("alerting.scheduler"),
}
}
@ -23,7 +23,7 @@ func NewScheduler() Scheduler {
func (s *SchedulerImpl) Update(rules []*Rule) {
s.log.Debug("Scheduling update", "ruleCount", len(rules))
jobs := make(map[int64]*Job, 0)
jobs := make(map[int64]*Job)
for i, rule := range rules {
var job *Job

View File

@ -57,7 +57,7 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod
return cmd.Result, nil
}
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool) (*models.SaveDashboardCommand, error) {
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
dash := dto.Dashboard
dash.Title = strings.TrimSpace(dash.Title)
@ -103,6 +103,29 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
return nil, err
}
if validateBeforeSaveCmd.Result.IsParentFolderChanged {
folderGuardian := guardian.New(dash.FolderId, dto.OrgId, dto.User)
if canSave, err := folderGuardian.CanSave(); err != nil || !canSave {
if err != nil {
return nil, err
}
return nil, models.ErrDashboardUpdateAccessDenied
}
}
if validateProvisionedDashboard {
isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id}
err := bus.Dispatch(isDashboardProvisioned)
if err != nil {
return nil, err
}
if isDashboardProvisioned.Result {
return nil, models.ErrDashboardCannotSaveProvisionedDashboard
}
}
guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
if canSave, err := guard.CanSave(); err != nil || !canSave {
if err != nil {
@ -148,7 +171,7 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
UserId: 0,
OrgRole: models.ROLE_ADMIN,
}
cmd, err := dr.buildSaveDashboardCommand(dto, true)
cmd, err := dr.buildSaveDashboardCommand(dto, true, false)
if err != nil {
return nil, err
}
@ -178,7 +201,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
UserId: 0,
OrgRole: models.ROLE_ADMIN,
}
cmd, err := dr.buildSaveDashboardCommand(dto, false)
cmd, err := dr.buildSaveDashboardCommand(dto, false, false)
if err != nil {
return nil, err
}
@ -197,7 +220,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
}
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto, true)
cmd, err := dr.buildSaveDashboardCommand(dto, true, true)
if err != nil {
return nil, err
}
@ -216,7 +239,7 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da
}
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto, false)
cmd, err := dr.buildSaveDashboardCommand(dto, false, true)
if err != nil {
return nil, err
}

View File

@ -14,7 +14,9 @@ import (
func TestDashboardService(t *testing.T) {
Convey("Dashboard service tests", t, func() {
service := dashboardServiceImpl{}
bus.ClearBusHandlers()
service := &dashboardServiceImpl{}
origNewDashboardGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
@ -51,6 +53,12 @@ func TestDashboardService(t *testing.T) {
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
cmd.Result = false
return nil
})
@ -72,12 +80,42 @@ func TestDashboardService(t *testing.T) {
dto.Dashboard.SetUid(tc.Uid)
dto.User = &models.SignedInUser{}
_, err := service.buildSaveDashboardCommand(dto, true)
_, err := service.buildSaveDashboardCommand(dto, true, false)
So(err, ShouldEqual, tc.Error)
}
})
Convey("Should return validation error if dashboard is provisioned", func() {
provisioningValidated := false
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
provisioningValidated = true
cmd.Result = true
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
_, err := service.SaveDashboard(dto)
So(provisioningValidated, ShouldBeTrue)
So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
})
Convey("Should return validation error if alert data is invalid", func() {
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
cmd.Result = false
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return errors.New("error")
})
@ -88,6 +126,80 @@ func TestDashboardService(t *testing.T) {
})
})
Convey("Save provisioned dashboard validation", func() {
dto := &SaveDashboardDTO{}
Convey("Should not return validation error if dashboard is provisioned", func() {
provisioningValidated := false
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
provisioningValidated = true
cmd.Result = true
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
})
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
_, err := service.SaveProvisionedDashboard(dto, nil)
So(err, ShouldBeNil)
So(provisioningValidated, ShouldBeFalse)
})
})
Convey("Import dashboard validation", func() {
dto := &SaveDashboardDTO{}
Convey("Should return validation error if dashboard is provisioned", func() {
provisioningValidated := false
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
provisioningValidated = true
cmd.Result = true
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
})
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
_, err := service.ImportDashboard(dto)
So(provisioningValidated, ShouldBeTrue)
So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
})
})
Reset(func() {
guardian.New = origNewDashboardGuardian
})

View File

@ -104,7 +104,7 @@ func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) er
User: dr.user,
}
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
if err != nil {
return toFolderError(err)
}
@ -141,7 +141,7 @@ func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.Upd
Overwrite: cmd.Overwrite,
}
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
if err != nil {
return toFolderError(err)
}

View File

@ -32,6 +32,7 @@ func TestFolderService(t *testing.T) {
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return models.ErrDashboardUpdateAccessDenied
})
@ -92,6 +93,7 @@ func TestFolderService(t *testing.T) {
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
@ -108,11 +110,19 @@ func TestFolderService(t *testing.T) {
return nil
})
provisioningValidated := false
bus.AddHandler("test", func(query *models.IsDashboardProvisionedQuery) error {
provisioningValidated = true
return nil
})
Convey("When creating folder should not return access denied error", func() {
err := service.CreateFolder(&models.CreateFolderCommand{
Title: "Folder",
})
So(err, ShouldBeNil)
So(provisioningValidated, ShouldBeFalse)
})
Convey("When updating folder should not return access denied error", func() {
@ -121,6 +131,7 @@ func TestFolderService(t *testing.T) {
Title: "Folder",
})
So(err, ShouldBeNil)
So(provisioningValidated, ShouldBeFalse)
})
Convey("When deleting folder by uid should not return access denied error", func() {

View File

@ -173,7 +173,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss
return true, nil
}
return g.checkAcl(permission, acl)
return g.checkAcl(permission, existingPermissions)
}
// GetAcl returns dashboard acl

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,256 @@
package guardian
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
type scenarioContext struct {
t *testing.T
orgRoleScenario string
permissionScenario string
g DashboardGuardian
givenUser *m.SignedInUser
givenDashboardID int64
givenPermissions []*m.DashboardAclInfoDTO
givenTeams []*m.Team
updatePermissions []*m.DashboardAcl
expectedFlags permissionFlags
callerFile string
callerLine int
}
type scenarioFunc func(c *scenarioContext)
func orgRoleScenario(desc string, t *testing.T, role m.RoleType, fn scenarioFunc) {
user := &m.SignedInUser{
UserId: userID,
OrgId: orgID,
OrgRole: role,
}
guard := New(dashboardID, orgID, user)
sc := &scenarioContext{
t: t,
orgRoleScenario: desc,
givenUser: user,
givenDashboardID: dashboardID,
g: guard,
}
Convey(desc, func() {
fn(sc)
})
}
func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
bus.ClearBusHandlers()
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
if query.OrgId != sc.givenUser.OrgId {
sc.reportFailure("Invalid organization id for GetDashboardAclInfoListQuery", sc.givenUser.OrgId, query.OrgId)
}
if query.DashboardId != sc.givenDashboardID {
sc.reportFailure("Invalid dashboard id for GetDashboardAclInfoListQuery", sc.givenDashboardID, query.DashboardId)
}
query.Result = permissions
return nil
})
teams := []*m.Team{}
for _, p := range permissions {
if p.TeamId > 0 {
teams = append(teams, &m.Team{Id: p.TeamId})
}
}
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
if query.OrgId != sc.givenUser.OrgId {
sc.reportFailure("Invalid organization id for GetTeamsByUserQuery", sc.givenUser.OrgId, query.OrgId)
}
if query.UserId != sc.givenUser.UserId {
sc.reportFailure("Invalid user id for GetTeamsByUserQuery", sc.givenUser.UserId, query.UserId)
}
query.Result = teams
return nil
})
sc.permissionScenario = desc
sc.g = New(dashboardID, sc.givenUser.OrgId, sc.givenUser)
sc.givenDashboardID = dashboardID
sc.givenPermissions = permissions
sc.givenTeams = teams
Convey(desc, func() {
fn(sc)
})
}
type permissionType uint8
const (
USER permissionType = 1 << iota
TEAM
EDITOR
VIEWER
)
func (p permissionType) String() string {
names := map[uint8]string{
uint8(USER): "user",
uint8(TEAM): "team",
uint8(EDITOR): "editor role",
uint8(VIEWER): "viewer role",
}
return names[uint8(p)]
}
type permissionFlags uint8
const (
NO_ACCESS permissionFlags = 1 << iota
CAN_ADMIN
CAN_EDIT
CAN_SAVE
CAN_VIEW
FULL_ACCESS = CAN_ADMIN | CAN_EDIT | CAN_SAVE | CAN_VIEW
EDITOR_ACCESS = CAN_EDIT | CAN_SAVE | CAN_VIEW
VIEWER_ACCESS = CAN_VIEW
)
func (flag permissionFlags) canAdmin() bool {
return flag&CAN_ADMIN != 0
}
func (flag permissionFlags) canEdit() bool {
return flag&CAN_EDIT != 0
}
func (flag permissionFlags) canSave() bool {
return flag&CAN_SAVE != 0
}
func (flag permissionFlags) canView() bool {
return flag&CAN_VIEW != 0
}
func (flag permissionFlags) noAccess() bool {
return flag&(CAN_ADMIN|CAN_EDIT|CAN_SAVE|CAN_VIEW) == 0
}
func (f permissionFlags) String() string {
r := []string{}
if f.canAdmin() {
r = append(r, "admin")
}
if f.canEdit() {
r = append(r, "edit")
}
if f.canSave() {
r = append(r, "save")
}
if f.canView() {
r = append(r, "view")
}
if f.noAccess() {
r = append(r, "<no access>")
}
return strings.Join(r[:], ", ")
}
func (sc *scenarioContext) reportSuccess() {
So(true, ShouldBeTrue)
}
func (sc *scenarioContext) reportFailure(desc string, expected interface{}, actual interface{}) {
var buf bytes.Buffer
buf.WriteString("\n")
buf.WriteString(sc.orgRoleScenario)
buf.WriteString(" ")
buf.WriteString(sc.permissionScenario)
buf.WriteString("\n ")
buf.WriteString(desc)
buf.WriteString("\n")
buf.WriteString(fmt.Sprintf("Source test: %s:%d\n", sc.callerFile, sc.callerLine))
buf.WriteString(fmt.Sprintf("Expected: %v\n", expected))
buf.WriteString(fmt.Sprintf("Actual: %v\n", actual))
buf.WriteString("Context:")
buf.WriteString(fmt.Sprintf("\n Given user: orgRole=%s, id=%d, orgId=%d", sc.givenUser.OrgRole, sc.givenUser.UserId, sc.givenUser.OrgId))
buf.WriteString(fmt.Sprintf("\n Given dashboard id: %d", sc.givenDashboardID))
for i, p := range sc.givenPermissions {
r := "<nil>"
if p.Role != nil {
r = string(*p.Role)
}
buf.WriteString(fmt.Sprintf("\n Given permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardId, p.UserId, p.TeamId, r, p.Permission.String()))
}
for i, t := range sc.givenTeams {
buf.WriteString(fmt.Sprintf("\n Given team (%d): id=%d", i, t.Id))
}
for i, p := range sc.updatePermissions {
r := "<nil>"
if p.Role != nil {
r = string(*p.Role)
}
buf.WriteString(fmt.Sprintf("\n Update permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardId, p.UserId, p.TeamId, r, p.Permission.String()))
}
sc.t.Fatalf(buf.String())
}
func newCustomUserPermission(dashboardID int64, userID int64, permission m.PermissionType) *m.DashboardAcl {
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, UserId: userID, Permission: permission}
}
func newDefaultUserPermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
return newCustomUserPermission(dashboardID, userID, permission)
}
func newCustomTeamPermission(dashboardID int64, teamID int64, permission m.PermissionType) *m.DashboardAcl {
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, TeamId: teamID, Permission: permission}
}
func newDefaultTeamPermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
return newCustomTeamPermission(dashboardID, teamID, permission)
}
func newAdminRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &adminRole, Permission: permission}
}
func newEditorRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &editorRole, Permission: permission}
}
func newViewerRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &viewerRole, Permission: permission}
}
func toDto(acl *m.DashboardAcl) *m.DashboardAclInfoDTO {
return &m.DashboardAclInfoDTO{
OrgId: acl.OrgId,
DashboardId: acl.DashboardId,
UserId: acl.UserId,
TeamId: acl.TeamId,
Role: acl.Role,
Permission: acl.Permission,
PermissionName: acl.Permission.String(),
}
}

View File

@ -7,7 +7,6 @@ package notifications
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"html/template"
"net"
@ -135,7 +134,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
subjectText, hasSubject := subjectData["value"]
if !hasSubject {
return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
return nil, fmt.Errorf("Missing subject in Template %s", cmd.Template)
}
subjectTmpl, err := template.New("subject").Parse(subjectText.(string))

View File

@ -55,9 +55,6 @@ func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *Das
dash.OrgId = cfg.OrgId
dash.Dashboard.OrgId = cfg.OrgId
dash.Dashboard.FolderId = folderId
if !cfg.Editable {
dash.Dashboard.Data.Set("editable", cfg.Editable)
}
if dash.Dashboard.Title == "" {
return nil, models.ErrDashboardTitleEmpty

View File

@ -20,11 +20,7 @@ func Init(ctx context.Context, homePath string, cfg *ini.File) error {
dashboardPath := path.Join(provisioningPath, "dashboards")
_, err := dashboards.Provision(ctx, dashboardPath)
if err != nil {
return err
}
return nil
return err
}
func makeAbsolute(path string, root string) string {

View File

@ -23,12 +23,7 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
return inTransaction(func(sess *DBSession) error {
sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
if err != nil {
return err
}
return nil
return err
})
}

View File

@ -102,11 +102,8 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
existing.Tags = item.Tags
if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil {
return err
}
return nil
_, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing)
return err
})
}

View File

@ -55,7 +55,7 @@ func GetApiKeyById(query *m.GetApiKeyByIdQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrInvalidApiKey
}
@ -69,7 +69,7 @@ func GetApiKeyByName(query *m.GetApiKeyByNameQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrInvalidApiKey
}

View File

@ -63,7 +63,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
if existing.PluginId != "" && !cmd.Overwrite {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
}
@ -172,7 +172,7 @@ func GetDashboard(query *m.GetDashboardQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrDashboardNotFound
}
@ -308,7 +308,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
has, err := sess.Get(&dashboard)
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrDashboardNotFound
}
@ -347,12 +347,7 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
err := x.In("id", query.DashboardIds).Find(&dashboards)
query.Result = dashboards
if err != nil {
return err
}
return nil
return err
}
// GetDashboardPermissionsForUser returns the maximum permission the specified user has for a dashboard(s)
@ -431,12 +426,7 @@ func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
query.Result = dashboards
if err != nil {
return err
}
return nil
return err
}
type DashboardSlugDTO struct {
@ -451,7 +441,7 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
if err != nil {
return err
} else if exists == false {
} else if !exists {
return m.ErrDashboardNotFound
}
@ -479,7 +469,7 @@ func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
if err != nil {
return err
} else if exists == false {
} else if !exists {
return m.ErrDashboardNotFound
}
@ -544,6 +534,10 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
dash.SetId(existingByUid.Id)
dash.SetUid(existingByUid.Uid)
existing = existingByUid
if !dash.IsFolder {
cmd.Result.IsParentFolderChanged = true
}
}
if (existing.IsFolder && !dash.IsFolder) ||
@ -551,6 +545,10 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
return m.ErrDashboardTypeMismatch
}
if !dash.IsFolder && dash.FolderId != existing.FolderId {
cmd.Result.IsParentFolderChanged = true
}
// check for is someone else has written in between
if dash.Version != existing.Version {
if cmd.Overwrite {
@ -561,7 +559,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
if existing.PluginId != "" && !cmd.Overwrite {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
@ -586,6 +584,10 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
return m.ErrDashboardFolderWithSameNameAsDashboard
}
if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) {
cmd.Result.IsParentFolderChanged = true
}
if cmd.Overwrite {
dash.SetId(existing.Id)
dash.SetUid(existing.Uid)
@ -599,6 +601,7 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
}
func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
cmd.Result = &m.ValidateDashboardBeforeSaveResult{}
return inTransaction(func(sess *DBSession) error {
if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil {
return err

View File

@ -35,10 +35,8 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
// Update dashboard HasAcl flag
dashboard := m.Dashboard{HasAcl: true}
if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil {
return err
}
return nil
_, err = sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard)
return err
})
}
@ -92,6 +90,7 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
u.login AS user_login,
u.email AS user_email,
ug.name AS team,
ug.email AS team_email,
d.title,
d.slug,
d.uid,

View File

@ -8,6 +8,7 @@ import (
func init() {
bus.AddHandler("sql", GetProvisionedDashboardDataQuery)
bus.AddHandler("sql", SaveProvisionedDashboard)
bus.AddHandler("sql", GetProvisionedDataByDashboardId)
}
type DashboardExtras struct {
@ -17,6 +18,19 @@ type DashboardExtras struct {
Value string
}
func GetProvisionedDataByDashboardId(cmd *models.IsDashboardProvisionedQuery) error {
result := &models.DashboardProvisioning{}
exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result)
if err != nil {
return err
}
cmd.Result = exist
return nil
}
func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error {
return inTransaction(func(sess *DBSession) error {
err := saveDashboard(sess, cmd.DashboardCmd)

View File

@ -50,6 +50,23 @@ func TestDashboardProvisioningTest(t *testing.T) {
So(query.Result[0].DashboardId, ShouldEqual, dashId)
So(query.Result[0].Updated, ShouldEqual, now.Unix())
})
Convey("Can query for one provisioned dashboard", func() {
query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id}
err := GetProvisionedDataByDashboardId(query)
So(err, ShouldBeNil)
So(query.Result, ShouldBeTrue)
})
Convey("Can query for none provisioned dashboard", func() {
query := &models.IsDashboardProvisionedQuery{DashboardId: 3000}
err := GetProvisionedDataByDashboardId(query)
So(err, ShouldBeNil)
So(query.Result, ShouldBeFalse)
})
})
})
}

View File

@ -19,7 +19,6 @@ func TestIntegratedDashboardService(t *testing.T) {
var testOrgId int64 = 1
Convey("Given saved folders and dashboards in organization A", func() {
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
@ -28,6 +27,11 @@ func TestIntegratedDashboardService(t *testing.T) {
return nil
})
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
cmd.Result = false
return nil
})
savedFolder := saveTestFolder("Saved folder", testOrgId)
savedDashInFolder := saveTestDashboard("Saved dash in folder", testOrgId, savedFolder.Id)
saveTestDashboard("Other saved dash in folder", testOrgId, savedFolder.Id)
@ -74,7 +78,7 @@ func TestIntegratedDashboardService(t *testing.T) {
Convey("Given organization B", func() {
var otherOrgId int64 = 2
Convey("When saving a dashboard with id that are saved in organization A", func() {
Convey("When creating a dashboard with same id as dashboard in organization A", func() {
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -93,7 +97,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
permissionScenario("Given user has permission to save", true, func(sc *dashboardPermissionScenarioContext) {
Convey("When saving a dashboard with uid that are saved in organization A", func() {
Convey("When creating a dashboard with same uid as dashboard in organization A", func() {
var otherOrgId int64 = 2
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
@ -106,7 +110,7 @@ func TestIntegratedDashboardService(t *testing.T) {
res := callSaveWithResult(cmd)
Convey("It should create dashboard in other organization", func() {
Convey("It should create a new dashboard in organization B", func() {
So(res, ShouldNotBeNil)
query := models.GetDashboardQuery{OrgId: otherOrgId, Uid: savedDashInFolder.Uid}
@ -126,7 +130,7 @@ func TestIntegratedDashboardService(t *testing.T) {
permissionScenario("Given user has no permission to save", false, func(sc *dashboardPermissionScenarioContext) {
Convey("When trying to create a new dashboard in the General folder", func() {
Convey("When creating a new dashboard in the General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -138,7 +142,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
Convey("It should create dashboard guardian for General Folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@ -148,7 +152,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to create a new dashboard in other folder", func() {
Convey("When creating a new dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -161,7 +165,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and rsult in access denied error", func() {
Convey("It should create dashboard guardian for other folder with correct arguments and rsult in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@ -171,7 +175,54 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update a dashboard by existing id in the General folder", func() {
Convey("When creating a new dashboard by existing title in folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedDashInFolder.Title,
}),
FolderId: savedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When creating a new dashboard by existing uid in folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "New dash",
}),
FolderId: savedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When updating a dashboard by existing id in the General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -185,7 +236,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
Convey("It should create dashboard guardian for dashboard with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@ -195,7 +246,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update a dashboard by existing id in other folder", func() {
Convey("When updating a dashboard by existing id in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -209,7 +260,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
Convey("It should create dashboard guardian for dashboard with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@ -218,6 +269,102 @@ func TestIntegratedDashboardService(t *testing.T) {
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When moving a dashboard by existing id to other folder from General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInGeneralFolder.Id,
"title": "Dash",
}),
FolderId: otherSavedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for other folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When moving a dashboard by existing id to the General folder from other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"title": "Dash",
}),
FolderId: 0,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for General folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When moving a dashboard by existing uid to other folder from General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInGeneralFolder.Uid,
"title": "Dash",
}),
FolderId: otherSavedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for other folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When moving a dashboard by existing uid to the General folder from other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "Dash",
}),
FolderId: 0,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should create dashboard guardian for General folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
})
// Given user has permission to save
@ -668,7 +815,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing folder to a dashboard using id", func() {
Convey("When updating existing folder to a dashboard using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -687,7 +834,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing dashboard to a folder using id", func() {
Convey("When updating existing dashboard to a folder using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -706,7 +853,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing folder to a dashboard using uid", func() {
Convey("When updating existing folder to a dashboard using uid", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -725,7 +872,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing dashboard to a folder using uid", func() {
Convey("When updating existing dashboard to a folder using uid", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -744,7 +891,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing folder to a dashboard using title", func() {
Convey("When updating existing folder to a dashboard using title", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -762,7 +909,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
Convey("When trying to update existing dashboard to a folder using title", func() {
Convey("When updating existing dashboard to a folder using title", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -850,23 +997,6 @@ func callSaveWithError(cmd models.SaveDashboardCommand) error {
return err
}
func dashboardServiceScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) {
Convey(desc, func() {
origNewDashboardGuardian := guardian.New
guardian.MockDashboardGuardian(mock)
sc := &scenarioContext{
dashboardGuardianMock: mock,
}
defer func() {
guardian.New = origNewDashboardGuardian
}()
fn(sc)
})
}
func saveTestDashboard(title string, orgId int64, folderId int64) *models.Dashboard {
cmd := models.SaveDashboardCommand{
OrgId: orgId,

View File

@ -80,7 +80,7 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrDashboardSnapshotNotFound
}

View File

@ -84,8 +84,7 @@ func (db *BaseDialect) DateTimeFunc(value string) string {
}
func (b *BaseDialect) CreateTableSql(table *Table) string {
var sql string
sql = "CREATE TABLE IF NOT EXISTS "
sql := "CREATE TABLE IF NOT EXISTS "
sql += b.dialect.Quote(table.Name) + " (\n"
pkList := table.PrimaryKeys
@ -162,8 +161,7 @@ func (db *BaseDialect) RenameTable(oldName string, newName string) string {
func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
quote := db.dialect.Quote
var name string
name = index.XName(tableName)
name := index.XName(tableName)
return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName))
}

View File

@ -1,7 +1,6 @@
package migrator
import (
"fmt"
"strings"
)
@ -113,7 +112,7 @@ func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration {
func (m *DropIndexMigration) Sql(dialect Dialect) string {
if m.index.Name == "" {
m.index.Name = fmt.Sprintf("%s", strings.Join(m.index.Cols, "_"))
m.index.Name = strings.Join(m.index.Cols, "_")
}
return dialect.DropIndexSql(m.tableName, m.index)
}

View File

@ -46,7 +46,7 @@ type Index struct {
func (index *Index) XName(tableName string) string {
if index.Name == "" {
index.Name = fmt.Sprintf("%s", strings.Join(index.Cols, "_"))
index.Name = strings.Join(index.Cols, "_")
}
if !strings.HasPrefix(index.Name, "UQE_") &&

View File

@ -36,7 +36,7 @@ func GetPluginSettingById(query *m.GetPluginSettingByIdQuery) error {
has, err := x.Get(&pluginSetting)
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrPluginSettingNotFound
}
query.Result = &pluginSetting

View File

@ -31,7 +31,7 @@ func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error {
has, err := x.Get(&quota)
if err != nil {
return err
} else if has == false {
} else if !has {
quota.Limit = query.Default
}
@ -108,7 +108,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
return err
}
quota.Limit = cmd.Limit
if has == false {
if !has {
quota.Created = time.Now()
//No quota in the DB for this target, so create a new one.
if _, err := sess.Insert(&quota); err != nil {
@ -133,7 +133,7 @@ func GetUserQuotaByTarget(query *m.GetUserQuotaByTargetQuery) error {
has, err := x.Get(&quota)
if err != nil {
return err
} else if has == false {
} else if !has {
quota.Limit = query.Default
}
@ -210,7 +210,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
return err
}
quota.Limit = cmd.Limit
if has == false {
if !has {
quota.Created = time.Now()
//No quota in the DB for this target, so create a new one.
if _, err := sess.Insert(&quota); err != nil {

View File

@ -19,10 +19,6 @@ func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
query.Result = make([]*m.DataSourceStats, 0)
err := x.SQL(rawSql).Find(&query.Result)
if err != nil {
return err
}
return err
}
@ -68,6 +64,7 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
}
query.Result = &stats
return err
}

View File

@ -210,11 +210,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
err := sess.Find(&query.Result)
if err != nil {
return err
}
return nil
return err
}
// AddTeamMember adds a user to a team

View File

@ -126,7 +126,7 @@ func GetTempUserByCode(query *m.GetTempUserByCodeQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrTempUserNotFound
}

View File

@ -154,7 +154,7 @@ func GetUserById(query *m.GetUserByIdQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrUserNotFound
}
@ -179,7 +179,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
return err
}
if has == false && strings.Contains(query.LoginOrEmail, "@") {
if !has && strings.Contains(query.LoginOrEmail, "@") {
// If the user wasn't found, and it contains an "@" fallback to finding the
// user by email.
user = &m.User{Email: query.LoginOrEmail}
@ -188,7 +188,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrUserNotFound
}
@ -209,7 +209,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrUserNotFound
}
@ -253,11 +253,8 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
Updated: time.Now(),
}
if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
return err
}
return nil
_, err := sess.Id(cmd.UserId).Update(&user)
return err
})
}
@ -271,11 +268,8 @@ func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error {
LastSeenAt: time.Now(),
}
if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
return err
}
return nil
_, err := sess.Id(cmd.UserId).Update(&user)
return err
})
}
@ -310,7 +304,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
if err != nil {
return err
} else if has == false {
} else if !has {
return m.ErrUserNotFound
}
@ -479,10 +473,7 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
Updated: time.Now(),
}
if _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user); err != nil {
return err
}
return nil
_, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user)
return err
})
}

View File

@ -182,7 +182,7 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
var data UserInfoJson
var err error
if s.extractToken(&data, token) != true {
if !s.extractToken(&data, token) {
response, err := HttpGet(client, s.apiUrl)
if err != nil {
return nil, fmt.Errorf("Error getting user info: %s", err)

View File

@ -72,7 +72,7 @@ func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryCo
MetricName: aws.String(metricName),
Dimensions: qd,
Statistic: aws.String(s),
Period: aws.Int64(int64(period)),
Period: aws.Int64(period),
}
resp, err := svc.DescribeAlarmsForMetric(params)
if err != nil {
@ -88,7 +88,7 @@ func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryCo
MetricName: aws.String(metricName),
Dimensions: qd,
ExtendedStatistic: aws.String(s),
Period: aws.Int64(int64(period)),
Period: aws.Int64(period),
}
resp, err := svc.DescribeAlarmsForMetric(params)
if err != nil {

View File

@ -71,15 +71,12 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
switch queryType {
case "metricFindQuery":
result, err = e.executeMetricFindQuery(ctx, queryContext)
break
case "annotationQuery":
result, err = e.executeAnnotationQuery(ctx, queryContext)
break
case "timeSeriesQuery":
fallthrough
default:
result, err = e.executeTimeSeriesQuery(ctx, queryContext)
break
}
return result, err

View File

@ -175,25 +175,18 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo
switch subType {
case "regions":
data, err = e.handleGetRegions(ctx, parameters, queryContext)
break
case "namespaces":
data, err = e.handleGetNamespaces(ctx, parameters, queryContext)
break
case "metrics":
data, err = e.handleGetMetrics(ctx, parameters, queryContext)
break
case "dimension_keys":
data, err = e.handleGetDimensions(ctx, parameters, queryContext)
break
case "dimension_values":
data, err = e.handleGetDimensionValues(ctx, parameters, queryContext)
break
case "ebs_volume_ids":
data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
break
case "ec2_instance_attribute":
data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
break
}
transformToTable(data, queryResult)
@ -261,7 +254,7 @@ func (e *CloudWatchExecutor) handleGetNamespaces(ctx context.Context, parameters
keys = append(keys, strings.Split(customNamespaces, ",")...)
}
sort.Sort(sort.StringSlice(keys))
sort.Strings(keys)
result := make([]suggestData, 0)
for _, key := range keys {
@ -290,7 +283,7 @@ func (e *CloudWatchExecutor) handleGetMetrics(ctx context.Context, parameters *s
return nil, errors.New("Unable to call AWS API")
}
}
sort.Sort(sort.StringSlice(namespaceMetrics))
sort.Strings(namespaceMetrics)
result := make([]suggestData, 0)
for _, name := range namespaceMetrics {
@ -319,7 +312,7 @@ func (e *CloudWatchExecutor) handleGetDimensions(ctx context.Context, parameters
return nil, errors.New("Unable to call AWS API")
}
}
sort.Sort(sort.StringSlice(dimensionValues))
sort.Strings(dimensionValues)
result := make([]suggestData, 0)
for _, name := range dimensionValues {
@ -573,11 +566,7 @@ func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error)
}
return !lastPage
})
if err != nil {
return resp, err
}
return resp, nil
return resp, err
}
var metricsCacheLock sync.Mutex

View File

@ -181,10 +181,7 @@ func TestCloudWatchMetrics(t *testing.T) {
}
func TestParseMultiSelectValue(t *testing.T) {
var values []string
values = parseMultiSelectValue(" i-someInstance ")
values := parseMultiSelectValue(" i-someInstance ")
assert.Equal(t, []string{"i-someInstance"}, values)
values = parseMultiSelectValue("{i-05}")

View File

@ -145,7 +145,7 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.
// convert types not handled by denisenkom/go-mssqldb
// unhandled types are returned as []byte
for i := 0; i < len(types); i++ {
if value, ok := values[i].([]byte); ok == true {
if value, ok := values[i].([]byte); ok {
switch types[i].DatabaseTypeName() {
case "MONEY", "SMALLMONEY", "DECIMAL":
if v, err := strconv.ParseFloat(string(value), 64); err == nil {
@ -209,7 +209,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
fillValue := null.Float{}
if fillMissing {
fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
if query.Model.Get("fillNull").MustBool(false) == false {
if !query.Model.Get("fillNull").MustBool(false) {
fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
fillValue.Valid = true
}
@ -244,7 +244,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
}
if metricIndex >= 0 {
if columnValue, ok := values[metricIndex].(string); ok == true {
if columnValue, ok := values[metricIndex].(string); ok {
metric = columnValue
} else {
return fmt.Errorf("Column metric must be of type CHAR, VARCHAR, NCHAR or NVARCHAR. metric column name: %s type: %s but datatype is %T", columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex])
@ -271,7 +271,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
}
series, exist := pointsBySeries[metric]
if exist == false {
if !exist {
series = &tsdb.TimeSeries{Name: metric}
pointsBySeries[metric] = series
seriesByQueryOrder.PushBack(metric)
@ -279,7 +279,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
if fillMissing {
var intervalStart float64
if exist == false {
if !exist {
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
} else {
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval

View File

@ -218,7 +218,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
fillValue := null.Float{}
if fillMissing {
fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
if query.Model.Get("fillNull").MustBool(false) == false {
if !query.Model.Get("fillNull").MustBool(false) {
fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
fillValue.Valid = true
}
@ -253,7 +253,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
}
if metricIndex >= 0 {
if columnValue, ok := values[metricIndex].(string); ok == true {
if columnValue, ok := values[metricIndex].(string); ok {
metric = columnValue
} else {
return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
@ -280,7 +280,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
}
series, exist := pointsBySeries[metric]
if exist == false {
if !exist {
series = &tsdb.TimeSeries{Name: metric}
pointsBySeries[metric] = series
seriesByQueryOrder.PushBack(metric)
@ -288,7 +288,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
if fillMissing {
var intervalStart float64
if exist == false {
if !exist {
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
} else {
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval

View File

@ -79,15 +79,15 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
}
return fmt.Sprintf("extract(epoch from %s) as \"time\"", args[0]), nil
case "__timeFilter":
// don't use to_timestamp in this macro for redshift compatibility #9566
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
return fmt.Sprintf("extract(epoch from %s) BETWEEN %d AND %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), m.TimeRange.GetToAsSecondsEpoch()), nil
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeFrom":
return fmt.Sprintf("to_timestamp(%d)", m.TimeRange.GetFromAsSecondsEpoch()), nil
return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__timeTo":
return fmt.Sprintf("to_timestamp(%d)", m.TimeRange.GetToAsSecondsEpoch()), nil
return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)

View File

@ -12,7 +12,7 @@ import (
func TestMacroEngine(t *testing.T) {
Convey("MacroEngine", t, func() {
engine := &PostgresMacroEngine{}
engine := NewPostgresMacroEngine()
query := &tsdb.Query{}
Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
@ -38,14 +38,14 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE extract(epoch from time_column) BETWEEN %d AND %d", from.Unix(), to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", from.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
Convey("interpolate __timeGroup function", func() {
@ -68,7 +68,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
})
Convey("interpolate __unixEpochFilter function", func() {
@ -102,21 +102,21 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE extract(epoch from time_column) BETWEEN %d AND %d", from.Unix(), to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", from.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
})
Convey("interpolate __unixEpochFilter function", func() {
@ -150,21 +150,21 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("WHERE extract(epoch from time_column) BETWEEN %d AND %d", from.Unix(), to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", from.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select to_timestamp(%d)", to.Unix()))
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
})
Convey("interpolate __unixEpochFilter function", func() {

View File

@ -131,7 +131,7 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
// convert types not handled by lib/pq
// unhandled types are returned as []byte
for i := 0; i < len(types); i++ {
if value, ok := values[i].([]byte); ok == true {
if value, ok := values[i].([]byte); ok {
switch types[i].DatabaseTypeName() {
case "NUMERIC":
if v, err := strconv.ParseFloat(string(value), 64); err == nil {
@ -198,7 +198,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
fillValue := null.Float{}
if fillMissing {
fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
if query.Model.Get("fillNull").MustBool(false) == false {
if !query.Model.Get("fillNull").MustBool(false) {
fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
fillValue.Valid = true
}
@ -233,7 +233,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
}
if metricIndex >= 0 {
if columnValue, ok := values[metricIndex].(string); ok == true {
if columnValue, ok := values[metricIndex].(string); ok {
metric = columnValue
} else {
return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
@ -260,7 +260,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
}
series, exist := pointsBySeries[metric]
if exist == false {
if !exist {
series = &tsdb.TimeSeries{Name: metric}
pointsBySeries[metric] = series
seriesByQueryOrder.PushBack(metric)
@ -268,7 +268,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
if fillMissing {
var intervalStart float64
if exist == false {
if !exist {
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
} else {
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval

View File

@ -108,8 +108,8 @@ func (e *PrometheusExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus")
span.SetTag("expr", query.Expr)
span.SetTag("start_unixnano", int64(query.Start.UnixNano()))
span.SetTag("stop_unixnano", int64(query.End.UnixNano()))
span.SetTag("start_unixnano", query.Start.UnixNano())
span.SetTag("stop_unixnano", query.End.UnixNano())
defer span.Finish()
value, err := client.QueryRange(ctx, query.Expr, timeRange)

View File

@ -51,7 +51,7 @@ func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSour
defer engineCache.Unlock()
if engine, present := engineCache.cache[dsInfo.Id]; present {
if version, _ := engineCache.versions[dsInfo.Id]; version == dsInfo.Version {
if version := engineCache.versions[dsInfo.Id]; version == dsInfo.Version {
e.XormEngine = engine
return nil
}

View File

@ -37,6 +37,10 @@ func (tr *TimeRange) GetFromAsSecondsEpoch() int64 {
return tr.GetFromAsMsEpoch() / 1000
}
func (tr *TimeRange) GetFromAsTimeUTC() time.Time {
return tr.MustGetFrom().UTC()
}
func (tr *TimeRange) GetToAsMsEpoch() int64 {
return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
}
@ -45,6 +49,10 @@ func (tr *TimeRange) GetToAsSecondsEpoch() int64 {
return tr.GetToAsMsEpoch() / 1000
}
func (tr *TimeRange) GetToAsTimeUTC() time.Time {
return tr.MustGetTo().UTC()
}
func (tr *TimeRange) MustGetFrom() time.Time {
if res, err := tr.ParseFrom(); err != nil {
return time.Unix(0, 0)

View File

@ -17,11 +17,7 @@ func init() {
// IsValidShortUid checks if short unique identifier contains valid characters
func IsValidShortUid(uid string) bool {
if !validUidPattern(uid) {
return false
}
return true
return validUidPattern(uid)
}
// GenerateShortUid generates a short unique identifier.

View File

@ -39,7 +39,7 @@ class AddPermissions extends Component<IProps, any> {
permissions.newItem.setUser(null, null);
return;
}
return permissions.newItem.setUser(user.id, user.login);
return permissions.newItem.setUser(user.id, user.login, user.avatarUrl);
}
teamPicked(team: Team) {
@ -48,7 +48,7 @@ class AddPermissions extends Component<IProps, any> {
permissions.newItem.setTeam(null, null);
return;
}
return permissions.newItem.setTeam(team.id, team.name);
return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl);
}
permissionPicked(permission: OptionWithDescription) {

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { Component } from 'react';
import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
@ -12,9 +12,12 @@ export default class DisabledPermissionListItem extends Component<IProps, any> {
return (
<tr className="gf-form-disabled">
<td style={{ width: '100%' }}>
<i className={`fa--permissions-list ${item.icon}`} />
<span dangerouslySetInnerHTML={{ __html: item.nameHtml }} />
<td style={{ width: '1%' }}>
<i style={{ width: '25px', height: '25px' }} className="gicon gicon-shield" />
</td>
<td style={{ width: '90%' }}>
{item.name}
<span className="filter-table__weak-italic"> (Role)</span>
</td>
<td />
<td className="query-keyword">Can</td>

View File

@ -15,9 +15,8 @@ export interface DashboardAcl {
permissionName?: string;
role?: string;
icon?: string;
nameHtml?: string;
name?: string;
inherited?: boolean;
sortName?: string;
sortRank?: number;
}

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { Component } from 'react';
import PermissionsListItem from './PermissionsListItem';
import DisabledPermissionsListItem from './DisabledPermissionsListItem';
import { observer } from 'mobx-react';
@ -23,7 +23,7 @@ class PermissionsList extends Component<IProps, any> {
<DisabledPermissionsListItem
key={0}
item={{
nameHtml: 'Everyone with <span class="query-keyword">Admin</span> Role',
name: 'Admin',
permission: 4,
icon: 'fa fa-fw fa-street-view',
}}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React from 'react';
import { observer } from 'mobx-react';
import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
@ -7,6 +7,30 @@ const setClassNameHelper = inherited => {
return inherited ? 'gf-form-disabled' : '';
};
function ItemAvatar({ item }) {
if (item.userAvatarUrl) {
return <img className="filter-table__avatar" src={item.userAvatarUrl} />;
}
if (item.teamAvatarUrl) {
return <img className="filter-table__avatar" src={item.teamAvatarUrl} />;
}
if (item.role === 'Editor') {
return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-editor" />;
}
return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-viewer" />;
}
function ItemDescription({ item }) {
if (item.userId) {
return <span className="filter-table__weak-italic">(User)</span>;
}
if (item.teamId) {
return <span className="filter-table__weak-italic">(Team)</span>;
}
return <span className="filter-table__weak-italic">(Role)</span>;
}
export default observer(({ item, removeItem, permissionChanged, itemIndex, folderInfo }) => {
const handleRemoveItem = evt => {
evt.preventDefault();
@ -21,9 +45,11 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde
return (
<tr className={setClassNameHelper(item.inherited)}>
<td style={{ width: '100%' }}>
<i className={`fa--permissions-list ${item.icon}`} />
<span dangerouslySetInnerHTML={{ __html: item.nameHtml }} />
<td style={{ width: '1%' }}>
<ItemAvatar item={item} />
</td>
<td style={{ width: '90%' }}>
{item.name} <ItemDescription item={item} />
</td>
<td>
{item.inherited &&

View File

@ -37,7 +37,7 @@
<i class="fa fa-fw fa-sign-in"></i>
</span>
</a>
<a href="{{ctrl.loginUrl}}">
<a href="{{ctrl.loginUrl}}" target="_self">
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
<li class="side-menu-header">
<span class="sidemenu-item-text">Sign In</span>

View File

@ -6,6 +6,7 @@ import './dashnav/dashnav';
import './submenu/submenu';
import './save_as_modal';
import './save_modal';
import './save_provisioned_modal';
import './shareModalCtrl';
import './share_snapshot_ctrl';
import './dashboard_srv';

View File

@ -0,0 +1,186 @@
import angular from 'angular';
import _ from 'lodash';
import { DashboardModel } from './dashboard_model';
export class ChangeTracker {
current: any;
originalPath: any;
scope: any;
original: any;
next: any;
$window: any;
/** @ngInject */
constructor(
dashboard,
scope,
originalCopyDelay,
private $location,
$window,
private $timeout,
private contextSrv,
private $rootScope
) {
this.$location = $location;
this.$window = $window;
this.current = dashboard;
this.originalPath = $location.path();
this.scope = scope;
// register events
scope.onAppEvent('dashboard-saved', () => {
this.original = this.current.getSaveModelClone();
this.originalPath = $location.path();
});
$window.onbeforeunload = () => {
if (this.ignoreChanges()) {
return undefined;
}
if (this.hasChanges()) {
return 'There are unsaved changes to this dashboard';
}
return undefined;
};
scope.$on('$locationChangeStart', (event, next) => {
// check if we should look for changes
if (this.originalPath === $location.path()) {
return true;
}
if (this.ignoreChanges()) {
return true;
}
if (this.hasChanges()) {
event.preventDefault();
this.next = next;
this.$timeout(() => {
this.open_modal();
});
}
return false;
});
if (originalCopyDelay) {
this.$timeout(() => {
// wait for different services to patch the dashboard (missing properties)
this.original = dashboard.getSaveModelClone();
}, originalCopyDelay);
} else {
this.original = dashboard.getSaveModelClone();
}
}
// for some dashboards and users
// changes should be ignored
ignoreChanges() {
if (!this.original) {
return true;
}
if (!this.contextSrv.isEditor) {
return true;
}
if (!this.current || !this.current.meta) {
return true;
}
var meta = this.current.meta;
return !meta.canSave || meta.fromScript || meta.fromFile;
}
// remove stuff that should not count in diff
cleanDashboardFromIgnoredChanges(dashData) {
// need to new up the domain model class to get access to expand / collapse row logic
let model = new DashboardModel(dashData);
// Expand all rows before making comparison. This is required because row expand / collapse
// change order of panel array and panel positions.
model.expandRows();
let dash = model.getSaveModelClone();
// ignore time and refresh
dash.time = 0;
dash.refresh = 0;
dash.schemaVersion = 0;
// ignore iteration property
delete dash.iteration;
dash.panels = _.filter(dash.panels, panel => {
if (panel.repeatPanelId) {
return false;
}
// remove scopedVars
panel.scopedVars = null;
// ignore panel legend sort
if (panel.legend) {
delete panel.legend.sort;
delete panel.legend.sortDesc;
}
return true;
});
// ignore template variable values
_.each(dash.templating.list, function(value) {
value.current = null;
value.options = null;
value.filters = null;
});
return dash;
}
hasChanges() {
let current = this.cleanDashboardFromIgnoredChanges(this.current.getSaveModelClone());
let original = this.cleanDashboardFromIgnoredChanges(this.original);
var currentTimepicker = _.find(current.nav, { type: 'timepicker' });
var originalTimepicker = _.find(original.nav, { type: 'timepicker' });
if (currentTimepicker && originalTimepicker) {
currentTimepicker.now = originalTimepicker.now;
}
var currentJson = angular.toJson(current, true);
var originalJson = angular.toJson(original, true);
return currentJson !== originalJson;
}
discardChanges() {
this.original = null;
this.gotoNext();
}
open_modal() {
this.$rootScope.appEvent('show-modal', {
templateHtml: '<unsaved-changes-modal dismiss="dismiss()"></unsaved-changes-modal>',
modalClass: 'modal--narrow confirm-modal',
});
}
saveChanges() {
var self = this;
var cancel = this.$rootScope.$on('dashboard-saved', () => {
cancel();
this.$timeout(() => {
self.gotoNext();
});
});
this.$rootScope.appEvent('save-dashboard');
}
gotoNext() {
var baseLen = this.$location.absUrl().length - this.$location.url().length;
var nextUrl = this.next.substring(baseLen);
this.$location.url(nextUrl);
}
}

View File

@ -649,6 +649,7 @@ export class DashboardModel {
for (let panel of row.panels) {
// make sure y is adjusted (in case row moved while collapsed)
// console.log('yDiff', yDiff);
panel.gridPos.y -= yDiff;
// insert after row
this.panels.splice(insertPos, 0, new PanelModel(panel));
@ -657,7 +658,7 @@ export class DashboardModel {
yMax = Math.max(yMax, panel.gridPos.y + panel.gridPos.h);
}
const pushDownAmount = yMax - row.gridPos.y;
const pushDownAmount = yMax - row.gridPos.y - 1;
// push panels below down
for (let panelIndex = insertPos; panelIndex < this.panels.length; panelIndex++) {

View File

@ -105,6 +105,10 @@ export class DashboardSrv {
this.setCurrent(this.create(clone, this.dash.meta));
}
if (this.dash.meta.provisioned) {
return this.showDashboardProvisionedModal();
}
if (!this.dash.meta.canSave && options.makeEditable !== true) {
return Promise.resolve();
}
@ -120,6 +124,12 @@ export class DashboardSrv {
return this.save(this.dash.getSaveModelClone(), options);
}
showDashboardProvisionedModal() {
this.$rootScope.appEvent('show-modal', {
templateHtml: '<save-provisioned-dashboard-modal dismiss="dismiss()"></save-provisioned-dashboard-modal>',
});
}
showSaveAsModal() {
this.$rootScope.appEvent('show-modal', {
templateHtml: '<save-dashboard-as-modal dismiss="dismiss()"></save-dashboard-as-modal>',

View File

@ -19,9 +19,12 @@ export class FolderPickerCtrl {
newFolderNameTouched: boolean;
hasValidationError: boolean;
validationError: any;
isEditor: boolean;
/** @ngInject */
constructor(private backendSrv, private validationSrv) {
constructor(private backendSrv, private validationSrv, private contextSrv) {
this.isEditor = this.contextSrv.isEditor;
if (!this.labelClass) {
this.labelClass = 'width-7';
}
@ -38,19 +41,20 @@ export class FolderPickerCtrl {
return this.backendSrv.get('api/search', params).then(result => {
if (
query === '' ||
query.toLowerCase() === 'g' ||
query.toLowerCase() === 'ge' ||
query.toLowerCase() === 'gen' ||
query.toLowerCase() === 'gene' ||
query.toLowerCase() === 'gener' ||
query.toLowerCase() === 'genera' ||
query.toLowerCase() === 'general'
this.isEditor &&
(query === '' ||
query.toLowerCase() === 'g' ||
query.toLowerCase() === 'ge' ||
query.toLowerCase() === 'gen' ||
query.toLowerCase() === 'gene' ||
query.toLowerCase() === 'gener' ||
query.toLowerCase() === 'genera' ||
query.toLowerCase() === 'general')
) {
result.unshift({ title: this.rootName, id: 0 });
}
if (this.enableCreateNew && query === '') {
if (this.isEditor && this.enableCreateNew && query === '') {
result.unshift({ title: '-- New Folder --', id: -1 });
}

View File

@ -0,0 +1,77 @@
import angular from 'angular';
import { saveAs } from 'file-saver';
import coreModule from 'app/core/core_module';
const template = `
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<i class="fa fa-save"></i><span class="p-l-1">Cannot save provisioned dashboard</span>
</h2>
<a class="modal-header-close" ng-click="ctrl.dismiss();">
<i class="fa fa-remove"></i>
</a>
</div>
<div class="modal-content">
<small>
This dashboard cannot be saved from Grafana's UI since it has been provisioned from another source.
Copy the JSON or save it to a file below. Then you can update your dashboard in corresponding provisioning source.<br/>
<i>See <a class="external-link" href="http://docs.grafana.org/administration/provisioning/#dashboards" target="_blank">
documentation</a> for more information about provisioning.</i>
</small>
<div class="p-t-2">
<div class="gf-form">
<code-editor content="ctrl.dashboardJson" data-mode="json" data-max-lines=15></code-editor>
</div>
<div class="gf-form-button-row">
<button class="btn btn-success" clipboard-button="ctrl.getJsonForClipboard()">
<i class="fa fa-clipboard"></i>&nbsp;Copy JSON to Clipboard
</button>
<button class="btn btn-secondary" clipboard-button="ctrl.save()">
<i class="fa fa-save"></i>&nbsp;Save JSON to file
</button>
<a class="btn btn-link" ng-click="ctrl.dismiss();">Cancel</a>
</div>
</div>
</div>
</div>
`;
export class SaveProvisionedDashboardModalCtrl {
dash: any;
dashboardJson: string;
dismiss: () => void;
/** @ngInject */
constructor(dashboardSrv) {
this.dash = dashboardSrv.getCurrent().getSaveModelClone();
delete this.dash.id;
this.dashboardJson = JSON.stringify(this.dash, null, 2);
}
save() {
var blob = new Blob([angular.toJson(this.dash, true)], {
type: 'application/json;charset=utf-8',
});
saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json');
}
getJsonForClipboard() {
return this.dashboardJson;
}
}
export function saveProvisionedDashboardModalDirective() {
return {
restrict: 'E',
template: template,
controller: SaveProvisionedDashboardModalCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: { dismiss: '&' },
};
}
coreModule.directive('saveProvisionedDashboardModal', saveProvisionedDashboardModalDirective);

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