From 401b01e1e6e08aa3818ec34f56a59bdf7df7bb58 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 29 Jan 2018 14:57:19 +0100 Subject: [PATCH 01/84] db: add new column uid to the dashboard table. #7883 --- pkg/services/sqlstore/migrations/dashboard_mig.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 4f1602be931..a17338b73ae 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -150,4 +150,9 @@ func addDashboardMigration(mg *Migrator) { mg.AddMigration("Add column has_acl in dashboard", NewAddColumnMigration(dashboardV2, &Column{ Name: "has_acl", Type: DB_Bool, Nullable: false, Default: "0", })) + + // new uid column + mg.AddMigration("Add column uid in dashboard", NewAddColumnMigration(dashboardV2, &Column{ + Name: "uid", Type: DB_NVarchar, Length: 12, Nullable: true, + })) } From 50aa9ec69c7d8dc836c23f5ce7891c86e0a19764 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 29 Jan 2018 16:24:22 +0100 Subject: [PATCH 02/84] db: add migrations for generating uid for existing dashboards. #7883 --- pkg/services/sqlstore/migrations/dashboard_mig.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index a17338b73ae..e67a2adf73d 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -155,4 +155,9 @@ func addDashboardMigration(mg *Migrator) { mg.AddMigration("Add column uid in dashboard", NewAddColumnMigration(dashboardV2, &Column{ Name: "uid", Type: DB_NVarchar, Length: 12, Nullable: true, })) + + mg.AddMigration("Set uid column values", new(RawSqlMigration). + Sqlite("UPDATE dashboard SET uid=printf('%09d',id) WHERE uid IS NULL;"). + Postgres("UPDATE dashboard SET uid=lpad('' || id,9,'0') WHERE uid IS NULL;"). + Mysql("UPDATE dashboard SET uid=lpad(id,9,'0') WHERE uid IS NULL;")) } From 025a14ec2465d2d028762b06f49239bf2bc56733 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 29 Jan 2018 16:27:52 +0100 Subject: [PATCH 03/84] db: add migrations for creating a unique index for uid. #7883 --- pkg/services/sqlstore/migrations/dashboard_mig.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index e67a2adf73d..1035ef35015 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -160,4 +160,8 @@ func addDashboardMigration(mg *Migrator) { Sqlite("UPDATE dashboard SET uid=printf('%09d',id) WHERE uid IS NULL;"). Postgres("UPDATE dashboard SET uid=lpad('' || id,9,'0') WHERE uid IS NULL;"). Mysql("UPDATE dashboard SET uid=lpad(id,9,'0') WHERE uid IS NULL;")) + + mg.AddMigration("Add index for uid in dashboard", NewAddIndexMigration(dashboardV2, &Index{ + Cols: []string{"uid"}, Type: UniqueIndex, + })) } From 5b35c694dc9b64fe023b8fae3c04d83661a52e66 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 29 Jan 2018 17:20:18 +0100 Subject: [PATCH 04/84] dashboard: generate and include uid in dashboard model. #7883 --- Gopkg.lock | 8 +- Gopkg.toml | 4 + pkg/models/dashboards.go | 15 + pkg/services/sqlstore/dashboard.go | 1 + vendor/github.com/teris-io/shortid/LICENSE | 18 + vendor/github.com/teris-io/shortid/shortid.go | 362 ++++++++++++++++++ 6 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 vendor/github.com/teris-io/shortid/LICENSE create mode 100644 vendor/github.com/teris-io/shortid/shortid.go diff --git a/Gopkg.lock b/Gopkg.lock index 4d5f6ca0a46..8d82b29d622 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -412,6 +412,12 @@ revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857" version = "1.6.3" +[[projects]] + branch = "master" + name = "github.com/teris-io/shortid" + packages = ["."] + revision = "771a37caa5cf0c81f585d7b6df4dfc77e0615b5c" + [[projects]] name = "github.com/uber/jaeger-client-go" packages = [ @@ -625,6 +631,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "98e8d8f5fb21fe448aeb3db41c9fed85fe3bf80400e553211cf39a9c05720e01" + inputs-digest = "4de68f1342ba98a637ec8ca7496aeeae2021bf9e4c7c80db7924e14709151a62" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 144dcc1e4af..22c56d29c71 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -193,3 +193,7 @@ ignored = [ non-go = true go-tests = true unused-packages = true + +[[constraint]] + branch = "master" + name = "github.com/teris-io/shortid" diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 091f27ec413..b377e184829 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -7,6 +7,7 @@ import ( "github.com/gosimple/slug" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/teris-io/shortid" ) // Typed errors @@ -39,6 +40,7 @@ var ( // Dashboard model type Dashboard struct { Id int64 + Uid string Slug string OrgId int64 GnetId int64 @@ -61,6 +63,7 @@ type Dashboard struct { // NewDashboard creates a new dashboard func NewDashboard(title string) *Dashboard { dash := &Dashboard{} + dash.Uid = DashboardUid() dash.Data = simplejson.New() dash.Data.Set("title", title) dash.Title = title @@ -107,9 +110,21 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { dash.GnetId = int64(gnetId) } + if uid, err := dash.Data.Get("uid").String(); err == nil { + dash.Uid = uid + } else { + dash.Uid = DashboardUid() + } + return dash } +func DashboardUid() string { + gen, _ := shortid.New(1, shortid.DefaultABC, 1) + uid, _ := gen.Generate() + return uid +} + // GetDashboardModel turns the command into the savable model func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash := NewDashboardFromJson(cmd.Dashboard) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 0b6b60a5e11..b8d3d9a36a5 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -175,6 +175,7 @@ func GetDashboard(query *m.GetDashboardQuery) error { } dashboard.Data.Set("id", dashboard.Id) + dashboard.Data.Set("uid", dashboard.Uid) query.Result = &dashboard return nil } diff --git a/vendor/github.com/teris-io/shortid/LICENSE b/vendor/github.com/teris-io/shortid/LICENSE new file mode 100644 index 00000000000..fc737ef1ce8 --- /dev/null +++ b/vendor/github.com/teris-io/shortid/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/teris-io/shortid/shortid.go b/vendor/github.com/teris-io/shortid/shortid.go new file mode 100644 index 00000000000..c9461c29168 --- /dev/null +++ b/vendor/github.com/teris-io/shortid/shortid.go @@ -0,0 +1,362 @@ +// Copyright (c) 2016-2017. Oleg Sklyar & teris.io. All rights reserved. +// See the LICENSE file in the project root for licensing information. + +// Original algorithm: +// Copyright (c) 2015 Dylan Greene, contributors: https://github.com/dylang/shortid. +// MIT-license as found in the LICENSE file. + +// Seed computation: based on The Central Randomizer 1.3 +// Copyright (c) 1997 Paul Houle (houle@msc.cornell.edu) + +// Package shortid enables the generation of short, unique, non-sequential and by default URL friendly +// Ids. The package is heavily inspired by the node.js https://github.com/dylang/shortid library. +// +// Id Length +// +// The standard Id length is 9 symbols when generated at a rate of 1 Id per millisecond, +// occasionally it reaches 11 (at the rate of a few thousand Ids per millisecond) and very-very +// rarely it can go beyond that during continuous generation at full throttle on high-performant +// hardware. A test generating 500k Ids at full throttle on conventional hardware generated the +// following Ids at the head and the tail (length > 9 is expected for this test): +// +// -NDveu-9Q +// iNove6iQ9J +// NVDve6-9Q +// VVDvc6i99J +// NVovc6-QQy +// VVoveui9QC +// ... +// tFmGc6iQQs +// KpTvcui99k +// KFTGcuiQ9p +// KFmGeu-Q9O +// tFTvcu-QQt +// tpTveu-99u +// +// Life span +// +// The package guarantees the generation of unique Ids with zero collisions for 34 years +// (1/1/2016-1/1/2050) using the same worker Id within a single (although concurrent) application if +// application restarts take longer than 1 millisecond. The package supports up to 32 works, all +// providing unique sequences. +// +// Implementation details +// +// Although heavily inspired by the node.js shortid library this is +// not a simple Go port. In addition it +// +// - is safe to concurrency; +// - does not require any yearly version/epoch resets; +// - provides stable Id size over a long period at the rate of 1ms; +// - guarantees no collisions (due to guaranteed fixed size of Ids between milliseconds and because +// multiple requests within the same ms lead to longer Ids with the prefix unique to the ms); +// - supports 32 over 16 workers. +// +// The algorithm uses less randomness than the original node.js implementation, which permits to +// extend the life span as well as reduce and guarantee the length. In general terms, each Id +// has the following 3 pieces of information encoded: the millisecond (first 8 symbols), the worker +// Id (9th symbol), running concurrent counter within the same millisecond, only if required, over +// all remaining symbols. The element of randomness per symbol is 1/2 for the worker and the +// millisecond and 0 for the counter. Here 0 means no randomness, i.e. every value is encoded using +// a 64-base alphabet; 1/2 means one of two matching symbols of the supplied alphabet, 1/4 one of +// four matching symbols. The original algorithm of the node.js module uses 1/4 throughout. +// +// All methods accepting the parameters that govern the randomness are exported and can be used +// to directly implement an algorithm with e.g. more randomness, but with longer Ids and shorter +// life spans. +package shortid + +import ( + randc "crypto/rand" + "errors" + "fmt" + "math" + randm "math/rand" + "sync" + "sync/atomic" + "time" + "unsafe" +) + +// Version defined the library version. +const Version = 1.1 + +// DefaultABC is the default URL-friendly alphabet. +const DefaultABC = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-" + +// Abc represents a shuffled alphabet used to generate the Ids and provides methods to +// encode data. +type Abc struct { + alphabet []rune +} + +// Shortid type represents a short Id generator working with a given alphabet. +type Shortid struct { + abc Abc + worker uint + epoch time.Time // ids can be generated for 34 years since this date + ms uint // ms since epoch for the last id + count uint // request count within the same ms + mx sync.Mutex // locks access to ms and count +} + +var shortid *Shortid + +func init() { + shortid = MustNew(0, DefaultABC, 1) +} + +// GetDefault retrieves the default short Id generator initialised with the default alphabet, +// worker=0 and seed=1. The default can be overwritten using SetDefault. +func GetDefault() *Shortid { + return (*Shortid)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&shortid)))) +} + +// SetDefault overwrites the default generator. +func SetDefault(sid *Shortid) { + target := (*unsafe.Pointer)(unsafe.Pointer(&shortid)) + source := unsafe.Pointer(sid) + atomic.SwapPointer(target, source) +} + +// Generate generates an Id using the default generator. +func Generate() (string, error) { + return shortid.Generate() +} + +// MustGenerate acts just like Generate, but panics instead of returning errors. +func MustGenerate() string { + id, err := Generate() + if err == nil { + return id + } + panic(err) +} + +// New constructs an instance of the short Id generator for the given worker number [0,31], alphabet +// (64 unique symbols) and seed value (to shuffle the alphabet). The worker number should be +// different for multiple or distributed processes generating Ids into the same data space. The +// seed, on contrary, should be identical. +func New(worker uint8, alphabet string, seed uint64) (*Shortid, error) { + if worker > 31 { + return nil, errors.New("expected worker in the range [0,31]") + } + abc, err := NewAbc(alphabet, seed) + if err == nil { + sid := &Shortid{ + abc: abc, + worker: uint(worker), + epoch: time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC), + ms: 0, + count: 0, + } + return sid, nil + } + return nil, err +} + +// MustNew acts just like New, but panics instead of returning errors. +func MustNew(worker uint8, alphabet string, seed uint64) *Shortid { + sid, err := New(worker, alphabet, seed) + if err == nil { + return sid + } + panic(err) +} + +// Generate generates a new short Id. +func (sid *Shortid) Generate() (string, error) { + return sid.GenerateInternal(nil, sid.epoch) +} + +// MustGenerate acts just like Generate, but panics instead of returning errors. +func (sid *Shortid) MustGenerate() string { + id, err := sid.Generate() + if err == nil { + return id + } + panic(err) +} + +// GenerateInternal should only be used for testing purposes. +func (sid *Shortid) GenerateInternal(tm *time.Time, epoch time.Time) (string, error) { + ms, count := sid.getMsAndCounter(tm, epoch) + idrunes := make([]rune, 9) + if tmp, err := sid.abc.Encode(ms, 8, 5); err == nil { + copy(idrunes, tmp) // first 8 symbols + } else { + return "", err + } + if tmp, err := sid.abc.Encode(sid.worker, 1, 5); err == nil { + idrunes[8] = tmp[0] + } else { + return "", err + } + if count > 0 { + if countrunes, err := sid.abc.Encode(count, 0, 6); err == nil { + // only extend if really need it + idrunes = append(idrunes, countrunes...) + } else { + return "", err + } + } + return string(idrunes), nil +} + +func (sid *Shortid) getMsAndCounter(tm *time.Time, epoch time.Time) (uint, uint) { + sid.mx.Lock() + defer sid.mx.Unlock() + var ms uint + if tm != nil { + ms = uint(tm.Sub(epoch).Nanoseconds() / 1000000) + } else { + ms = uint(time.Now().Sub(epoch).Nanoseconds() / 1000000) + } + if ms == sid.ms { + sid.count++ + } else { + sid.count = 0 + sid.ms = ms + } + return sid.ms, sid.count +} + +// String returns a string representation of the short Id generator. +func (sid *Shortid) String() string { + return fmt.Sprintf("Shortid(worker=%v, epoch=%v, abc=%v)", sid.worker, sid.epoch, sid.abc) +} + +// Abc returns the instance of alphabet used for representing the Ids. +func (sid *Shortid) Abc() Abc { + return sid.abc +} + +// Epoch returns the value of epoch used as the beginning of millisecond counting (normally +// 2016-01-01 00:00:00 local time) +func (sid *Shortid) Epoch() time.Time { + return sid.epoch +} + +// Worker returns the value of worker for this short Id generator. +func (sid *Shortid) Worker() uint { + return sid.worker +} + +// NewAbc constructs a new instance of shuffled alphabet to be used for Id representation. +func NewAbc(alphabet string, seed uint64) (Abc, error) { + runes := []rune(alphabet) + if len(runes) != len(DefaultABC) { + return Abc{}, fmt.Errorf("alphabet must contain %v unique characters", len(DefaultABC)) + } + if nonUnique(runes) { + return Abc{}, errors.New("alphabet must contain unique characters only") + } + abc := Abc{alphabet: nil} + abc.shuffle(alphabet, seed) + return abc, nil +} + +// MustNewAbc acts just like NewAbc, but panics instead of returning errors. +func MustNewAbc(alphabet string, seed uint64) Abc { + res, err := NewAbc(alphabet, seed) + if err == nil { + return res + } + panic(err) +} + +func nonUnique(runes []rune) bool { + found := make(map[rune]struct{}) + for _, r := range runes { + if _, seen := found[r]; !seen { + found[r] = struct{}{} + } + } + return len(found) < len(runes) +} + +func (abc *Abc) shuffle(alphabet string, seed uint64) { + source := []rune(alphabet) + for len(source) > 1 { + seed = (seed*9301 + 49297) % 233280 + i := int(seed * uint64(len(source)) / 233280) + + abc.alphabet = append(abc.alphabet, source[i]) + source = append(source[:i], source[i+1:]...) + } + abc.alphabet = append(abc.alphabet, source[0]) +} + +// Encode encodes a given value into a slice of runes of length nsymbols. In case nsymbols==0, the +// length of the result is automatically computed from data. Even if fewer symbols is required to +// encode the data than nsymbols, all positions are used encoding 0 where required to guarantee +// uniqueness in case further data is added to the sequence. The value of digits [4,6] represents +// represents n in 2^n, which defines how much randomness flows into the algorithm: 4 -- every value +// can be represented by 4 symbols in the alphabet (permitting at most 16 values), 5 -- every value +// can be represented by 2 symbols in the alphabet (permitting at most 32 values), 6 -- every value +// is represented by exactly 1 symbol with no randomness (permitting 64 values). +func (abc *Abc) Encode(val, nsymbols, digits uint) ([]rune, error) { + if digits < 4 || 6 < digits { + return nil, fmt.Errorf("allowed digits range [4,6], found %v", digits) + } + + var computedSize uint = 1 + if val >= 1 { + computedSize = uint(math.Log2(float64(val)))/digits + 1 + } + if nsymbols == 0 { + nsymbols = computedSize + } else if nsymbols < computedSize { + return nil, fmt.Errorf("cannot accommodate data, need %v digits, got %v", computedSize, nsymbols) + } + + mask := 1<>shift) & mask) | random[i] + res[i] = abc.alphabet[index] + } + return res, nil +} + +// MustEncode acts just like Encode, but panics instead of returning errors. +func (abc *Abc) MustEncode(val, size, digits uint) []rune { + res, err := abc.Encode(val, size, digits) + if err == nil { + return res + } + panic(err) +} + +func maskedRandomInts(size, mask int) []int { + ints := make([]int, size) + bytes := make([]byte, size) + if _, err := randc.Read(bytes); err == nil { + for i, b := range bytes { + ints[i] = int(b) & mask + } + } else { + for i := range ints { + ints[i] = randm.Intn(0xff) & mask + } + } + return ints +} + +// String returns a string representation of the Abc instance. +func (abc Abc) String() string { + return fmt.Sprintf("Abc{alphabet='%v')", abc.Alphabet()) +} + +// Alphabet returns the alphabet used as an immutable string. +func (abc Abc) Alphabet() string { + return string(abc.alphabet) +} From fc7bab8bf06940ba4b2919b5eac53129573c850d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 29 Jan 2018 17:51:10 +0100 Subject: [PATCH 05/84] dashboard: fix failing test. #7883 --- pkg/services/sqlstore/dashboard_version_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index 6ed37cd6904..cbc33c8ad84 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -49,6 +49,7 @@ func TestGetDashboardVersion(t *testing.T) { err = GetDashboard(&dashCmd) So(err, ShouldBeNil) + dashCmd.Result.Data.Del("uid") eq := reflect.DeepEqual(dashCmd.Result.Data, query.Result.Data) So(eq, ShouldEqual, true) }) From e229f8aea81063ae3eeeb4fd95a1cb07b2dbf515 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 29 Jan 2018 18:52:19 +0100 Subject: [PATCH 06/84] dashboards: extract short uid generator to util package. #7883 --- pkg/models/dashboards.go | 12 +++--------- pkg/util/shortid_generator.go | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 pkg/util/shortid_generator.go diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index b377e184829..184fe33e134 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -7,7 +7,7 @@ import ( "github.com/gosimple/slug" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/teris-io/shortid" + "github.com/grafana/grafana/pkg/util" ) // Typed errors @@ -63,7 +63,7 @@ type Dashboard struct { // NewDashboard creates a new dashboard func NewDashboard(title string) *Dashboard { dash := &Dashboard{} - dash.Uid = DashboardUid() + dash.Uid, _ = util.GenerateShortUid() dash.Data = simplejson.New() dash.Data.Set("title", title) dash.Title = title @@ -113,18 +113,12 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { if uid, err := dash.Data.Get("uid").String(); err == nil { dash.Uid = uid } else { - dash.Uid = DashboardUid() + dash.Uid, _ = util.GenerateShortUid() } return dash } -func DashboardUid() string { - gen, _ := shortid.New(1, shortid.DefaultABC, 1) - uid, _ := gen.Generate() - return uid -} - // GetDashboardModel turns the command into the savable model func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash := NewDashboardFromJson(cmd.Dashboard) diff --git a/pkg/util/shortid_generator.go b/pkg/util/shortid_generator.go new file mode 100644 index 00000000000..ab3b9400f4f --- /dev/null +++ b/pkg/util/shortid_generator.go @@ -0,0 +1,24 @@ +package util + +import ( + "github.com/teris-io/shortid" +) + +func init() { + gen, _ := shortid.New(1, shortid.DefaultABC, 1) + shortid.SetDefault(gen) + +} + +// GenerateShortUid generates a short unique identifier. +func GenerateShortUid() (uid string, err error) { + if uid, err = shortid.Generate(); err != nil { + if uid, err = shortid.Generate(); err != nil { + if uid, err = shortid.Generate(); err != nil { + return "", err + } + } + } + + return uid, nil +} From 46e1296700ef08b30e1b70c4c6f4a02cd1220776 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 29 Jan 2018 19:27:53 +0100 Subject: [PATCH 07/84] dashboards: return uid in response to creating/updating a dashboard. #7883 --- pkg/api/dashboard.go | 2 +- pkg/api/dashboard_test.go | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 87c42884e31..bf9f2cadec5 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -226,7 +226,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { } c.TimeRequest(metrics.M_Api_Dashboard_Save) - return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id}) + return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id, "uid": dashboard.Uid}) } func GetHomeDashboard(c *middleware.Context) Response { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index e6228878625..5dd55d9ae01 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -135,6 +135,11 @@ func TestDashboardApiEndpoint(t *testing.T) { postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { CallPostDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) + result := sc.ToJson() + So(result.Get("status").MustString(), ShouldEqual, "success") + So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) + So(result.Get("uid").MustString(), ShouldNotBeNil) + So(result.Get("slug").MustString(), ShouldNotBeNil) }) Convey("When saving a dashboard folder in another folder", func() { @@ -306,6 +311,11 @@ func TestDashboardApiEndpoint(t *testing.T) { postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { CallPostDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) + result := sc.ToJson() + So(result.Get("status").MustString(), ShouldEqual, "success") + So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) + So(result.Get("uid").MustString(), ShouldNotBeNil) + So(result.Get("slug").MustString(), ShouldNotBeNil) }) }) @@ -378,6 +388,11 @@ func TestDashboardApiEndpoint(t *testing.T) { postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { CallPostDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) + result := sc.ToJson() + So(result.Get("status").MustString(), ShouldEqual, "success") + So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) + So(result.Get("uid").MustString(), ShouldNotBeNil) + So(result.Get("slug").MustString(), ShouldNotBeNil) }) }) @@ -519,3 +534,10 @@ func postDashboardScenario(desc string, url string, routePattern string, role m. fn(sc) }) } + +func (sc *scenarioContext) ToJson() *simplejson.Json { + var result *simplejson.Json + err := json.NewDecoder(sc.resp.Body).Decode(&result) + So(err, ShouldBeNil) + return result +} From c1cff3849e8ab7cd38565b9e64ec9651aed0dc7f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Jan 2018 11:15:51 +0100 Subject: [PATCH 08/84] dashboard: change unique index for uid to include org_id Make the max length of uid longer in case we want to change it later #7883 --- pkg/services/sqlstore/migrations/dashboard_mig.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 1035ef35015..7da7c5a974f 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -151,17 +151,16 @@ func addDashboardMigration(mg *Migrator) { Name: "has_acl", Type: DB_Bool, Nullable: false, Default: "0", })) - // new uid column mg.AddMigration("Add column uid in dashboard", NewAddColumnMigration(dashboardV2, &Column{ - Name: "uid", Type: DB_NVarchar, Length: 12, Nullable: true, + Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: true, })) - mg.AddMigration("Set uid column values", new(RawSqlMigration). + mg.AddMigration("Update uid column values in dashboard", new(RawSqlMigration). Sqlite("UPDATE dashboard SET uid=printf('%09d',id) WHERE uid IS NULL;"). Postgres("UPDATE dashboard SET uid=lpad('' || id,9,'0') WHERE uid IS NULL;"). Mysql("UPDATE dashboard SET uid=lpad(id,9,'0') WHERE uid IS NULL;")) - mg.AddMigration("Add index for uid in dashboard", NewAddIndexMigration(dashboardV2, &Index{ - Cols: []string{"uid"}, Type: UniqueIndex, + mg.AddMigration("Add unique index dashboard_org_id_uid", NewAddIndexMigration(dashboardV2, &Index{ + Cols: []string{"org_id", "uid"}, Type: UniqueIndex, })) } From 13d5db7d19b2faa7e73ce49d819fcbf3a4e5cb38 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 29 Jan 2018 21:23:07 +0100 Subject: [PATCH 09/84] dashboards: add support for retrieving a dashboard by uid Introduces new url in api /dashboards/ for fetching dashboard by unique id Leave the old dashboard by slug url /dashboards/db/ for backward compatibility and for supporting fallback WIP for #7883 --- pkg/api/api.go | 2 + pkg/api/dashboard.go | 17 +- pkg/api/dashboard_test.go | 211 ++++++++++++++++++++++-- pkg/models/dashboards.go | 3 +- pkg/services/sqlstore/dashboard.go | 2 +- pkg/services/sqlstore/dashboard_test.go | 38 ++++- 6 files changed, 249 insertions(+), 24 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index ea082ff4741..2d45868e58d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -242,6 +242,8 @@ func (hs *HttpServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) { + dashboardRoute.Get("/uid/:uid", wrap(GetDashboard)) + dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index bf9f2cadec5..bfd53513feb 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -44,7 +44,7 @@ func dashboardGuardianResponse(err error) Response { } func GetDashboard(c *middleware.Context) Response { - dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0) + dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid")) if rsp != nil { return rsp } @@ -124,8 +124,15 @@ func getUserLogin(userId int64) string { } } -func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Response) { - query := m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId} +func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dashboard, Response) { + var query m.GetDashboardQuery + + if len(uid) > 0 { + query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgId} + } else { + query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId} + } + if err := bus.Dispatch(&query); err != nil { return nil, ApiError(404, "Dashboard not found", err) } @@ -133,7 +140,7 @@ func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Respo } func DeleteDashboard(c *middleware.Context) Response { - dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0) + dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "") if rsp != nil { return rsp } @@ -393,7 +400,7 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff // RestoreDashboardVersion restores a dashboard to the given version. func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response { - dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId")) + dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "") if rsp != nil { return rsp } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 5dd55d9ae01..cc718229f96 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -39,8 +39,11 @@ func TestDashboardApiEndpoint(t *testing.T) { fakeDash.FolderId = 1 fakeDash.HasAcl = false + var getDashboardQueries []*m.GetDashboardQuery + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { query.Result = fakeDash + getDashboardQueries = append(getDashboardQueries, query) return nil }) @@ -73,9 +76,13 @@ func TestDashboardApiEndpoint(t *testing.T) { Convey("When user is an Org Viewer", func() { role := m.ROLE_VIEWER - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { dash := GetDashboardShouldReturn200(sc) + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) + Convey("Should not be able to edit or save dashboard", func() { So(dash.Meta.CanEdit, ShouldBeFalse) So(dash.Meta.CanSave, ShouldBeFalse) @@ -83,9 +90,27 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + + Convey("Should not be able to edit or save dashboard", func() { + So(dash.Meta.CanEdit, ShouldBeFalse) + So(dash.Meta.CanSave, ShouldBeFalse) + So(dash.Meta.CanAdmin, ShouldBeFalse) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { @@ -107,9 +132,13 @@ func TestDashboardApiEndpoint(t *testing.T) { Convey("When user is an Org Editor", func() { role := m.ROLE_EDITOR - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { dash := GetDashboardShouldReturn200(sc) + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) + Convey("Should be able to edit or save dashboard", func() { So(dash.Meta.CanEdit, ShouldBeTrue) So(dash.Meta.CanSave, ShouldBeTrue) @@ -117,9 +146,27 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + + Convey("Should be able to edit or save dashboard", func() { + So(dash.Meta.CanEdit, ShouldBeTrue) + So(dash.Meta.CanSave, ShouldBeTrue) + So(dash.Meta.CanAdmin, ShouldBeFalse) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) + + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { @@ -186,8 +233,11 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) + var getDashboardQueries []*m.GetDashboardQuery + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { query.Result = fakeDash + getDashboardQueries = append(getDashboardQueries, query) return nil }) @@ -208,18 +258,39 @@ func TestDashboardApiEndpoint(t *testing.T) { Convey("When user is an Org Viewer and has no permissions for this dashboard", func() { role := m.ROLE_VIEWER - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { sc.handlerFunc = GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) + Convey("Should be denied access", func() { So(sc.resp.Code, ShouldEqual, 403) }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + sc.handlerFunc = GetDashboard + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + + Convey("Should be denied access", func() { + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { @@ -241,18 +312,39 @@ func TestDashboardApiEndpoint(t *testing.T) { Convey("When user is an Org Editor and has no permissions for this dashboard", func() { role := m.ROLE_EDITOR - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { sc.handlerFunc = GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) + Convey("Should be denied access", func() { So(sc.resp.Code, ShouldEqual, 403) }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + sc.handlerFunc = GetDashboard + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + + Convey("Should be denied access", func() { + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { @@ -283,9 +375,13 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { dash := GetDashboardShouldReturn200(sc) + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) + Convey("Should be able to get dashboard with edit rights", func() { So(dash.Meta.CanEdit, ShouldBeTrue) So(dash.Meta.CanSave, ShouldBeTrue) @@ -293,9 +389,27 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + + Convey("Should be able to get dashboard with edit rights", func() { + So(dash.Meta.CanEdit, ShouldBeTrue) + So(dash.Meta.CanSave, ShouldBeTrue) + So(dash.Meta.CanAdmin, ShouldBeFalse) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) + + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { @@ -332,9 +446,13 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { dash := GetDashboardShouldReturn200(sc) + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) + Convey("Should be able to get dashboard with edit rights but can save should be false", func() { So(dash.Meta.CanEdit, ShouldBeTrue) So(dash.Meta.CanSave, ShouldBeFalse) @@ -342,9 +460,27 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + + Convey("Should be able to get dashboard with edit rights but can save should be false", func() { + So(dash.Meta.CanEdit, ShouldBeTrue) + So(dash.Meta.CanSave, ShouldBeFalse) + So(dash.Meta.CanAdmin, ShouldBeFalse) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) }) }) @@ -360,9 +496,13 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { dash := GetDashboardShouldReturn200(sc) + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) + Convey("Should be able to get dashboard with edit rights", func() { So(dash.Meta.CanEdit, ShouldBeTrue) So(dash.Meta.CanSave, ShouldBeTrue) @@ -370,9 +510,27 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + + Convey("Should be able to get dashboard with edit rights", func() { + So(dash.Meta.CanEdit, ShouldBeTrue) + So(dash.Meta.CanSave, ShouldBeTrue) + So(dash.Meta.CanAdmin, ShouldBeTrue) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) + + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { @@ -408,18 +566,39 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { dash := GetDashboardShouldReturn200(sc) + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) + Convey("Should not be able to edit or save dashboard", func() { So(dash.Meta.CanEdit, ShouldBeFalse) So(dash.Meta.CanSave, ShouldBeFalse) }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + dash := GetDashboardShouldReturn200(sc) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + + Convey("Should not be able to edit or save dashboard", func() { + So(dash.Meta.CanEdit, ShouldBeFalse) + So(dash.Meta.CanSave, ShouldBeFalse) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup dashboard by slug", func() { + So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + }) }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 184fe33e134..a858666a44e 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -186,8 +186,9 @@ type DeleteDashboardCommand struct { // type GetDashboardQuery struct { - Slug string // required if no Id is specified + Slug string // required if no Id or Uid is specified Id int64 // optional if slug is set + Uid string // optional if slug is set OrgId int64 Result *Dashboard diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index b8d3d9a36a5..497afa8f73f 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -165,7 +165,7 @@ func setHasAcl(sess *DBSession, dash *m.Dashboard) error { } func GetDashboard(query *m.GetDashboardQuery) error { - dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id} + dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id, Uid: query.Uid} has, err := x.Get(&dashboard) if err != nil { diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index a552bd0546a..18e283883a2 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -30,15 +30,33 @@ func TestDashboardDataAccess(t *testing.T) { So(savedDash.Id, ShouldNotEqual, 0) So(savedDash.IsFolder, ShouldBeFalse) So(savedDash.FolderId, ShouldBeGreaterThan, 0) + So(len(savedDash.Uid), ShouldBeGreaterThan, 0) So(savedFolder.Title, ShouldEqual, "1 test dash folder") So(savedFolder.Slug, ShouldEqual, "1-test-dash-folder") So(savedFolder.Id, ShouldNotEqual, 0) So(savedFolder.IsFolder, ShouldBeTrue) So(savedFolder.FolderId, ShouldEqual, 0) + So(len(savedFolder.Uid), ShouldBeGreaterThan, 0) }) - Convey("Should be able to get dashboard", func() { + Convey("Should be able to get dashboard by id", func() { + query := m.GetDashboardQuery{ + Id: savedDash.Id, + OrgId: 1, + } + + err := GetDashboard(&query) + So(err, ShouldBeNil) + + So(query.Result.Title, ShouldEqual, "test dash 23") + So(query.Result.Slug, ShouldEqual, "test-dash-23") + So(query.Result.Id, ShouldEqual, savedDash.Id) + So(query.Result.Uid, ShouldEqual, savedDash.Uid) + So(query.Result.IsFolder, ShouldBeFalse) + }) + + Convey("Should be able to get dashboard by slug", func() { query := m.GetDashboardQuery{ Slug: "test-dash-23", OrgId: 1, @@ -49,6 +67,24 @@ func TestDashboardDataAccess(t *testing.T) { So(query.Result.Title, ShouldEqual, "test dash 23") So(query.Result.Slug, ShouldEqual, "test-dash-23") + So(query.Result.Id, ShouldEqual, savedDash.Id) + So(query.Result.Uid, ShouldEqual, savedDash.Uid) + So(query.Result.IsFolder, ShouldBeFalse) + }) + + Convey("Should be able to get dashboard by uid", func() { + query := m.GetDashboardQuery{ + Uid: savedDash.Uid, + OrgId: 1, + } + + err := GetDashboard(&query) + So(err, ShouldBeNil) + + So(query.Result.Title, ShouldEqual, "test dash 23") + So(query.Result.Slug, ShouldEqual, "test-dash-23") + So(query.Result.Id, ShouldEqual, savedDash.Id) + So(query.Result.Uid, ShouldEqual, savedDash.Uid) So(query.Result.IsFolder, ShouldBeFalse) }) From 7ee691dc480ab6f47689e5d7f7f69504a9313be7 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Jan 2018 13:11:48 +0100 Subject: [PATCH 10/84] dashboards: api for retrieving uid by slug. #7883 --- pkg/api/api.go | 1 + pkg/api/dashboard.go | 15 +++++++ pkg/api/dashboard_test.go | 88 +++++++++++++++++++++++++++++++++++++++ pkg/models/dashboards.go | 6 +++ 4 files changed, 110 insertions(+) diff --git a/pkg/api/api.go b/pkg/api/api.go index 2d45868e58d..1c3036eccee 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -245,6 +245,7 @@ func (hs *HttpServer) registerRoutes() { dashboardRoute.Get("/uid/:uid", wrap(GetDashboard)) dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) + dashboardRoute.Get("/db/:slug/uid", wrap(GetDashboardUidBySlug)) dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard)) dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index bfd53513feb..0c30fd54ee5 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -113,6 +113,21 @@ func GetDashboard(c *middleware.Context) Response { return Json(200, dto) } +func GetDashboardUidBySlug(c *middleware.Context) Response { + dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "") + if rsp != nil { + return rsp + } + + guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser) + if canView, err := guardian.CanView(); err != nil || !canView { + fmt.Printf("%v", err) + return dashboardGuardianResponse(err) + } + + return Json(200, util.DynMap{"uid": dash.Uid}) +} + func getUserLogin(userId int64) string { query := m.GetUserByIdQuery{Id: userId} err := bus.Dispatch(&query) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index cc718229f96..4255764d892 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -47,6 +47,11 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) + bus.AddHandler("test", func(query *m.GetDashboardUidBySlugQuery) error { + query.Result = fakeDash.Uid + return nil + }) + viewerRole := m.ROLE_VIEWER editorRole := m.ROLE_EDITOR @@ -104,6 +109,14 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { + uid := GetDashboardUidBySlugShouldReturn200(sc) + + Convey("Should return uid", func() { + So(uid, ShouldEqual, fakeDash.Uid) + }) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -160,6 +173,14 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { + uid := GetDashboardUidBySlugShouldReturn200(sc) + + Convey("Should return uid", func() { + So(uid, ShouldEqual, fakeDash.Uid) + }) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -241,6 +262,11 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) + bus.AddHandler("test", func(query *m.GetDashboardUidBySlugQuery) error { + query.Result = fakeDash.Uid + return nil + }) + bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { query.Result = []*m.Team{} return nil @@ -284,6 +310,14 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { + CallGetDashboardUidBySlug(sc) + + Convey("Should be denied access", func() { + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -338,6 +372,14 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { + CallGetDashboardUidBySlug(sc) + + Convey("Should be denied access", func() { + So(sc.resp.Code, ShouldEqual, 403) + }) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -403,6 +445,14 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { + uid := GetDashboardUidBySlugShouldReturn200(sc) + + Convey("Should return uid", func() { + So(uid, ShouldEqual, fakeDash.Uid) + }) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -474,6 +524,14 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { + uid := GetDashboardUidBySlugShouldReturn200(sc) + + Convey("Should return uid", func() { + So(uid, ShouldEqual, fakeDash.Uid) + }) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -524,6 +582,14 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { + uid := GetDashboardUidBySlugShouldReturn200(sc) + + Convey("Should return uid", func() { + So(uid, ShouldEqual, fakeDash.Uid) + }) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -592,6 +658,14 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { + uid := GetDashboardUidBySlugShouldReturn200(sc) + + Convey("Should return uid", func() { + So(uid, ShouldEqual, fakeDash.Uid) + }) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -632,6 +706,20 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta return dash } +func GetDashboardUidBySlugShouldReturn200(sc *scenarioContext) string { + CallGetDashboardUidBySlug(sc) + + So(sc.resp.Code, ShouldEqual, 200) + + result := sc.ToJson() + return result.Get("uid").MustString() +} + +func CallGetDashboardUidBySlug(sc *scenarioContext) { + sc.handlerFunc = GetDashboardUidBySlug + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() +} + func CallGetDashboardVersion(sc *scenarioContext) { bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error { query.Result = &m.DashboardVersion{} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index a858666a44e..0dfbd3b924f 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -219,3 +219,9 @@ type GetDashboardSlugByIdQuery struct { Id int64 Result string } + +type GetDashboardUidBySlugQuery struct { + OrgId int64 + Slug string + Result string +} From 9fb7b887db0d571b421ad34f9b021ec1c5f840bd Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Jan 2018 15:24:14 +0100 Subject: [PATCH 11/84] dashboards: add url property to dashboard meta and search api responses #7883 --- pkg/api/dashboard.go | 6 ++++++ pkg/api/dtos/dashboard.go | 1 + pkg/models/dashboards.go | 12 ++++++++++++ pkg/services/search/models.go | 1 + pkg/services/sqlstore/dashboard.go | 9 +++++++++ pkg/services/sqlstore/dashboard_test.go | 3 +++ pkg/services/sqlstore/search_builder.go | 1 + 7 files changed, 33 insertions(+) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 0c30fd54ee5..efb9a790f2d 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -101,6 +101,12 @@ func GetDashboard(c *middleware.Context) Response { meta.FolderTitle = query.Result.Title } + if dash.IsFolder { + meta.Url = m.GetFolderUrl(dash.Uid, dash.Slug) + } else { + meta.Url = m.GetDashboardUrl(dash.Uid, dash.Slug) + } + // make sure db version is in sync with json model version dash.Data.Set("version", dash.Version) diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index 0be0537527b..604bb8d12ed 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -16,6 +16,7 @@ type DashboardMeta struct { CanAdmin bool `json:"canAdmin"` CanStar bool `json:"canStar"` Slug string `json:"slug"` + Url string `json:"url"` Expires time.Time `json:"expires"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 0dfbd3b924f..616ebfeb187 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -2,11 +2,13 @@ package models import ( "errors" + "fmt" "strings" "time" "github.com/gosimple/slug" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -156,6 +158,16 @@ func SlugifyTitle(title string) string { return slug.Make(strings.ToLower(title)) } +// GetDashboardUrl return the html url for a dashboard +func GetDashboardUrl(uid string, slug string) string { + return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug) +} + +// GetFolderUrl return the html url for a folder +func GetFolderUrl(folderUid string, slug string) string { + return fmt.Sprintf("%s/f/%v/%s", setting.AppSubUrl, folderUid, slug) +} + // // COMMANDS // diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index cf510ed8462..6214a854db7 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -15,6 +15,7 @@ type Hit struct { Id int64 `json:"id"` Title string `json:"title"` Uri string `json:"uri"` + Url string `json:"url"` Slug string `json:"slug"` Type HitType `json:"type"` Tags []string `json:"tags"` diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 497afa8f73f..84cfd1999c7 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -182,6 +182,7 @@ func GetDashboard(query *m.GetDashboardQuery) error { type DashboardSearchProjection struct { Id int64 + Uid string Title string Slug string Term string @@ -257,10 +258,17 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard for _, item := range res { hit, exists := hits[item.Id] if !exists { + var url string + if item.IsFolder { + url = m.GetFolderUrl(item.Uid, item.Slug) + } else { + url = m.GetDashboardUrl(item.Uid, item.Slug) + } hit = &search.Hit{ Id: item.Id, Title: item.Title, Uri: "db/" + item.Slug, + Url: url, Slug: item.Slug, Type: getHitType(item), FolderId: item.FolderId, @@ -268,6 +276,7 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard FolderSlug: item.FolderSlug, Tags: []string{}, } + query.Result = append(query.Result, hit) hits[item.Id] = hit } diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 18e283883a2..19e82614718 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -1,6 +1,7 @@ package sqlstore import ( + "fmt" "testing" "github.com/go-xorm/xorm" @@ -145,6 +146,7 @@ func TestDashboardDataAccess(t *testing.T) { So(len(query.Result), ShouldEqual, 1) hit := query.Result[0] So(hit.Type, ShouldEqual, search.DashHitFolder) + So(hit.Url, ShouldEqual, fmt.Sprintf("/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) }) Convey("Should be able to search for a dashboard folder's children", func() { @@ -160,6 +162,7 @@ func TestDashboardDataAccess(t *testing.T) { So(len(query.Result), ShouldEqual, 2) hit := query.Result[0] So(hit.Id, ShouldEqual, savedDash.Id) + So(hit.Url, ShouldEqual, fmt.Sprintf("/d/%s/%s", savedDash.Uid, savedDash.Slug)) }) Convey("Should be able to search for dashboard by dashboard ids", func() { diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go index ddf192da5ff..480e7fa99e6 100644 --- a/pkg/services/sqlstore/search_builder.go +++ b/pkg/services/sqlstore/search_builder.go @@ -101,6 +101,7 @@ func (sb *SearchBuilder) buildSelect() { sb.sql.WriteString( `SELECT dashboard.id, + dashboard.uid, dashboard.title, dashboard.slug, dashboard_tag.term, From 4829ea0e9f994f3befa566383142688039147ed2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Jan 2018 23:07:21 +0100 Subject: [PATCH 12/84] util: remove retry logic in shortid_generator Use shortid.MustGenerate() instead of shortid.Generate(). Instead of returning errors it will panic. --- pkg/models/dashboards.go | 4 ++-- pkg/util/shortid_generator.go | 13 ++----------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 616ebfeb187..e9b3768578e 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -65,7 +65,7 @@ type Dashboard struct { // NewDashboard creates a new dashboard func NewDashboard(title string) *Dashboard { dash := &Dashboard{} - dash.Uid, _ = util.GenerateShortUid() + dash.Uid = util.GenerateShortUid() dash.Data = simplejson.New() dash.Data.Set("title", title) dash.Title = title @@ -115,7 +115,7 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { if uid, err := dash.Data.Get("uid").String(); err == nil { dash.Uid = uid } else { - dash.Uid, _ = util.GenerateShortUid() + dash.Uid = util.GenerateShortUid() } return dash diff --git a/pkg/util/shortid_generator.go b/pkg/util/shortid_generator.go index ab3b9400f4f..067f7c756ba 100644 --- a/pkg/util/shortid_generator.go +++ b/pkg/util/shortid_generator.go @@ -7,18 +7,9 @@ import ( func init() { gen, _ := shortid.New(1, shortid.DefaultABC, 1) shortid.SetDefault(gen) - } // GenerateShortUid generates a short unique identifier. -func GenerateShortUid() (uid string, err error) { - if uid, err = shortid.Generate(); err != nil { - if uid, err = shortid.Generate(); err != nil { - if uid, err = shortid.Generate(); err != nil { - return "", err - } - } - } - - return uid, nil +func GenerateShortUid() string { + return shortid.MustGenerate() } From fd59241e35bc6584bfcef2a1c9dfc41f19337b38 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Jan 2018 23:18:08 +0100 Subject: [PATCH 13/84] dashboards: revert adding api for retrieving uid by slug Since we're already have possibility to get a dashboard by slug it makes little sense to have a separate endpoint in api for retrieving uid by slug. #7883 --- pkg/api/api.go | 1 - pkg/api/dashboard.go | 15 ------- pkg/api/dashboard_test.go | 88 --------------------------------------- pkg/models/dashboards.go | 6 --- 4 files changed, 110 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 1c3036eccee..2d45868e58d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -245,7 +245,6 @@ func (hs *HttpServer) registerRoutes() { dashboardRoute.Get("/uid/:uid", wrap(GetDashboard)) dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) - dashboardRoute.Get("/db/:slug/uid", wrap(GetDashboardUidBySlug)) dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard)) dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index efb9a790f2d..a1c9950c457 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -119,21 +119,6 @@ func GetDashboard(c *middleware.Context) Response { return Json(200, dto) } -func GetDashboardUidBySlug(c *middleware.Context) Response { - dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "") - if rsp != nil { - return rsp - } - - guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser) - if canView, err := guardian.CanView(); err != nil || !canView { - fmt.Printf("%v", err) - return dashboardGuardianResponse(err) - } - - return Json(200, util.DynMap{"uid": dash.Uid}) -} - func getUserLogin(userId int64) string { query := m.GetUserByIdQuery{Id: userId} err := bus.Dispatch(&query) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 4255764d892..cc718229f96 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -47,11 +47,6 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *m.GetDashboardUidBySlugQuery) error { - query.Result = fakeDash.Uid - return nil - }) - viewerRole := m.ROLE_VIEWER editorRole := m.ROLE_EDITOR @@ -109,14 +104,6 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { - uid := GetDashboardUidBySlugShouldReturn200(sc) - - Convey("Should return uid", func() { - So(uid, ShouldEqual, fakeDash.Uid) - }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -173,14 +160,6 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { - uid := GetDashboardUidBySlugShouldReturn200(sc) - - Convey("Should return uid", func() { - So(uid, ShouldEqual, fakeDash.Uid) - }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -262,11 +241,6 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *m.GetDashboardUidBySlugQuery) error { - query.Result = fakeDash.Uid - return nil - }) - bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { query.Result = []*m.Team{} return nil @@ -310,14 +284,6 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { - CallGetDashboardUidBySlug(sc) - - Convey("Should be denied access", func() { - So(sc.resp.Code, ShouldEqual, 403) - }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -372,14 +338,6 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { - CallGetDashboardUidBySlug(sc) - - Convey("Should be denied access", func() { - So(sc.resp.Code, ShouldEqual, 403) - }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -445,14 +403,6 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { - uid := GetDashboardUidBySlugShouldReturn200(sc) - - Convey("Should return uid", func() { - So(uid, ShouldEqual, fakeDash.Uid) - }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -524,14 +474,6 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { - uid := GetDashboardUidBySlugShouldReturn200(sc) - - Convey("Should return uid", func() { - So(uid, ShouldEqual, fakeDash.Uid) - }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -582,14 +524,6 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { - uid := GetDashboardUidBySlugShouldReturn200(sc) - - Convey("Should return uid", func() { - So(uid, ShouldEqual, fakeDash.Uid) - }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -658,14 +592,6 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash/uid", "/api/dashboards/db/:slug/uid", role, func(sc *scenarioContext) { - uid := GetDashboardUidBySlugShouldReturn200(sc) - - Convey("Should return uid", func() { - So(uid, ShouldEqual, fakeDash.Uid) - }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboard(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -706,20 +632,6 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta return dash } -func GetDashboardUidBySlugShouldReturn200(sc *scenarioContext) string { - CallGetDashboardUidBySlug(sc) - - So(sc.resp.Code, ShouldEqual, 200) - - result := sc.ToJson() - return result.Get("uid").MustString() -} - -func CallGetDashboardUidBySlug(sc *scenarioContext) { - sc.handlerFunc = GetDashboardUidBySlug - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() -} - func CallGetDashboardVersion(sc *scenarioContext) { bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error { query.Result = &m.DashboardVersion{} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index e9b3768578e..84ede04f226 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -231,9 +231,3 @@ type GetDashboardSlugByIdQuery struct { Id int64 Result string } - -type GetDashboardUidBySlugQuery struct { - OrgId int64 - Slug string - Result string -} From 6ab526881a87d0e6d4b7a9454ffba744c09413dd Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 00:06:31 +0100 Subject: [PATCH 14/84] dashboards: ensure that uid is returned from getSaveModelClone Without this the uid will not be sent to the backend when saving a dashboard. #7883 --- public/app/features/dashboard/dashboard_model.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index dccd910a85e..a416e610132 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -12,6 +12,7 @@ import { DashboardMigrator } from './dashboard_migration'; export class DashboardModel { id: any; + uid: any; title: any; autoUpdate: any; description: any; @@ -56,6 +57,7 @@ export class DashboardModel { this.events = new Emitter(); this.id = data.id || null; + this.uid = data.uid || null; this.revision = data.revision; this.title = data.title || 'No Title'; this.autoUpdate = data.autoUpdate; From 369597f7b2b8093bf5509a8d85012aef9cc1772f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Jan 2018 23:37:54 +0100 Subject: [PATCH 15/84] dashboards: return url in response to save dashboard. #7883 --- pkg/api/dashboard.go | 16 +++++++++++++++- pkg/api/dashboard_test.go | 36 +++++++++++++++--------------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index a1c9950c457..7799ad868f9 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -238,8 +238,22 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { return ApiError(500, "Invalid alert data. Cannot save dashboard", err) } + var url string + if dash.IsFolder { + url = m.GetFolderUrl(dashboard.Uid, dashboard.Slug) + } else { + url = m.GetDashboardUrl(dashboard.Uid, dashboard.Slug) + } + c.TimeRequest(metrics.M_Api_Dashboard_Save) - return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id, "uid": dashboard.Uid}) + return Json(200, util.DynMap{ + "status": "success", + "slug": dashboard.Slug, + "version": dashboard.Version, + "id": dashboard.Id, + "uid": dashboard.Uid, + "url": url, + }) } func GetHomeDashboard(c *middleware.Context) Response { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index cc718229f96..0bd586f6067 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -180,13 +180,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { - CallPostDashboard(sc) - So(sc.resp.Code, ShouldEqual, 200) - result := sc.ToJson() - So(result.Get("status").MustString(), ShouldEqual, "success") - So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) - So(result.Get("uid").MustString(), ShouldNotBeNil) - So(result.Get("slug").MustString(), ShouldNotBeNil) + CallPostDashboardShouldReturnSuccess(sc) }) Convey("When saving a dashboard folder in another folder", func() { @@ -423,13 +417,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { - CallPostDashboard(sc) - So(sc.resp.Code, ShouldEqual, 200) - result := sc.ToJson() - So(result.Get("status").MustString(), ShouldEqual, "success") - So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) - So(result.Get("uid").MustString(), ShouldNotBeNil) - So(result.Get("slug").MustString(), ShouldNotBeNil) + CallPostDashboardShouldReturnSuccess(sc) }) }) @@ -544,13 +532,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) { - CallPostDashboard(sc) - So(sc.resp.Code, ShouldEqual, 200) - result := sc.ToJson() - So(result.Get("status").MustString(), ShouldEqual, "success") - So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) - So(result.Get("uid").MustString(), ShouldNotBeNil) - So(result.Get("slug").MustString(), ShouldNotBeNil) + CallPostDashboardShouldReturnSuccess(sc) }) }) @@ -678,6 +660,18 @@ func CallPostDashboard(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } +func CallPostDashboardShouldReturnSuccess(sc *scenarioContext) { + CallPostDashboard(sc) + + So(sc.resp.Code, ShouldEqual, 200) + result := sc.ToJson() + So(result.Get("status").MustString(), ShouldEqual, "success") + So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0) + So(result.Get("uid").MustString(), ShouldNotBeNil) + So(result.Get("slug").MustString(), ShouldNotBeNil) + So(result.Get("url").MustString(), ShouldNotBeNil) +} + func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() From aefcff26a61f026958ee3ffe58f13b2b07ea43cd Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 00:26:26 +0100 Subject: [PATCH 16/84] dashboards: add new default frontend route for loading a dashboard New default route /d// where dashboard are loaded by unique identifier. If old route /dashboard/db/ are used, try to lookup dashboard by slug and redirect to new default route. #7883 --- public/app/core/services/backend_srv.ts | 4 ++++ .../app/features/dashboard/dashboard_loader_srv.ts | 6 +++--- public/app/routes/dashboard_loaders.ts | 14 ++++++++++++-- public/app/routes/routes.ts | 6 ++++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 3204db0ce5d..9ade4264bb3 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -225,6 +225,10 @@ export class BackendSrv { return this.get('/api/dashboards/' + type + '/' + slug); } + getDashboardByUid(uid: string) { + return this.get(`/api/dashboards/uid/${uid}`); + } + saveDashboard(dash, options) { options = options || {}; diff --git a/public/app/features/dashboard/dashboard_loader_srv.ts b/public/app/features/dashboard/dashboard_loader_srv.ts index e4def4385e1..9e458c4e4bb 100644 --- a/public/app/features/dashboard/dashboard_loader_srv.ts +++ b/public/app/features/dashboard/dashboard_loader_srv.ts @@ -35,18 +35,18 @@ export class DashboardLoaderSrv { }; } - loadDashboard(type, slug) { + loadDashboard(type, slug, uid) { var promise; if (type === 'script') { promise = this._loadScriptedDashboard(slug); } else if (type === 'snapshot') { - promise = this.backendSrv.get('/api/snapshots/' + this.$routeParams.slug).catch(() => { + promise = this.backendSrv.get('/api/snapshots/' + slug).catch(() => { return this._dashboardLoadFailed('Snapshot not found', true); }); } else { promise = this.backendSrv - .getDashboard(this.$routeParams.type, this.$routeParams.slug) + .getDashboardByUid(uid) .then(result => { if (result.meta.isFolder) { this.$rootScope.appEvent('alert-error', ['Dashboard not found']); diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index f4f0b36ac72..e3e6f4c18bc 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -5,7 +5,7 @@ export class LoadDashboardCtrl { constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) { $scope.appEvent('dashboard-fetch-start'); - if (!$routeParams.slug) { + if (!$routeParams.uid && !$routeParams.slug) { backendSrv.get('/api/dashboards/home').then(function(homeDash) { if (homeDash.redirectUri) { $location.path('dashboard/' + homeDash.redirectUri); @@ -18,7 +18,17 @@ export class LoadDashboardCtrl { return; } - dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { + // if no uid, redirect to new route based on slug + if (!$routeParams.uid) { + backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => { + if (res) { + $location.path(res.meta.url); + } + }); + return; + } + + dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) { if ($routeParams.keepRows) { result.meta.keepRows = true; } diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index ec65e4ec25a..081fb88af6e 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -14,6 +14,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { reloadOnSearch: false, pageClass: 'page-dashboard', }) + .when('/d/:uid/:slug', { + templateUrl: 'public/app/partials/dashboard.html', + controller: 'LoadDashboardCtrl', + reloadOnSearch: false, + pageClass: 'page-dashboard', + }) .when('/dashboard/:type/:slug', { templateUrl: 'public/app/partials/dashboard.html', controller: 'LoadDashboardCtrl', From 8009307c16732e39eb83e482cc720f3897d29b9b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 00:32:51 +0100 Subject: [PATCH 17/84] dashboards: when saving dashboard redirect if url changes Redirect if new dashboard created or existing url changes. #7883 --- public/app/features/dashboard/dashboard_srv.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index 708ba047d2b..07fc14f2f2e 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -73,9 +73,8 @@ export class DashboardSrv { postSave(clone, data) { this.dash.version = data.version; - var dashboardUrl = '/dashboard/db/' + data.slug; - if (dashboardUrl !== this.$location.path()) { - this.$location.url(dashboardUrl); + if (data.url !== this.$location.path()) { + this.$location.url(data.url); } this.$rootScope.appEvent('dashboard-saved', this.dash); From f2014223b490911b699a723ba3e8f1b58e9affa4 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 00:42:17 +0100 Subject: [PATCH 18/84] dashboards: use new *url* prop from dashboard search for linking to dashboards #7883 --- public/app/core/services/search_srv.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index a909b4af09f..a0989e74aed 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -41,10 +41,7 @@ export class SearchSrv { .map(orderId => { return _.find(result, { id: orderId }); }) - .filter(hit => hit && !hit.isStarred) - .map(hit => { - return this.transformToViewModel(hit); - }); + .filter(hit => hit && !hit.isStarred); }); } @@ -81,17 +78,12 @@ export class SearchSrv { score: -2, expanded: this.starredIsOpen, toggle: this.toggleStarred.bind(this), - items: result.map(this.transformToViewModel), + items: result, }; } }); } - private transformToViewModel(hit) { - hit.url = 'dashboard/db/' + hit.slug; - return hit; - } - search(options) { let sections: any = {}; let promises = []; @@ -181,7 +173,7 @@ export class SearchSrv { } section.expanded = true; - section.items.push(this.transformToViewModel(hit)); + section.items.push(hit); } } @@ -198,7 +190,7 @@ export class SearchSrv { }; return this.backendSrv.search(query).then(results => { - section.items = _.map(results, this.transformToViewModel); + section.items = results; return Promise.resolve(section); }); } From 7734df1d1859d7aed6a8c04d0a3131eaa1cd7e86 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 00:48:36 +0100 Subject: [PATCH 19/84] dashboards: fix links to recently viewed and starred dashboards Use new property url from dashboard search for linking to dashboards #7883 --- public/app/plugins/panel/dashlist/module.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/dashlist/module.html b/public/app/plugins/panel/dashlist/module.html index 7f71811ac08..8fa3e7ef71f 100644 --- a/public/app/plugins/panel/dashlist/module.html +++ b/public/app/plugins/panel/dashlist/module.html @@ -4,7 +4,7 @@ {{group.header}}
- + {{dash.title}} From 6d2a555866b354d66ca270f6bd6ee2b7e0d7673d Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 30 Jan 2018 17:06:23 +0100 Subject: [PATCH 20/84] Drops unique index orgid_slug from dashboards. --- pkg/services/sqlstore/migrations/dashboard_mig.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 7da7c5a974f..edca733e174 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -163,4 +163,8 @@ func addDashboardMigration(mg *Migrator) { mg.AddMigration("Add unique index dashboard_org_id_uid", NewAddIndexMigration(dashboardV2, &Index{ Cols: []string{"org_id", "uid"}, Type: UniqueIndex, })) + + mg.AddMigration("Remove unique index org_id_slug", NewDropIndexMigration(dashboardV2, &Index{ + Cols: []string{"org_id", "slug"}, Type: UniqueIndex, + })) } From bb3183f6cdc8e92c98d02a079cb1a8f1af776e75 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 30 Jan 2018 18:57:33 +0100 Subject: [PATCH 21/84] removes uniqnes check on slug when saving dashboards --- pkg/api/dashboard.go | 2 +- pkg/models/dashboards.go | 3 ++- pkg/services/sqlstore/dashboard.go | 19 ++++++++++--------- pkg/services/sqlstore/dashboard_test.go | 16 ++++++++++++++++ .../sqlstore/dashboard_version_test.go | 5 ++--- tests/test-app/dashboards/connections.json | 1 + .../dashboards/connections_result.json | 1 + 7 files changed, 33 insertions(+), 14 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index a1c9950c457..77d3d8882f5 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -214,7 +214,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { } if err != nil { - if err == m.ErrDashboardWithSameNameExists { + if err == m.ErrDashboardWithSameUIDExists { return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()}) } if err == m.ErrDashboardVersionMismatch { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 84ede04f226..cf7f6355537 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -16,7 +16,7 @@ import ( var ( ErrDashboardNotFound = errors.New("Dashboard not found") ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") - ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists") + ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid 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") @@ -116,6 +116,7 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { dash.Uid = uid } else { dash.Uid = util.GenerateShortUid() + dash.Data.Set("uid", dash.Uid) } return dash diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 84cfd1999c7..376e4e3c382 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -25,7 +25,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { dash := cmd.GetDashboardModel() // try get existing dashboard - var existing, sameTitle m.Dashboard + var existing m.Dashboard if dash.Id > 0 { dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) @@ -51,19 +51,20 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } } - sameTitleExists, err := sess.Where("org_id=? AND slug=?", dash.OrgId, dash.Slug).Get(&sameTitle) + var sameUid m.Dashboard + sameUidExists, err := sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&sameUid) if err != nil { return err } - if sameTitleExists { - // another dashboard with same name - if dash.Id != sameTitle.Id { + if sameUidExists { + // another dashboard with same uid + if dash.Id != sameUid.Id { if cmd.Overwrite { - dash.Id = sameTitle.Id - dash.Version = sameTitle.Version + dash.Id = sameUid.Id + dash.Version = sameUid.Version } else { - return m.ErrDashboardWithSameNameExists + return m.ErrDashboardWithSameUIDExists } } } @@ -89,7 +90,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { dash.Updated = cmd.UpdatedAt } - affectedRows, err = sess.MustCols("folder_id", "has_acl").Id(dash.Id).Update(dash) + affectedRows, err = sess.MustCols("folder_id", "has_acl").ID(dash.Id).Update(dash) } if err != nil { diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 19e82614718..e8fb5332d32 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -207,6 +207,22 @@ func TestDashboardDataAccess(t *testing.T) { } err := SaveDashboard(&cmd) + So(err, ShouldBeNil) + }) + + Convey("Should not be able to save dashboard with same uid", func() { + cmd := m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "test dash 23", + "uid": "dsfalkjngailuedt", + }), + } + + err := SaveDashboard(&cmd) + So(err, ShouldBeNil) + err = SaveDashboard(&cmd) So(err, ShouldNotBeNil) }) diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index cbc33c8ad84..e20ac897b3d 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -12,7 +12,7 @@ import ( ) func updateTestDashboard(dashboard *m.Dashboard, data map[string]interface{}) { - data["title"] = dashboard.Title + data["uid"] = dashboard.Uid saveCmd := m.SaveDashboardCommand{ OrgId: dashboard.OrgId, @@ -44,12 +44,11 @@ func TestGetDashboardVersion(t *testing.T) { dashCmd := m.GetDashboardQuery{ OrgId: savedDash.OrgId, - Slug: savedDash.Slug, + Uid: savedDash.Uid, } err = GetDashboard(&dashCmd) So(err, ShouldBeNil) - dashCmd.Result.Data.Del("uid") eq := reflect.DeepEqual(dashCmd.Result.Data, query.Result.Data) So(eq, ShouldEqual, true) }) diff --git a/tests/test-app/dashboards/connections.json b/tests/test-app/dashboards/connections.json index cc189d2d113..042dc4baec5 100644 --- a/tests/test-app/dashboards/connections.json +++ b/tests/test-app/dashboards/connections.json @@ -7,6 +7,7 @@ } ], + "uid": "1MHHlVjzz", "title": "Nginx Connections", "revision": 25, "schemaVersion": 11, diff --git a/tests/test-app/dashboards/connections_result.json b/tests/test-app/dashboards/connections_result.json index 4bf0570ac1e..2d2bc8de238 100644 --- a/tests/test-app/dashboards/connections_result.json +++ b/tests/test-app/dashboards/connections_result.json @@ -16,5 +16,6 @@ ], "schemaVersion": 11, "title": "Nginx Connections", + "uid": "1MHHlVjzz", "version": 0 } From 666d29fafae85b0d6f315d2bb8ca88f9fd166cbc Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Jan 2018 10:39:38 +0100 Subject: [PATCH 22/84] dashfolders: POC - Use separate component for "Add permission" #10676 --- .../ManageDashboards/FolderPermissions.tsx | 19 +- .../components/Permissions/AddPermissions.tsx | 144 ++++++++++++++ .../components/Permissions/Permissions.tsx | 61 +----- .../app/core/components/Picker/TeamPicker.tsx | 4 +- .../app/core/components/Picker/UserPicker.tsx | 7 +- .../app/core/components/Picker/withPicker.tsx | 1 + .../PermissionsStore/PermissionsStore.ts | 184 +++++++++++++----- public/sass/components/_gf-form.scss | 6 + 8 files changed, 315 insertions(+), 111 deletions(-) create mode 100644 public/app/core/components/Permissions/AddPermissions.tsx diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 3214382732a..98f63a46cbe 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -6,11 +6,14 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import AddPermissions from 'app/core/components/Permissions/AddPermissions'; + @inject('nav', 'folder', 'view', 'permissions') @observer export class FolderPermissions extends Component { constructor(props) { super(props); + this.handleAddPermission = this.handleAddPermission.bind(this); this.loadStore(); } @@ -21,6 +24,11 @@ export class FolderPermissions extends Component { }); } + handleAddPermission() { + const { permissions } = this.props; + permissions.toggleAddPermissions(); + } + render() { const { nav, folder, permissions, backendSrv } = this.props; @@ -34,13 +42,20 @@ export class FolderPermissions extends Component {
-
+

Folder Permissions

+
+
- + {permissions.isAddPermissionsVisible ? ( + + ) : null}
diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx new file mode 100644 index 00000000000..36b57e448e7 --- /dev/null +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -0,0 +1,144 @@ +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore'; +import UserPicker, { User } from 'app/core/components/Picker/UserPicker'; +import TeamPicker, { Team } from 'app/core/components/Picker/TeamPicker'; +import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; +import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; + +export interface IProps { + permissions: any; + backendSrv: any; + dashboardId: any; +} + +@observer +class AddPermissions extends Component { + constructor(props) { + super(props); + this.userPicked = this.userPicked.bind(this); + this.teamPicked = this.teamPicked.bind(this); + this.permissionPicked = this.permissionPicked.bind(this); + this.typeChanged = this.typeChanged.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + componentWillMount() { + const { permissions } = this.props; + permissions.resetNewType(); + } + + typeChanged(evt) { + const { value } = evt.target; + const { permissions } = this.props; + + // if (value === 'Viewer' || value === 'Editor') { + // // permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId); + // // this.resetNewType(); + // return; + // } + + permissions.setNewType(value); + } + + userPicked(user: User) { + const { permissions } = this.props; + if (!user) { + permissions.newItem.setUser(null, null); + return; + } + permissions.newItem.setUser(user.id, user.login); + // return permissions.addStoreItem({ userId: user.id, userLogin: user.login, permission: 1 }); + } + + teamPicked(team: Team) { + const { permissions } = this.props; + if (!team) { + permissions.newItem.setTeam(null, null); + return; + } + permissions.newItem.setTeam(team.id, team.name); + } + + permissionPicked(permission: OptionWithDescription) { + const { permissions } = this.props; + permissions.newItem.setPermission(permission.value); + } + + resetNewType() { + const { permissions } = this.props; + permissions.resetNewType(); + } + + handleSubmit(evt) { + evt.preventDefault(); + const { permissions } = this.props; + permissions.addStoreItem(); + } + + render() { + const { permissions, backendSrv } = this.props; + const newItem = permissions.newItem; + + return ( +
+
+
Add Permission For
+
+
+
+ +
+
+ + {newItem.type === 'User' ? ( +
+ +
+ ) : null} + + {newItem.type === 'Group' ? ( +
+ +
+ ) : null} + +
+ +
+ +
+ +
+
+
+ {permissions.error ? ( +
+ + + {permissions.error} + +
+ ) : null} +
+ ); + } +} + +export default AddPermissions; diff --git a/public/app/core/components/Permissions/Permissions.tsx b/public/app/core/components/Permissions/Permissions.tsx index f5af579094b..1849d7da173 100644 --- a/public/app/core/components/Permissions/Permissions.tsx +++ b/public/app/core/components/Permissions/Permissions.tsx @@ -1,9 +1,6 @@ import React, { Component } from 'react'; import PermissionsList from './PermissionsList'; import { observer } from 'mobx-react'; -import UserPicker, { User } from 'app/core/components/Picker/UserPicker'; -import TeamPicker, { Team } from 'app/core/components/Picker/TeamPicker'; -import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore'; import { FolderInfo } from './FolderInfo'; export interface DashboardAcl { @@ -40,8 +37,6 @@ class Permissions extends Component { this.permissionChanged = this.permissionChanged.bind(this); this.typeChanged = this.typeChanged.bind(this); this.removeItem = this.removeItem.bind(this); - this.userPicked = this.userPicked.bind(this); - this.teamPicked = this.teamPicked.bind(this); this.loadStore(dashboardId, isFolder); } @@ -77,18 +72,8 @@ class Permissions extends Component { permissions.setNewType(value); } - userPicked(user: User) { - const { permissions } = this.props; - return permissions.addStoreItem({ userId: user.id, userLogin: user.login, permission: 1 }); - } - - teamPicked(team: Team) { - const { permissions } = this.props; - return permissions.addStoreItem({ teamId: team.id, team: team.name, permission: 1 }); - } - render() { - const { permissions, folderInfo, backendSrv } = this.props; + const { permissions, folderInfo } = this.props; return (
@@ -99,50 +84,6 @@ class Permissions extends Component { fetching={permissions.fetching} folderInfo={folderInfo} /> -
-
-
Add Permission For
-
-
-
- -
-
- - {permissions.newType === 'User' ? ( -
- -
- ) : null} - - {permissions.newType === 'Group' ? ( -
- -
- ) : null} -
-
- {permissions.error ? ( -
- - - {permissions.error} - -
- ) : null} -
); } diff --git a/public/app/core/components/Picker/TeamPicker.tsx b/public/app/core/components/Picker/TeamPicker.tsx index 4b5a049c0ff..18f7258c221 100644 --- a/public/app/core/components/Picker/TeamPicker.tsx +++ b/public/app/core/components/Picker/TeamPicker.tsx @@ -9,6 +9,7 @@ export interface IProps { isLoading: boolean; toggleLoading: any; handlePicked: (user) => void; + value?: string; } export interface Team { @@ -54,7 +55,7 @@ class TeamPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked } = this.props; + const { isLoading, handlePicked, value } = this.props; return (
@@ -70,6 +71,7 @@ class TeamPicker extends Component { className="width-8 gf-form-input gf-form-input--form-dropdown" optionComponent={PickerOption} placeholder="Choose" + value={value} />
); diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 141371813a2..99b0ef258ca 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -9,6 +9,7 @@ export interface IProps { isLoading: boolean; toggleLoading: any; handlePicked: (user) => void; + value?: string; } export interface User { @@ -53,8 +54,8 @@ class UserPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked } = this.props; - + const { isLoading, handlePicked, value } = this.props; + console.log('value', value); return (
{ className="width-8 gf-form-input gf-form-input--form-dropdown" optionComponent={PickerOption} placeholder="Choose" + value={value} + autosize={true} />
); diff --git a/public/app/core/components/Picker/withPicker.tsx b/public/app/core/components/Picker/withPicker.tsx index bdfcb02676e..cf3954850b2 100644 --- a/public/app/core/components/Picker/withPicker.tsx +++ b/public/app/core/components/Picker/withPicker.tsx @@ -3,6 +3,7 @@ export interface IProps { backendSrv: any; handlePicked: (data) => void; + value?: string; } export default function withPicker(WrappedComponent) { diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index 52e5dcdb339..ae100fd0751 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -13,15 +13,62 @@ export const permissionOptions = [ }, ]; -export const aclTypes = [ - { value: 'Group', text: 'Team' }, - { value: 'User', text: 'User' }, - { value: 'Viewer', text: 'Everyone With Viewer Role' }, - { value: 'Editor', text: 'Everyone With Editor Role' }, -]; +export const aclTypeValues = { + GROUP: { value: 'Group', text: 'Team' }, + USER: { value: 'User', text: 'User' }, + VIEWER: { value: 'Viewer', text: 'Everyone With Viewer Role' }, + EDITOR: { value: 'Editor', text: 'Everyone With Editor Role' }, +}; + +export const aclTypes = Object.keys(aclTypeValues).map(item => aclTypeValues[item]); const defaultNewType = aclTypes[0].value; +const NewPermissionsItem = types + .model('NewPermissionsItem', { + type: types.optional( + types.enumeration(Object.keys(aclTypeValues).map(item => aclTypeValues[item].value)), + defaultNewType + ), + userId: types.maybe(types.number), + userLogin: types.maybe(types.string), + teamId: types.maybe(types.number), + team: types.maybe(types.string), + permission: types.optional(types.number, 1), + }) + .views(self => ({ + isValid: () => { + switch (self.type) { + case aclTypeValues.GROUP.value: + return self.teamId && self.team; + case aclTypeValues.USER.value: + return self.userId && self.userLogin; + case aclTypeValues.VIEWER.value: + case aclTypeValues.EDITOR.value: + return true; + default: + return false; + } + }, + })) + .actions(self => ({ + setUser(userId: number, userLogin: string) { + self.userId = userId; + self.userLogin = userLogin; + self.teamId = null; + self.team = null; + }, + setTeam(teamId: number, team: string) { + self.userId = null; + self.userLogin = null; + self.teamId = teamId; + self.team = team; + }, + setPermission(permission: number) { + self.permission = permission; + }, + })); + export const PermissionsStore = types .model('PermissionsStore', { fetching: types.boolean, @@ -31,6 +78,8 @@ export const PermissionsStore = types error: types.maybe(types.string), originalItems: types.optional(types.array(PermissionsStoreItem), []), newType: types.optional(types.string, defaultNewType), + newItem: types.maybe(NewPermissionsItem), + isAddPermissionsVisible: types.optional(types.boolean, false), }) .views(self => ({ isValid: item => { @@ -46,48 +95,91 @@ export const PermissionsStore = types return true; }, })) - .actions(self => ({ - load: flow(function* load(dashboardId: number, isFolder: boolean) { - const backendSrv = getEnv(self).backendSrv; - self.fetching = true; - self.isFolder = isFolder; - self.dashboardId = dashboardId; - const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`); - const items = prepareServerResponse(res, dashboardId, isFolder); - self.items = items; - self.originalItems = items; - self.fetching = false; - }), - addStoreItem: flow(function* addStoreItem(item) { + .actions(self => { + const resetNewType = () => { self.error = null; - if (!self.isValid(item)) { - return undefined; - } + self.newItem = NewPermissionsItem.create(); + }; - self.items.push(prepareItem(item, self.dashboardId, self.isFolder)); - return updateItems(self); - }), - removeStoreItem: flow(function* removeStoreItem(idx: number) { - self.error = null; - self.items.splice(idx, 1); - return updateItems(self); - }), - updatePermissionOnIndex: flow(function* updatePermissionOnIndex( - idx: number, - permission: number, - permissionName: string - ) { - self.error = null; - self.items[idx].updatePermission(permission, permissionName); - return updateItems(self); - }), - setNewType(newType: string) { - self.newType = newType; - }, - resetNewType() { - self.newType = defaultNewType; - }, - })); + return { + load: flow(function* load(dashboardId: number, isFolder: boolean) { + const backendSrv = getEnv(self).backendSrv; + self.fetching = true; + self.isFolder = isFolder; + self.dashboardId = dashboardId; + const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`); + const items = prepareServerResponse(res, dashboardId, isFolder); + self.items = items; + self.originalItems = items; + self.fetching = false; + }), + addStoreItem: flow(function* addStoreItem() { + self.error = null; + let item = { + type: self.newItem.type, + permission: self.newItem.permission, + team: undefined, + teamId: undefined, + userLogin: undefined, + userId: undefined, + role: undefined, + }; + switch (self.newItem.type) { + case aclTypeValues.GROUP.value: + item.team = self.newItem.team; + item.teamId = self.newItem.teamId; + break; + case aclTypeValues.USER.value: + item.userLogin = self.newItem.userLogin; + item.userId = self.newItem.userId; + break; + case aclTypeValues.VIEWER.value: + case aclTypeValues.EDITOR.value: + item.role = self.newItem.type; + break; + default: + throw Error('Unknown type: ' + self.newItem.type); + } + + if (!self.isValid(item)) { + throw Error('New item not valid'); + } + + self.items.push(prepareItem(item, self.dashboardId, self.isFolder)); + resetNewType(); + return updateItems(self); + }), + removeStoreItem: flow(function* removeStoreItem(idx: number) { + self.error = null; + self.items.splice(idx, 1); + return updateItems(self); + }), + updatePermissionOnIndex: flow(function* updatePermissionOnIndex( + idx: number, + permission: number, + permissionName: string + ) { + self.error = null; + self.items[idx].updatePermission(permission, permissionName); + return updateItems(self); + }), + setNewType(newType: string) { + self.newItem = NewPermissionsItem.create({ type: newType }); + }, + resetNewType() { + resetNewType(); + }, + toggleAddPermissions() { + self.isAddPermissionsVisible = !self.isAddPermissionsVisible; + }, + showAddPermissions() { + self.isAddPermissionsVisible = true; + }, + hideAddPermissions() { + self.isAddPermissionsVisible = false; + }, + }; + }); const updateItems = self => { self.error = null; diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 2113bbae43c..dd6c7c39b83 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -392,3 +392,9 @@ select.gf-form-input ~ .gf-form-help-icon { top: 10px; color: $text-muted; } + +.cta-form { + padding: 1rem; + background-color: $dark-2; + margin-bottom: 1rem; +} From 3efb3d8efa11e05d8e629f1043f657ecd89d0572 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 11:01:01 +0100 Subject: [PATCH 23/84] dashboards: add new default frontend route for rendering a dashboard panel New default route /d-solo// where dashboard panel are rendered by unique identifier and panel identifier. If old route /dashboard-solo/db/ are used, try to lookup dashboard by slug and redirect to new default route. Change url logic for sharing a panel so that the new default route for rendering a dashboard panel are used. #7883 --- public/app/features/dashboard/shareModalCtrl.ts | 7 ++----- .../dashboard/specs/share_modal_ctrl_specs.ts | 4 ++-- public/app/features/panel/solo_panel_ctrl.ts | 14 ++++++++++++-- public/app/routes/routes.ts | 6 ++++++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index 508658cd53b..2bde5c2d198 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -73,17 +73,14 @@ export class ShareModalCtrl { $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params); - var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/'); + var soloUrl = baseUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); delete params.fullscreen; delete params.edit; soloUrl = linkSrv.addParamsToUrl(soloUrl, params); $scope.iframeHtml = ''; - $scope.imageUrl = soloUrl.replace( - config.appSubUrl + '/dashboard-solo/', - config.appSubUrl + '/render/dashboard-solo/' - ); + $scope.imageUrl = soloUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/'); $scope.imageUrl += '&width=1000'; $scope.imageUrl += '&height=500'; $scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format('Z')); diff --git a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts b/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts index 8bf8f53ed46..e65d9c98f82 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts @@ -43,12 +43,12 @@ describe('ShareModalCtrl', function() { }); it('should generate render url', function() { - ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/db/my-dash'; + ctx.$location.$$absUrl = 'http://dashboards.grafana.com/d/abcdefghi/my-dash'; ctx.scope.panel = { id: 22 }; ctx.scope.init(); - var base = 'http://dashboards.grafana.com/render/dashboard-solo/db/my-dash'; + var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash'; var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; expect(ctx.scope.imageUrl).to.contain(base + params); }); diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index f141f89eb80..d8642bea4a0 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -2,7 +2,7 @@ import angular from 'angular'; export class SoloPanelCtrl { /** @ngInject */ - constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv) { + constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) { var panelId; $scope.init = function() { @@ -13,7 +13,17 @@ export class SoloPanelCtrl { $scope.onAppEvent('dashboard-initialized', $scope.initPanelScope); - dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { + // if no uid, redirect to new route based on slug + if (!$routeParams.uid) { + backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => { + if (res) { + $location.path(res.meta.url.replace('/d/', '/d-solo/')); + } + }); + return; + } + + dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) { result.meta.soloMode = true; $scope.initDashboard(result, $scope); }); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 081fb88af6e..865f4ce1682 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -26,6 +26,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { reloadOnSearch: false, pageClass: 'page-dashboard', }) + .when('/d-solo/:uid/:slug', { + templateUrl: 'public/app/features/panel/partials/soloPanel.html', + controller: 'SoloPanelCtrl', + reloadOnSearch: false, + pageClass: 'page-dashboard', + }) .when('/dashboard-solo/:type/:slug', { templateUrl: 'public/app/features/panel/partials/soloPanel.html', controller: 'SoloPanelCtrl', From a99331cdb94c12d43d7d385a35945c8163294e61 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 14:05:24 +0100 Subject: [PATCH 24/84] dashboards: redirect from old url used to load dashboard to new url If legacy backend routes (/dashboard/db/ and /dashboard-solo/db/) are requested we try to redirect to new routes with a 301 Moved Permanently #7883 --- pkg/api/api.go | 8 +++- pkg/middleware/dashboard_redirect.go | 46 +++++++++++++++++++ pkg/middleware/dashboard_redirect_test.go | 54 +++++++++++++++++++++++ pkg/middleware/middleware_test.go | 14 ++++++ 4 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 pkg/middleware/dashboard_redirect.go create mode 100644 pkg/middleware/dashboard_redirect_test.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 2d45868e58d..18e18bf5d54 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -15,6 +15,8 @@ func (hs *HttpServer) registerRoutes() { reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}) reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN) reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN) + redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardUrl() + redirectFromLegacyDashboardSoloUrl := middleware.RedirectFromLegacyDashboardSoloUrl() quota := middleware.Quota bind := binding.Bind @@ -63,9 +65,11 @@ func (hs *HttpServer) registerRoutes() { r.Get("/plugins/:id/edit", reqSignedIn, Index) r.Get("/plugins/:id/page/:page", reqSignedIn, Index) - r.Get("/dashboard/*", reqSignedIn, Index) + r.Get("/d/:uid/:slug", reqSignedIn, Index) + r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardUrl, Index) r.Get("/dashboard-solo/snapshot/*", Index) - r.Get("/dashboard-solo/*", reqSignedIn, Index) + r.Get("/d-solo/:uid/:slug", reqSignedIn, Index) + r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloUrl, Index) r.Get("/import/dashboard", reqSignedIn, Index) r.Get("/dashboards/", reqSignedIn, Index) r.Get("/dashboards/*", reqSignedIn, Index) diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go new file mode 100644 index 00000000000..1ca4ef741c6 --- /dev/null +++ b/pkg/middleware/dashboard_redirect.go @@ -0,0 +1,46 @@ +package middleware + +import ( + "strings" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "gopkg.in/macaron.v1" +) + +func getDashboardUrlBySlug(orgId int64, slug string) (string, error) { + query := m.GetDashboardQuery{Slug: slug, OrgId: orgId} + + if err := bus.Dispatch(&query); err != nil { + return "", m.ErrDashboardNotFound + } + + return m.GetDashboardUrl(query.Result.Uid, query.Result.Slug), nil +} + +func RedirectFromLegacyDashboardUrl() macaron.Handler { + return func(c *Context) { + slug := c.Params("slug") + + if slug != "" { + if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil { + c.Redirect(url, 301) + return + } + } + } +} + +func RedirectFromLegacyDashboardSoloUrl() macaron.Handler { + return func(c *Context) { + slug := c.Params("slug") + + if slug != "" { + if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil { + url = strings.Replace(url, "/d/", "/d-solo/", 1) + c.Redirect(url, 301) + return + } + } + } +} diff --git a/pkg/middleware/dashboard_redirect_test.go b/pkg/middleware/dashboard_redirect_test.go new file mode 100644 index 00000000000..bff4ee2253c --- /dev/null +++ b/pkg/middleware/dashboard_redirect_test.go @@ -0,0 +1,54 @@ +package middleware + +import ( + "strings" + "testing" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestMiddlewareDashboardRedirect(t *testing.T) { + Convey("Given the dashboard redirect middleware", t, func() { + bus.ClearBusHandlers() + redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardUrl() + redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloUrl() + + fakeDash := m.NewDashboard("Child dash") + fakeDash.Id = 1 + fakeDash.FolderId = 1 + fakeDash.HasAcl = false + + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = fakeDash + return nil + }) + + middlewareScenario("GET dashboard by legacy url", func(sc *scenarioContext) { + sc.m.Get("/dashboard/db/:slug", redirectFromLegacyDashboardUrl, sc.defaultHandler) + + sc.fakeReqWithParams("GET", "/dashboard/db/dash", map[string]string{}).exec() + + Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { + So(sc.resp.Code, ShouldEqual, 301) + redirectUrl, _ := sc.resp.Result().Location() + So(redirectUrl.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)) + }) + }) + + middlewareScenario("GET dashboard solo by legacy url", func(sc *scenarioContext) { + sc.m.Get("/dashboard-solo/db/:slug", redirectFromLegacyDashboardSoloUrl, sc.defaultHandler) + + sc.fakeReqWithParams("GET", "/dashboard-solo/db/dash", map[string]string{}).exec() + + Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { + So(sc.resp.Code, ShouldEqual, 301) + redirectUrl, _ := sc.resp.Result().Location() + expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) + expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1) + So(redirectUrl.Path, ShouldEqual, expectedUrl) + }) + }) + }) +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 0d9e0e5b973..ffd8e8a0af0 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -399,6 +399,20 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { return sc } +func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext { + sc.resp = httptest.NewRecorder() + req, err := http.NewRequest(method, url, nil) + q := req.URL.Query() + for k, v := range queryParams { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + So(err, ShouldBeNil) + sc.req = req + + return sc +} + func (sc *scenarioContext) handler(fn handlerFunc) *scenarioContext { sc.handlerFunc = fn return sc From 57edf890338e93f70f8c2fe3176d7702d7c90f1c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 14:07:49 +0100 Subject: [PATCH 25/84] dashboards: make scripted dashboards work using the old legacy urls Scripted dashboards are still requested from /dashboard/script/scripted.js #7883 --- pkg/api/api.go | 2 ++ public/app/features/dashboard/shareModalCtrl.ts | 9 +++++++-- .../dashboard/specs/share_modal_ctrl_specs.ts | 11 +++++++++++ public/app/features/panel/solo_panel_ctrl.ts | 2 +- public/app/routes/dashboard_loaders.ts | 2 +- 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 18e18bf5d54..6d2207971e7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -67,9 +67,11 @@ func (hs *HttpServer) registerRoutes() { r.Get("/d/:uid/:slug", reqSignedIn, Index) r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardUrl, Index) + r.Get("/dashboard/script/*", reqSignedIn, Index) r.Get("/dashboard-solo/snapshot/*", Index) r.Get("/d-solo/:uid/:slug", reqSignedIn, Index) r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloUrl, Index) + r.Get("/dashboard-solo/script/*", reqSignedIn, Index) r.Get("/import/dashboard", reqSignedIn, Index) r.Get("/dashboards/", reqSignedIn, Index) r.Get("/dashboards/*", reqSignedIn, Index) diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index 2bde5c2d198..8e30eaef91e 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -73,14 +73,19 @@ export class ShareModalCtrl { $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params); - var soloUrl = baseUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); + var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/'); + soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); delete params.fullscreen; delete params.edit; soloUrl = linkSrv.addParamsToUrl(soloUrl, params); $scope.iframeHtml = ''; - $scope.imageUrl = soloUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/'); + $scope.imageUrl = soloUrl.replace( + config.appSubUrl + '/dashboard-solo/', + config.appSubUrl + '/render/dashboard-solo/' + ); + $scope.imageUrl = $scope.imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/'); $scope.imageUrl += '&width=1000'; $scope.imageUrl += '&height=500'; $scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format('Z')); diff --git a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts b/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts index e65d9c98f82..fc70a54a41c 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts @@ -53,6 +53,17 @@ describe('ShareModalCtrl', function() { expect(ctx.scope.imageUrl).to.contain(base + params); }); + it('should generate render url for scripted dashboard', function() { + ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/script/my-dash.js'; + + ctx.scope.panel = { id: 22 }; + + ctx.scope.init(); + var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js'; + var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC'; + expect(ctx.scope.imageUrl).to.contain(base + params); + }); + it('should remove panel id when no panel in scope', function() { ctx.$location.path('/test'); ctx.scope.options.forCurrent = true; diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index d8642bea4a0..575c9e4c3b9 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -14,7 +14,7 @@ export class SoloPanelCtrl { $scope.onAppEvent('dashboard-initialized', $scope.initPanelScope); // if no uid, redirect to new route based on slug - if (!$routeParams.uid) { + if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) { backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => { if (res) { $location.path(res.meta.url.replace('/d/', '/d-solo/')); diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index e3e6f4c18bc..971c83f0414 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -19,7 +19,7 @@ export class LoadDashboardCtrl { } // if no uid, redirect to new route based on slug - if (!$routeParams.uid) { + if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) { backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => { if (res) { $location.path(res.meta.url); From 7e9605259407d8db483c5482c96c3d65b07e8e38 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 31 Jan 2018 13:47:28 +0100 Subject: [PATCH 26/84] ensure dashboard title is unique in folder --- pkg/models/dashboards.go | 17 +- pkg/services/sqlstore/dashboard.go | 24 ++ .../sqlstore/dashboard_folder_test.go | 219 +++++++++++++ pkg/services/sqlstore/dashboard_test.go | 287 +++++------------- 4 files changed, 336 insertions(+), 211 deletions(-) create mode 100644 pkg/services/sqlstore/dashboard_folder_test.go diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index cf7f6355537..c4361eefd7d 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -14,14 +14,15 @@ import ( // Typed errors var ( - ErrDashboardNotFound = errors.New("Dashboard not found") - ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") - ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid 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") + ErrDashboardNotFound = errors.New("Dashboard 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") ) type UpdatePluginDashboardError struct { diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 376e4e3c382..c378d70af10 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -20,6 +20,8 @@ func init() { bus.AddHandler("sql", GetDashboardsByPluginId) } + + func SaveDashboard(cmd *m.SaveDashboardCommand) error { return inTransaction(func(sess *DBSession) error { dash := cmd.GetDashboardModel() @@ -69,6 +71,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { } } + err = guaranteeDashboardNameIsUniqueInFolder(sess, dash) + if err != nil { + return err + } + err = setHasAcl(sess, dash) if err != nil { return err @@ -140,6 +147,23 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { }) } +func guaranteeDashboardNameIsUniqueInFolder(sess *DBSession, dash *m.Dashboard) error { + var sameNameInFolder m.Dashboard + sameNameInFolderExist, err := sess.Where("org_id=? AND title=? AND folder_id = ? AND uid <> ?", + dash.OrgId, dash.Title, dash.FolderId, dash.Uid). + Get(&sameNameInFolder) + + if err != nil { + return err + } + + if sameNameInFolderExist { + return m.ErrDashboardWithSameNameInFolderExists + } + + return nil +} + func setHasAcl(sess *DBSession, dash *m.Dashboard) error { // check if parent has acl if dash.FolderId > 0 { diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go new file mode 100644 index 00000000000..6f880e7bb4c --- /dev/null +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -0,0 +1,219 @@ +package sqlstore + +import ( + "testing" + + "github.com/go-xorm/xorm" + . "github.com/smartystreets/goconvey/convey" + + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/search" +) + +func TestDashboardFolderDataAccess(t *testing.T) { + var x *xorm.Engine + + Convey("Testing DB", t, func() { + x = InitTestDB(t) + + Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() { + folder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp") + dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp") + childDash := insertTestDashboard("test dash 23", 1, folder.Id, false, "prod", "webapp") + insertTestDashboard("test dash 45", 1, folder.Id, false, "prod") + + currentUser := createUser("viewer", "Viewer", false) + + Convey("and no acls are set", func() { + Convey("should return all dashboards", func() { + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].Id, ShouldEqual, folder.Id) + So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("and acl is set for dashboard folder", func() { + var otherUser int64 = 999 + updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) + + Convey("should not return folder", func() { + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) + }) + + Convey("when the user is given permission", func() { + updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT) + + Convey("should be able to access folder", func() { + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].Id, ShouldEqual, folder.Id) + So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("when the user is an admin", func() { + Convey("should be able to access folder", func() { + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{ + UserId: currentUser.Id, + OrgId: 1, + OrgRole: m.ROLE_ADMIN, + }, + OrgId: 1, + DashboardIds: []int64{folder.Id, dashInRoot.Id}, + } + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].Id, ShouldEqual, folder.Id) + So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) + }) + }) + }) + + Convey("and acl is set for dashboard child and folder has all permissions removed", func() { + var otherUser int64 = 999 + aclId := updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) + removeAcl(aclId) + updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT) + + Convey("should not return folder or child", func() { + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) + }) + + Convey("when the user is given permission to child", func() { + updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT) + + Convey("should be able to search for child dashboard but not folder", func() { + query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 2) + So(query.Result[0].Id, ShouldEqual, childDash.Id) + So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("when the user is an admin", func() { + Convey("should be able to search for child dash and folder", func() { + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{ + UserId: currentUser.Id, + OrgId: 1, + OrgRole: m.ROLE_ADMIN, + }, + OrgId: 1, + DashboardIds: []int64{folder.Id, dashInRoot.Id, childDash.Id}, + } + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 3) + So(query.Result[0].Id, ShouldEqual, folder.Id) + So(query.Result[1].Id, ShouldEqual, childDash.Id) + So(query.Result[2].Id, ShouldEqual, dashInRoot.Id) + }) + }) + }) + }) + + Convey("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func() { + folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod") + folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod") + dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod") + childDash1 := insertTestDashboard("child dash 1", 1, folder1.Id, false, "prod") + childDash2 := insertTestDashboard("child dash 2", 1, folder2.Id, false, "prod") + + currentUser := createUser("viewer", "Viewer", false) + var rootFolderId int64 = 0 + + Convey("and one folder is expanded, the other collapsed", func() { + Convey("should return dashboards in root and expanded folder", func() { + query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1} + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 4) + So(query.Result[0].Id, ShouldEqual, folder1.Id) + So(query.Result[1].Id, ShouldEqual, folder2.Id) + So(query.Result[2].Id, ShouldEqual, childDash1.Id) + So(query.Result[3].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("and acl is set for one dashboard folder", func() { + var otherUser int64 = 999 + updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT) + + Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() { + movedDash := moveDashboard(1, childDash2.Data, folder1.Id) + So(movedDash.HasAcl, ShouldBeTrue) + + Convey("should not return folder with acl or its children", func() { + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, + OrgId: 1, + DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + } + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() { + movedDash := moveDashboard(1, childDash1.Data, folder2.Id) + So(movedDash.HasAcl, ShouldBeFalse) + + Convey("should return folder without acl and its children", func() { + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, + OrgId: 1, + DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + } + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 4) + So(query.Result[0].Id, ShouldEqual, folder2.Id) + So(query.Result[1].Id, ShouldEqual, childDash1.Id) + So(query.Result[2].Id, ShouldEqual, childDash2.Id) + So(query.Result[3].Id, ShouldEqual, dashInRoot.Id) + }) + }) + + Convey("and a dashboard with an acl is moved to the folder without an acl", func() { + updateTestDashboardWithAcl(childDash1.Id, otherUser, m.PERMISSION_EDIT) + movedDash := moveDashboard(1, childDash1.Data, folder2.Id) + So(movedDash.HasAcl, ShouldBeTrue) + + Convey("should return folder without acl but not the dashboard with acl", func() { + query := &search.FindPersistedDashboardsQuery{ + SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, + OrgId: 1, + DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + } + err := SearchDashboards(query) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 3) + So(query.Result[0].Id, ShouldEqual, folder2.Id) + So(query.Result[1].Id, ShouldEqual, childDash2.Id) + So(query.Result[2].Id, ShouldEqual, dashInRoot.Id) + }) + }) + }) + }) + + }) +} diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index e8fb5332d32..6e77b44f8c1 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -196,18 +196,64 @@ func TestDashboardDataAccess(t *testing.T) { }) }) - Convey("Should not be able to save dashboard with same name", func() { - cmd := m.SaveDashboardCommand{ + Convey("Should be able to save dashboards with same name in different folders", func() { + firstSaveCmd := m.SaveDashboardCommand{ OrgId: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, - "title": "test dash 23", + "title": "test dash folder and title", "tags": []interface{}{}, + "uid": "randomHash", }), + FolderId: 3, } - err := SaveDashboard(&cmd) + err := SaveDashboard(&firstSaveCmd) So(err, ShouldBeNil) + + secondSaveCmd := m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "test dash folder and title", + "tags": []interface{}{}, + "uid": "moreRandomHash", + }), + FolderId: 1, + } + + err = SaveDashboard(&secondSaveCmd) + So(err, ShouldBeNil) + }) + + Convey("Should not be able to save dashboard with same name in the same folder", func() { + firstSaveCmd := m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "test dash folder and title", + "tags": []interface{}{}, + "uid": "randomHash", + }), + FolderId: 3, + } + + err := SaveDashboard(&firstSaveCmd) + So(err, ShouldBeNil) + + secondSaveCmd := m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "test dash folder and title", + "tags": []interface{}{}, + "uid": "moreRandomHash", + }), + FolderId: 3, + } + + err = SaveDashboard(&secondSaveCmd) + So(err, ShouldEqual, m.ErrDashboardWithSameNameInFolderExists) }) Convey("Should not be able to save dashboard with same uid", func() { @@ -226,6 +272,40 @@ func TestDashboardDataAccess(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("Should be able to update dashboard with the same title and folder id", func() { + cmd := m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + //"id": 1, + "uid": "randomHash", + "title": "folderId", + "style": "light", + "tags": []interface{}{}, + }), + FolderId: 2, + } + + err := SaveDashboard(&cmd) + So(err, ShouldBeNil) + So(cmd.Result.FolderId, ShouldEqual, 2) + + cmd = m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": cmd.Result.Id, + "uid": "randomHash", + "title": "folderId", + "style": "dark", + "version": cmd.Result.Version, + "tags": []interface{}{}, + }), + FolderId: 2, + } + + err = SaveDashboard(&cmd) + So(err, ShouldBeNil) + }) + Convey("Should be able to update dashboard and remove folderId", func() { cmd := m.SaveDashboardCommand{ OrgId: 1, @@ -315,205 +395,6 @@ func TestDashboardDataAccess(t *testing.T) { }) }) - Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() { - folder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp") - dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod", "webapp") - childDash := insertTestDashboard("test dash 23", 1, folder.Id, false, "prod", "webapp") - insertTestDashboard("test dash 45", 1, folder.Id, false, "prod") - - currentUser := createUser("viewer", "Viewer", false) - - Convey("and no acls are set", func() { - Convey("should return all dashboards", func() { - query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 2) - So(query.Result[0].Id, ShouldEqual, folder.Id) - So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) - }) - }) - - Convey("and acl is set for dashboard folder", func() { - var otherUser int64 = 999 - updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) - - Convey("should not return folder", func() { - query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 1) - So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) - }) - - Convey("when the user is given permission", func() { - updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT) - - Convey("should be able to access folder", func() { - query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}} - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 2) - So(query.Result[0].Id, ShouldEqual, folder.Id) - So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) - }) - }) - - Convey("when the user is an admin", func() { - Convey("should be able to access folder", func() { - query := &search.FindPersistedDashboardsQuery{ - SignedInUser: &m.SignedInUser{ - UserId: currentUser.Id, - OrgId: 1, - OrgRole: m.ROLE_ADMIN, - }, - OrgId: 1, - DashboardIds: []int64{folder.Id, dashInRoot.Id}, - } - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 2) - So(query.Result[0].Id, ShouldEqual, folder.Id) - So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) - }) - }) - }) - - Convey("and acl is set for dashboard child and folder has all permissions removed", func() { - var otherUser int64 = 999 - aclId := updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) - removeAcl(aclId) - updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT) - - Convey("should not return folder or child", func() { - query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 1) - So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) - }) - - Convey("when the user is given permission to child", func() { - updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT) - - Convey("should be able to search for child dashboard but not folder", func() { - query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 2) - So(query.Result[0].Id, ShouldEqual, childDash.Id) - So(query.Result[1].Id, ShouldEqual, dashInRoot.Id) - }) - }) - - Convey("when the user is an admin", func() { - Convey("should be able to search for child dash and folder", func() { - query := &search.FindPersistedDashboardsQuery{ - SignedInUser: &m.SignedInUser{ - UserId: currentUser.Id, - OrgId: 1, - OrgRole: m.ROLE_ADMIN, - }, - OrgId: 1, - DashboardIds: []int64{folder.Id, dashInRoot.Id, childDash.Id}, - } - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 3) - So(query.Result[0].Id, ShouldEqual, folder.Id) - So(query.Result[1].Id, ShouldEqual, childDash.Id) - So(query.Result[2].Id, ShouldEqual, dashInRoot.Id) - }) - }) - }) - }) - - Convey("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func() { - folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod") - folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod") - dashInRoot := insertTestDashboard("test dash 67", 1, 0, false, "prod") - childDash1 := insertTestDashboard("child dash 1", 1, folder1.Id, false, "prod") - childDash2 := insertTestDashboard("child dash 2", 1, folder2.Id, false, "prod") - - currentUser := createUser("viewer", "Viewer", false) - var rootFolderId int64 = 0 - - Convey("and one folder is expanded, the other collapsed", func() { - Convey("should return dashboards in root and expanded folder", func() { - query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, OrgId: 1} - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 4) - So(query.Result[0].Id, ShouldEqual, folder1.Id) - So(query.Result[1].Id, ShouldEqual, folder2.Id) - So(query.Result[2].Id, ShouldEqual, childDash1.Id) - So(query.Result[3].Id, ShouldEqual, dashInRoot.Id) - }) - }) - - Convey("and acl is set for one dashboard folder", func() { - var otherUser int64 = 999 - updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT) - - Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() { - movedDash := moveDashboard(1, childDash2.Data, folder1.Id) - So(movedDash.HasAcl, ShouldBeTrue) - - Convey("should not return folder with acl or its children", func() { - query := &search.FindPersistedDashboardsQuery{ - SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, - OrgId: 1, - DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, - } - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 1) - So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) - }) - }) - - Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() { - movedDash := moveDashboard(1, childDash1.Data, folder2.Id) - So(movedDash.HasAcl, ShouldBeFalse) - - Convey("should return folder without acl and its children", func() { - query := &search.FindPersistedDashboardsQuery{ - SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, - OrgId: 1, - DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, - } - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 4) - So(query.Result[0].Id, ShouldEqual, folder2.Id) - So(query.Result[1].Id, ShouldEqual, childDash1.Id) - So(query.Result[2].Id, ShouldEqual, childDash2.Id) - So(query.Result[3].Id, ShouldEqual, dashInRoot.Id) - }) - }) - - Convey("and a dashboard with an acl is moved to the folder without an acl", func() { - updateTestDashboardWithAcl(childDash1.Id, otherUser, m.PERMISSION_EDIT) - movedDash := moveDashboard(1, childDash1.Data, folder2.Id) - So(movedDash.HasAcl, ShouldBeTrue) - - Convey("should return folder without acl but not the dashboard with acl", func() { - query := &search.FindPersistedDashboardsQuery{ - SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1}, - OrgId: 1, - DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, - } - err := SearchDashboards(query) - So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 3) - So(query.Result[0].Id, ShouldEqual, folder2.Id) - So(query.Result[1].Id, ShouldEqual, childDash2.Id) - So(query.Result[2].Id, ShouldEqual, dashInRoot.Id) - }) - }) - }) - }) - Convey("Given a plugin with imported dashboards", func() { pluginId := "test-app" From 2ad4c30bc652079b919bf227eae90bacb5810a12 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Jan 2018 14:19:07 +0100 Subject: [PATCH 27/84] ux: POC - Update "Add permissions" design and add a fancy animation #10676 --- package.json | 1 + .../ManageDashboards/FolderPermissions.tsx | 12 ++++-- .../core/components/Animations/SlideDown.tsx | 37 +++++++++++++++++++ .../components/Permissions/AddPermissions.tsx | 8 ++-- .../app/core/components/Picker/TeamPicker.tsx | 4 +- .../app/core/components/Picker/UserPicker.tsx | 2 +- public/sass/components/_buttons.scss | 4 ++ public/sass/components/_gf-form.scss | 14 ++++++- yarn.lock | 25 +++++++++++++ 9 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 public/app/core/components/Animations/SlideDown.tsx diff --git a/package.json b/package.json index 80fa9a699f8..d7fb5d8e6f8 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,7 @@ "react-popper": "^0.7.5", "react-select": "^1.1.0", "react-sizeme": "^2.3.6", + "react-transition-group": "^2.2.1", "remarkable": "^1.7.1", "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^5.4.3", diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 98f63a46cbe..7c9e55bcac3 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -7,7 +7,7 @@ import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; - +import SlideDown from 'app/core/components/Animations/SlideDown'; @inject('nav', 'folder', 'view', 'permissions') @observer export class FolderPermissions extends Component { @@ -48,14 +48,18 @@ export class FolderPermissions extends Component {
-
- {permissions.isAddPermissionsVisible ? ( + - ) : null} +
diff --git a/public/app/core/components/Animations/SlideDown.tsx b/public/app/core/components/Animations/SlideDown.tsx new file mode 100644 index 00000000000..4d515f98f16 --- /dev/null +++ b/public/app/core/components/Animations/SlideDown.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import Transition from 'react-transition-group/Transition'; + +const defaultMaxHeight = '200px'; // When animating using max-height we need to use a static value. +// If this is not enough, pass in -
-
Add Permission For
+ + +
Add Permission For
diff --git a/public/app/core/components/Picker/TeamPicker.tsx b/public/app/core/components/Picker/TeamPicker.tsx index 18f7258c221..82809ee1cf6 100644 --- a/public/app/core/components/Picker/TeamPicker.tsx +++ b/public/app/core/components/Picker/TeamPicker.tsx @@ -67,11 +67,13 @@ class TeamPicker extends Component { isLoading={isLoading} loadOptions={this.debouncedSearch} loadingPlaceholder="Loading..." + noResultsText="No teams found" onChange={handlePicked} - className="width-8 gf-form-input gf-form-input--form-dropdown" + className="width-12 gf-form-input gf-form-input--form-dropdown" optionComponent={PickerOption} placeholder="Choose" value={value} + autosize={true} />
); diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 99b0ef258ca..733a8015a6c 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -68,7 +68,7 @@ class UserPicker extends Component { loadingPlaceholder="Loading..." noResultsText="No users found" onChange={handlePicked} - className="width-8 gf-form-input gf-form-input--form-dropdown" + className="width-12 gf-form-input gf-form-input--form-dropdown" optionComponent={PickerOption} placeholder="Choose" value={value} diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index 4c9b197c3d0..c21ed30b0f4 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -113,6 +113,10 @@ //border: 1px solid $tight-form-func-highlight-bg; } +.btn-transparent { + background-color: transparent; +} + .btn-outline-primary { @include button-outline-variant($btn-primary-bg); } diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index dd6c7c39b83..a1e208ee1c2 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -274,6 +274,10 @@ $input-border: 1px solid $input-border-color; } } + .gf-form-input { + margin-right: 0; + } + select.gf-form-input { text-indent: 0.01px; text-overflow: ''; @@ -394,7 +398,15 @@ select.gf-form-input ~ .gf-form-help-icon { } .cta-form { + position: relative; padding: 1rem; - background-color: $dark-2; + background-color: $dark-4; margin-bottom: 1rem; + border-top: 3px solid $green; +} + +.cta-form__close { + position: absolute; + right: 0; + top: 0; } diff --git a/yarn.lock b/yarn.lock index 5bb00023d7c..e95968736ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1604,6 +1604,10 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chain-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc" + chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0, chalk@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2801,6 +2805,10 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-helpers@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" + dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -8318,6 +8326,17 @@ react-test-renderer@^16.0.0, react-test-renderer@^16.0.0-0: object-assign "^4.1.1" prop-types "^15.6.0" +react-transition-group@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10" + dependencies: + chain-function "^1.0.0" + classnames "^2.2.5" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.8" + warning "^3.0.0" + react@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" @@ -10355,6 +10374,12 @@ walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" + watch@~0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" From 3da2ab61e0ccf63adb6c602af9e9abfed2cee18b Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 31 Jan 2018 14:36:14 +0100 Subject: [PATCH 28/84] Verifies requirement of id in dashboards. --- pkg/services/sqlstore/dashboard_test.go | 34 ++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 6e77b44f8c1..007ac4a01d5 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -276,7 +276,6 @@ func TestDashboardDataAccess(t *testing.T) { cmd := m.SaveDashboardCommand{ OrgId: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - //"id": 1, "uid": "randomHash", "title": "folderId", "style": "light", @@ -306,6 +305,39 @@ func TestDashboardDataAccess(t *testing.T) { So(err, ShouldBeNil) }) + Convey("Should not be able to update using just uid", func() { + cmd := m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": savedDash.Uid, + "title": "folderId", + "version": savedDash.Version, + "tags": []interface{}{}, + }), + FolderId: savedDash.FolderId, + } + + err := SaveDashboard(&cmd) + So(err, ShouldEqual, m.ErrDashboardWithSameUIDExists) + }) + + Convey("Should be able to update using just uid with overwrite", func() { + cmd := m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": savedDash.Uid, + "title": "folderId", + "version": savedDash.Version, + "tags": []interface{}{}, + }), + FolderId: savedDash.FolderId, + Overwrite: true, + } + + err := SaveDashboard(&cmd) + So(err, ShouldBeNil) + }) + Convey("Should be able to update dashboard and remove folderId", func() { cmd := m.SaveDashboardCommand{ OrgId: 1, From 16a1642831a521e7a5a57a3fe298f742c7e873da Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 31 Jan 2018 15:15:15 +0100 Subject: [PATCH 29/84] gofmt... --- pkg/services/sqlstore/dashboard.go | 2 -- pkg/services/sqlstore/dashboard_test.go | 10 +++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index c378d70af10..4c04fe1251a 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -20,8 +20,6 @@ func init() { bus.AddHandler("sql", GetDashboardsByPluginId) } - - func SaveDashboard(cmd *m.SaveDashboardCommand) error { return inTransaction(func(sess *DBSession) error { dash := cmd.GetDashboardModel() diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 007ac4a01d5..87010849a77 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -276,10 +276,10 @@ func TestDashboardDataAccess(t *testing.T) { cmd := m.SaveDashboardCommand{ OrgId: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": "randomHash", - "title": "folderId", - "style": "light", - "tags": []interface{}{}, + "uid": "randomHash", + "title": "folderId", + "style": "light", + "tags": []interface{}{}, }), FolderId: 2, } @@ -330,7 +330,7 @@ func TestDashboardDataAccess(t *testing.T) { "version": savedDash.Version, "tags": []interface{}{}, }), - FolderId: savedDash.FolderId, + FolderId: savedDash.FolderId, Overwrite: true, } From 1b9e02e4cc794c916a178fe19eccf1af55bd933f Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Jan 2018 16:12:51 +0100 Subject: [PATCH 30/84] tests: Update tests in PermissionsStore and rem out the Permissions-tests for now #10676 --- .../Permissions/Permissions.jest.tsx | 128 +++++++++--------- .../PermissionsStore/PermissionsStore.jest.ts | 122 +++++++---------- .../PermissionsStore/PermissionsStore.ts | 2 +- 3 files changed, 116 insertions(+), 136 deletions(-) diff --git a/public/app/core/components/Permissions/Permissions.jest.tsx b/public/app/core/components/Permissions/Permissions.jest.tsx index 0a608ee5842..893116ed725 100644 --- a/public/app/core/components/Permissions/Permissions.jest.tsx +++ b/public/app/core/components/Permissions/Permissions.jest.tsx @@ -1,73 +1,73 @@ -import React from 'react'; -import Permissions from './Permissions'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; -import { shallow } from 'enzyme'; +// import React from 'react'; +// import Permissions from './Permissions'; +// import { RootStore } from 'app/stores/RootStore/RootStore'; +// import { backendSrv } from 'test/mocks/common'; +// import { shallow } from 'enzyme'; -describe('Permissions', () => { - let wrapper; +// describe('Permissions', () => { +// let wrapper; - beforeAll(() => { - backendSrv.get.mockReturnValue( - Promise.resolve([ - { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, - { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, - { - id: 4, - dashboardId: 1, - userId: 2, - userLogin: 'danlimerick', - userEmail: 'dan.limerick@gmail.com', - permission: 4, - permissionName: 'Admin', - }, - ]) - ); +// beforeAll(() => { +// backendSrv.get.mockReturnValue( +// Promise.resolve([ +// { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, +// { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, +// { +// id: 4, +// dashboardId: 1, +// userId: 2, +// userLogin: 'danlimerick', +// userEmail: 'dan.limerick@gmail.com', +// permission: 4, +// permissionName: 'Admin', +// }, +// ]) +// ); - backendSrv.post = jest.fn(); +// backendSrv.post = jest.fn(); - const store = RootStore.create( - {}, - { - backendSrv: backendSrv, - } - ); +// const store = RootStore.create( +// {}, +// { +// backendSrv: backendSrv, +// } +// ); - wrapper = shallow(); - return wrapper.instance().loadStore(1, true); - }); +// wrapper = shallow(); +// return wrapper.instance().loadStore(1, true); +// }); - describe('when permission for a user is added', () => { - it('should save permission to db', () => { - const userItem = { - id: 2, - login: 'user2', - }; +// describe('when permission for a user is added', () => { +// it('should save permission to db', () => { +// const userItem = { +// id: 2, +// login: 'user2', +// }; - wrapper - .instance() - .userPicked(userItem) - .then(() => { - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); - }); - }); - }); +// wrapper +// .instance() +// .userPicked(userItem) +// .then(() => { +// expect(backendSrv.post.mock.calls.length).toBe(1); +// expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); +// }); +// }); +// }); - describe('when permission for team is added', () => { - it('should save permission to db', () => { - const teamItem = { - id: 2, - name: 'ug1', - }; +// describe('when permission for team is added', () => { +// it('should save permission to db', () => { +// const teamItem = { +// id: 2, +// name: 'ug1', +// }; - wrapper - .instance() - .teamPicked(teamItem) - .then(() => { - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); - }); - }); - }); -}); +// wrapper +// .instance() +// .teamPicked(teamItem) +// .then(() => { +// expect(backendSrv.post.mock.calls.length).toBe(1); +// expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); +// }); +// }); +// }); +// }); diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts index 34206ba41c9..5b59ada21d6 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts @@ -1,4 +1,4 @@ -import { PermissionsStore } from './PermissionsStore'; +import { PermissionsStore, aclTypeValues } from './PermissionsStore'; import { backendSrv } from 'test/mocks/common'; describe('PermissionsStore', () => { @@ -47,21 +47,6 @@ describe('PermissionsStore', () => { expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); }); - it('should save newly added permissions automatically', () => { - expect(store.items.length).toBe(3); - - const newItem = { - userId: 10, - userLogin: 'tester1', - permission: 1, - }; - store.addStoreItem(newItem); - - expect(store.items.length).toBe(4); - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); - }); - it('should save removed permissions automatically', () => { expect(store.items.length).toBe(3); @@ -72,6 +57,30 @@ describe('PermissionsStore', () => { expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); }); + describe('when duplicate team permissions are added', () => { + beforeEach(() => { + const newItem = { + teamId: 10, + team: 'tester-team', + permission: 1, + }; + store.resetNewType(); + store.newItem.setTeam(newItem.teamId, newItem.team); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + + store.newItem.setTeam(newItem.teamId, newItem.team); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + }); + + it('should return a validation error', () => { + expect(store.items.length).toBe(4); + expect(store.error).toBe('This permission exists already.'); + expect(backendSrv.post.mock.calls.length).toBe(1); + }); + }); + describe('when duplicate user permissions are added', () => { beforeEach(() => { const newItem = { @@ -79,8 +88,14 @@ describe('PermissionsStore', () => { userLogin: 'tester1', permission: 1, }; - store.addStoreItem(newItem); - store.addStoreItem(newItem); + store.setNewType(aclTypeValues.USER.value); + store.newItem.setUser(newItem.userId, newItem.userLogin); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + store.setNewType(aclTypeValues.USER.value); + store.newItem.setUser(newItem.userId, newItem.userLogin); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); }); it('should return a validation error', () => { @@ -90,59 +105,24 @@ describe('PermissionsStore', () => { }); }); - describe('when duplicate team permissions are added', () => { - beforeEach(() => { - const newItem = { - teamId: 1, - teamName: 'testerteam', - permission: 1, - }; - store.addStoreItem(newItem); - store.addStoreItem(newItem); - }); + // TODO: I dont get this one + // describe('when one inherited and one not inherited team permission are added', () => { + // beforeEach(() => { + // const teamItem = { + // team: 'MyTestTeam', + // dashboardId: 1, + // teamId: 1, + // permission: 2, + // }; + // store.addStoreItem(teamItem); + // }); - it('should return a validation error', () => { - expect(store.items.length).toBe(4); - expect(store.error).toBe('This permission exists already.'); - expect(backendSrv.post.mock.calls.length).toBe(1); - }); - }); + // it('should not throw a validation error', () => { + // expect(store.error).toBe(null); + // }); - describe('when duplicate role permissions are added', () => { - beforeEach(() => { - const newItem = { - team: 'MyTestTeam', - teamId: 1, - permission: 1, - }; - store.addStoreItem(newItem); - store.addStoreItem(newItem); - }); - - it('should return a validation error', () => { - expect(store.items.length).toBe(4); - expect(store.error).toBe('This permission exists already.'); - expect(backendSrv.post.mock.calls.length).toBe(1); - }); - }); - - describe('when one inherited and one not inherited team permission are added', () => { - beforeEach(() => { - const teamItem = { - team: 'MyTestTeam', - dashboardId: 1, - teamId: 1, - permission: 2, - }; - store.addStoreItem(teamItem); - }); - - it('should not throw a validation error', () => { - expect(store.error).toBe(null); - }); - - it('should add both permissions', () => { - expect(store.items.length).toBe(4); - }); - }); + // it('should add both permissions', () => { + // expect(store.items.length).toBe(4); + // }); + // }); }); diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index ae100fd0751..fe997980ea7 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -24,7 +24,7 @@ export const aclTypes = Object.keys(aclTypeValues).map(item => aclTypeValues[ite const defaultNewType = aclTypes[0].value; -const NewPermissionsItem = types +export const NewPermissionsItem = types .model('NewPermissionsItem', { type: types.optional( types.enumeration(Object.keys(aclTypeValues).map(item => aclTypeValues[item].value)), From 20052150bac78585b2c6c3284f24eaa802dd5b3c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Jan 2018 16:15:05 +0100 Subject: [PATCH 31/84] tests: Move tests from Permissions to AddPermissions #10676 --- .../Permissions/AddPermissions.jest.tsx | 79 +++++++++++++++++++ .../components/Permissions/AddPermissions.tsx | 2 +- .../app/core/components/Picker/UserPicker.tsx | 1 - 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 public/app/core/components/Permissions/AddPermissions.jest.tsx diff --git a/public/app/core/components/Permissions/AddPermissions.jest.tsx b/public/app/core/components/Permissions/AddPermissions.jest.tsx new file mode 100644 index 00000000000..a3164ec90af --- /dev/null +++ b/public/app/core/components/Permissions/AddPermissions.jest.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import AddPermissions from './AddPermissions'; +import { RootStore } from 'app/stores/RootStore/RootStore'; +import { backendSrv } from 'test/mocks/common'; +import { shallow } from 'enzyme'; + +describe('AddPermissions', () => { + let wrapper; + + beforeAll(() => { + backendSrv.get.mockReturnValue( + Promise.resolve([ + { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, + { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, + { + id: 4, + dashboardId: 1, + userId: 2, + userLogin: 'danlimerick', + userEmail: 'dan.limerick@gmail.com', + permission: 4, + permissionName: 'Admin', + }, + ]) + ); + + backendSrv.post = jest.fn(); + + const store = RootStore.create( + {}, + { + backendSrv: backendSrv, + } + ); + + // wrapper = shallow(); + wrapper = shallow(); + // + // return wrapper.instance().loadStore(1, true); + }); + + describe('when permission for a user is added', () => { + it('should save permission to db', async () => { + const evt = { + target: { + value: 'User', + }, + }; + const userItem = { + id: 2, + login: 'user2', + }; + + const instance = wrapper.instance(); + instance.typeChanged(evt); + instance.userPicked(userItem); + wrapper.find('[data-save-permission]').simulate('click'); + expect(backendSrv.post.mock.calls.length).toBe(1); + expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); + }); + }); + + // describe('when permission for team is added', () => { + // it('should save permission to db', () => { + // const teamItem = { + // id: 2, + // name: 'ug1', + // }; + + // wrapper + // .instance() + // .teamPicked(teamItem) + // .then(() => { + // expect(backendSrv.post.mock.calls.length).toBe(1); + // expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); + // }); + // }); + // }); +}); diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx index 989da32e187..fa2e69f17b3 100644 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -124,7 +124,7 @@ class AddPermissions extends Component {
-
diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 733a8015a6c..129c012692a 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -55,7 +55,6 @@ class UserPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; const { isLoading, handlePicked, value } = this.props; - console.log('value', value); return (
Date: Wed, 31 Jan 2018 16:44:14 +0100 Subject: [PATCH 32/84] ux: Add an optional className to the UserPicker and TeamPicker #10676 --- .../core/components/Permissions/AddPermissions.tsx | 14 ++++++++++++-- public/app/core/components/Picker/TeamPicker.tsx | 5 +++-- public/app/core/components/Picker/UserPicker.tsx | 5 +++-- public/app/core/components/Picker/withPicker.tsx | 1 + 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx index fa2e69f17b3..5a5c9b83137 100644 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -103,13 +103,23 @@ class AddPermissions extends Component { {newItem.type === 'User' ? (
- +
) : null} {newItem.type === 'Group' ? (
- +
) : null} diff --git a/public/app/core/components/Picker/TeamPicker.tsx b/public/app/core/components/Picker/TeamPicker.tsx index 82809ee1cf6..2dfff1850dd 100644 --- a/public/app/core/components/Picker/TeamPicker.tsx +++ b/public/app/core/components/Picker/TeamPicker.tsx @@ -10,6 +10,7 @@ export interface IProps { toggleLoading: any; handlePicked: (user) => void; value?: string; + className?: string; } export interface Team { @@ -55,7 +56,7 @@ class TeamPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked, value } = this.props; + const { isLoading, handlePicked, value, className } = this.props; return (
@@ -69,7 +70,7 @@ class TeamPicker extends Component { loadingPlaceholder="Loading..." noResultsText="No teams found" onChange={handlePicked} - className="width-12 gf-form-input gf-form-input--form-dropdown" + className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={PickerOption} placeholder="Choose" value={value} diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 129c012692a..5c36505aeaa 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -10,6 +10,7 @@ export interface IProps { toggleLoading: any; handlePicked: (user) => void; value?: string; + className?: string; } export interface User { @@ -54,7 +55,7 @@ class UserPicker extends Component { render() { const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked, value } = this.props; + const { isLoading, handlePicked, value, className } = this.props; return (
{ loadingPlaceholder="Loading..." noResultsText="No users found" onChange={handlePicked} - className="width-12 gf-form-input gf-form-input--form-dropdown" + className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={PickerOption} placeholder="Choose" value={value} diff --git a/public/app/core/components/Picker/withPicker.tsx b/public/app/core/components/Picker/withPicker.tsx index cf3954850b2..838ef927c30 100644 --- a/public/app/core/components/Picker/withPicker.tsx +++ b/public/app/core/components/Picker/withPicker.tsx @@ -4,6 +4,7 @@ export interface IProps { backendSrv: any; handlePicked: (data) => void; value?: string; + className?: string; } export default function withPicker(WrappedComponent) { From 780c7f8775b0eafb924740367b31acb61f5e227c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Jan 2018 16:48:45 +0100 Subject: [PATCH 33/84] tests: Add TeamPicker test and update TeamPicker/UserPicker snapshots so they match the latest classNames update #10676 --- .../components/Picker/TeamPicker.jest.tsx | 19 ++++ .../__snapshots__/TeamPicker.jest.tsx.snap | 98 +++++++++++++++++++ .../__snapshots__/UserPicker.jest.tsx.snap | 2 +- 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 public/app/core/components/Picker/TeamPicker.jest.tsx create mode 100644 public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap diff --git a/public/app/core/components/Picker/TeamPicker.jest.tsx b/public/app/core/components/Picker/TeamPicker.jest.tsx new file mode 100644 index 00000000000..20b7620e0ac --- /dev/null +++ b/public/app/core/components/Picker/TeamPicker.jest.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import TeamPicker from './TeamPicker'; + +const model = { + backendSrv: { + get: () => { + return new Promise((resolve, reject) => {}); + }, + }, + handlePicked: () => {}, +}; + +describe('TeamPicker', () => { + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap new file mode 100644 index 00000000000..67232d0ea5b --- /dev/null +++ b/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TeamPicker renders correctly 1`] = ` +
+
+
+ +
+ Loading... +
+
+ +
+ +
+
+
+
+
+
+`; diff --git a/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap index a1563ba8bc3..3262dc10efe 100644 --- a/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap +++ b/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap @@ -5,7 +5,7 @@ exports[`UserPicker renders correctly 1`] = ` className="user-picker" >
Date: Wed, 31 Jan 2018 17:01:04 +0100 Subject: [PATCH 34/84] ux: Change input width of UserPicker and TeamPicker in AddPermissions component #10676 --- public/app/core/components/Permissions/AddPermissions.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx index 5a5c9b83137..92656190bf2 100644 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -78,6 +78,7 @@ class AddPermissions extends Component { render() { const { permissions, backendSrv } = this.props; const newItem = permissions.newItem; + const pickerClassName = 'width-20'; return (
@@ -107,7 +108,7 @@ class AddPermissions extends Component { backendSrv={backendSrv} handlePicked={this.userPicked} value={newItem.userId} - className="width-8" + className={pickerClassName} />
) : null} @@ -118,7 +119,7 @@ class AddPermissions extends Component { backendSrv={backendSrv} handlePicked={this.teamPicked} value={newItem.teamId} - className="width-8" + className={pickerClassName} />
) : null} From 75cf9ae27de2887b8e0149814463e0b9053e0a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 31 Jan 2018 17:26:35 +0100 Subject: [PATCH 35/84] fix: use replace when redirecting to new url --- public/app/routes/dashboard_loaders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index 971c83f0414..4ad77512ad1 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -22,7 +22,7 @@ export class LoadDashboardCtrl { if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) { backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => { if (res) { - $location.path(res.meta.url); + $location.path(res.meta.url).replace(); } }); return; From 58cfb236250e0d5b3bac72c1d261abcfb5572435 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 31 Jan 2018 17:27:28 +0100 Subject: [PATCH 36/84] retry uid generation --- pkg/middleware/dashboard_redirect_test.go | 2 + pkg/models/dashboards.go | 6 +-- pkg/services/sqlstore/dashboard.go | 62 ++++++++++++++++------- pkg/services/sqlstore/dashboard_test.go | 28 +++++++++- 4 files changed, 74 insertions(+), 24 deletions(-) diff --git a/pkg/middleware/dashboard_redirect_test.go b/pkg/middleware/dashboard_redirect_test.go index bff4ee2253c..21fc12e5e84 100644 --- a/pkg/middleware/dashboard_redirect_test.go +++ b/pkg/middleware/dashboard_redirect_test.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" . "github.com/smartystreets/goconvey/convey" ) @@ -19,6 +20,7 @@ func TestMiddlewareDashboardRedirect(t *testing.T) { fakeDash.Id = 1 fakeDash.FolderId = 1 fakeDash.HasAcl = false + fakeDash.Uid = util.GenerateShortUid() bus.AddHandler("test", func(query *m.GetDashboardQuery) error { query.Result = fakeDash diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index c4361eefd7d..e06bc0d1f0d 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -9,7 +9,6 @@ import ( "github.com/gosimple/slug" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" ) // Typed errors @@ -23,6 +22,7 @@ var ( 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") + ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard uid.") ) type UpdatePluginDashboardError struct { @@ -66,7 +66,6 @@ type Dashboard struct { // NewDashboard creates a new dashboard func NewDashboard(title string) *Dashboard { dash := &Dashboard{} - dash.Uid = util.GenerateShortUid() dash.Data = simplejson.New() dash.Data.Set("title", title) dash.Title = title @@ -115,9 +114,6 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { if uid, err := dash.Data.Get("uid").String(); err == nil { dash.Uid = uid - } else { - dash.Uid = util.GenerateShortUid() - dash.Data.Set("uid", dash.Uid) } return dash diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 4c04fe1251a..f93e7f402ae 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/util" ) func init() { @@ -20,6 +21,8 @@ func init() { bus.AddHandler("sql", GetDashboardsByPluginId) } +var generateNewUid func() string = util.GenerateShortUid + func SaveDashboard(cmd *m.SaveDashboardCommand) error { return inTransaction(func(sess *DBSession) error { dash := cmd.GetDashboardModel() @@ -27,7 +30,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { // try get existing dashboard var existing m.Dashboard - if dash.Id > 0 { + if dash.Id != 0 { dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) if err != nil { return err @@ -49,27 +52,36 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { if existing.PluginId != "" && cmd.Overwrite == false { return m.UpdatePluginDashboardError{PluginId: existing.PluginId} } - } + } else if dash.Uid != "" { + var sameUid m.Dashboard + sameUidExists, err := sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&sameUid) + if err != nil { + return err + } - var sameUid m.Dashboard - sameUidExists, err := sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&sameUid) - if err != nil { - return err - } - - if sameUidExists { - // another dashboard with same uid - if dash.Id != sameUid.Id { - if cmd.Overwrite { - dash.Id = sameUid.Id - dash.Version = sameUid.Version - } else { - return m.ErrDashboardWithSameUIDExists + if sameUidExists { + // another dashboard with same uid + if dash.Id != sameUid.Id { + if cmd.Overwrite { + dash.Id = sameUid.Id + dash.Version = sameUid.Version + } else { + return m.ErrDashboardWithSameUIDExists + } } } } - err = guaranteeDashboardNameIsUniqueInFolder(sess, dash) + if dash.Uid == "" { + uid, err := generateNewDashboardUid(sess, dash.OrgId) + if err != nil { + return err + } + dash.Uid = uid + dash.Data.Set("uid", uid) + } + + err := guaranteeDashboardNameIsUniqueInFolder(sess, dash) if err != nil { return err } @@ -144,6 +156,22 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { return err }) } +func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { + for i := 0; i < 3; i++ { + uid := generateNewUid() + + exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&m.Dashboard{}) + if err != nil { + return "", err + } + + if !exists { + return uid, nil + } + } + + return "", m.ErrDashboardFailedGenerateUniqueUid +} func guaranteeDashboardNameIsUniqueInFolder(sess *DBSession, dash *m.Dashboard) error { var sameNameInFolder m.Dashboard diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 87010849a77..6e240b9585e 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -5,12 +5,12 @@ import ( "testing" "github.com/go-xorm/xorm" - . "github.com/smartystreets/goconvey/convey" - "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" + . "github.com/smartystreets/goconvey/convey" ) func TestDashboardDataAccess(t *testing.T) { @@ -338,6 +338,30 @@ func TestDashboardDataAccess(t *testing.T) { So(err, ShouldBeNil) }) + Convey("Should retry generation of uid once if it fails.", func() { + timesCalled := 0 + generateNewUid = func() string { + timesCalled += 1 + if timesCalled <= 2 { + return savedDash.Uid + } else { + return util.GenerateShortUid() + } + } + cmd := m.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": "new dash 12334", + "tags": []interface{}{}, + }), + } + + err := SaveDashboard(&cmd) + So(err, ShouldBeNil) + + generateNewUid = util.GenerateShortUid + }) + Convey("Should be able to update dashboard and remove folderId", func() { cmd := m.SaveDashboardCommand{ OrgId: 1, From 95d063621e29562b20d92589bb375363dd8c332d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 16:46:31 +0100 Subject: [PATCH 37/84] dashboards: new route for deleting dashboards by uid --- pkg/api/api.go | 1 + pkg/api/dashboard.go | 20 ++++++++++ pkg/api/dashboard_test.go | 81 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/pkg/api/api.go b/pkg/api/api.go index 6d2207971e7..d10b459cd6a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -249,6 +249,7 @@ func (hs *HttpServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) { dashboardRoute.Get("/uid/:uid", wrap(GetDashboard)) + dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid)) dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 7799ad868f9..ae8d819b981 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -165,6 +165,26 @@ func DeleteDashboard(c *middleware.Context) Response { return Json(200, resp) } +func DeleteDashboardByUid(c *middleware.Context) Response { + dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid")) + if rsp != nil { + return rsp + } + + guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser) + if canSave, err := guardian.CanSave(); err != nil || !canSave { + return dashboardGuardianResponse(err) + } + + cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id} + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to delete dashboard", err) + } + + var resp = map[string]interface{}{"title": dash.Title} + return Json(200, resp) +} + func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { cmd.OrgId = c.OrgId cmd.UserId = c.UserId diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 0bd586f6067..3d8f49b5f23 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -113,6 +113,15 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + CallDeleteDashboardByUid(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { CallGetDashboardVersion(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -169,6 +178,15 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + CallDeleteDashboardByUid(sc) + So(sc.resp.Code, ShouldEqual, 200) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { CallGetDashboardVersion(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -287,6 +305,15 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + CallDeleteDashboardByUid(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { CallGetDashboardVersion(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -341,6 +368,15 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + CallDeleteDashboardByUid(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { CallGetDashboardVersion(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -406,6 +442,15 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + CallDeleteDashboardByUid(sc) + So(sc.resp.Code, ShouldEqual, 200) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { CallGetDashboardVersion(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -470,6 +515,15 @@ func TestDashboardApiEndpoint(t *testing.T) { So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") }) }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + CallDeleteDashboardByUid(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + }) }) Convey("When user is an Org Viewer but has an admin permission", func() { @@ -521,6 +575,15 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + CallDeleteDashboardByUid(sc) + So(sc.resp.Code, ShouldEqual, 200) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { CallGetDashboardVersion(sc) So(sc.resp.Code, ShouldEqual, 200) @@ -583,6 +646,15 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + CallDeleteDashboardByUid(sc) + So(sc.resp.Code, ShouldEqual, 403) + + Convey("Should lookup dashboard by uid", func() { + So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + }) + }) + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { CallGetDashboardVersion(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -643,6 +715,15 @@ func CallDeleteDashboard(sc *scenarioContext) { sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } +func CallDeleteDashboardByUid(sc *scenarioContext) { + bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error { + return nil + }) + + sc.handlerFunc = DeleteDashboardByUid + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() +} + func CallPostDashboard(sc *scenarioContext) { bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error { return nil From b23560ed5a33e76eda5292cff7b231a9ca3d98b3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 16:51:06 +0100 Subject: [PATCH 38/84] dashboards: add validation to delete dashboard by slug Validates that there are only one folder/dashboard having that slug, otherwise returns 412 Precondition Failed --- pkg/api/dashboard.go | 10 +++++++ pkg/api/dashboard_test.go | 43 ++++++++++++++++++++++++++++++ pkg/models/dashboards.go | 10 ++++++- pkg/services/sqlstore/dashboard.go | 11 ++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index ae8d819b981..7b3977df0d8 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -146,6 +146,16 @@ func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dash } func DeleteDashboard(c *middleware.Context) Response { + query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")} + + if err := bus.Dispatch(&query); err != nil { + return ApiError(500, "Failed to retrieve dashboards by slug", err) + } + + if len(query.Result) > 1 { + return Json(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()}) + } + dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "") if rsp != nil { return rsp diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 3d8f49b5f23..5ff9efc84e6 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -39,6 +39,12 @@ func TestDashboardApiEndpoint(t *testing.T) { fakeDash.FolderId = 1 fakeDash.HasAcl = false + bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error { + dashboards := []*m.Dashboard{fakeDash} + query.Result = dashboards + return nil + }) + var getDashboardQueries []*m.GetDashboardQuery bus.AddHandler("test", func(query *m.GetDashboardQuery) error { @@ -232,6 +238,12 @@ func TestDashboardApiEndpoint(t *testing.T) { fakeDash.HasAcl = true setting.ViewersCanEdit = false + bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error { + dashboards := []*m.Dashboard{fakeDash} + query.Result = dashboards + return nil + }) + aclMockResp := []*m.DashboardAclInfoDTO{ { DashboardId: 1, @@ -671,6 +683,37 @@ func TestDashboardApiEndpoint(t *testing.T) { }) }) }) + + Convey("Given two dashboards with the same title in different folders", t, func() { + dashOne := m.NewDashboard("dash") + dashOne.Id = 2 + dashOne.FolderId = 1 + dashOne.HasAcl = false + + dashTwo := m.NewDashboard("dash") + dashTwo.Id = 4 + dashTwo.FolderId = 3 + dashTwo.HasAcl = false + + bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error { + dashboards := []*m.Dashboard{dashOne, dashTwo} + query.Result = dashboards + return nil + }) + + role := m.ROLE_EDITOR + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + CallDeleteDashboard(sc) + + Convey("Should result in 412 Precondition failed", func() { + So(sc.resp.Code, ShouldEqual, 412) + result := sc.ToJson() + So(result.Get("status").MustString(), ShouldEqual, "multiple-slugs-exists") + So(result.Get("message").MustString(), ShouldEqual, m.ErrDashboardsWithSameSlugExists.Error()) + }) + }) + }) } func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 84ede04f226..dc296d2b3bd 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -22,6 +22,7 @@ var ( 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") ) type UpdatePluginDashboardError struct { @@ -165,7 +166,7 @@ func GetDashboardUrl(uid string, slug string) string { // GetFolderUrl return the html url for a folder func GetFolderUrl(folderUid string, slug string) string { - return fmt.Sprintf("%s/f/%v/%s", setting.AppSubUrl, folderUid, slug) + return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug) } // @@ -231,3 +232,10 @@ type GetDashboardSlugByIdQuery struct { Id int64 Result string } + +type GetDashboardsBySlugQuery struct { + OrgId int64 + Slug string + + Result []*Dashboard +} diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 84cfd1999c7..43e9d03e649 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -386,3 +386,14 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error { query.Result = slug.Slug return nil } + +func GetDashboardsBySlug(query *m.GetDashboardsBySlugQuery) error { + var dashboards = make([]*m.Dashboard, 0) + + if err := x.Where("org_id=? AND slug=?", query.OrgId, query.Slug).Find(&dashboards); err != nil { + return err + } + + query.Result = dashboards + return nil +} From 92a0171a9bf2ca42ebf7c3c2201a5199ee1e8060 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 17:15:00 +0100 Subject: [PATCH 39/84] folders: change the front end route for browsing folders Change the front end route for folders to /dashboards/f//. Use new route for deleting dashboard/folder by uid. Retrieve dashboard/folder by uid when moving or deleting dashboards/folders. --- pkg/services/search/models.go | 1 + pkg/services/sqlstore/dashboard.go | 1 + pkg/services/sqlstore/dashboard_test.go | 2 +- .../manage_dashboards/manage_dashboards.ts | 6 ++--- public/app/core/services/backend_srv.ts | 22 +++++++++---------- public/app/core/services/search_srv.ts | 6 +++-- .../app/core/specs/manage_dashboards.jest.ts | 16 +++++++------- .../features/dashboard/create_folder_ctrl.ts | 4 +--- .../dashboard/folder_dashboards_ctrl.ts | 9 ++++---- .../features/dashboard/folder_page_loader.ts | 13 +++++------ .../dashboard/folder_permissions_ctrl.ts | 7 +++--- .../dashboard/folder_settings_ctrl.ts | 16 +++++++------- .../features/dashboard/settings/settings.ts | 2 +- public/app/routes/routes.ts | 6 ++--- 14 files changed, 56 insertions(+), 55 deletions(-) diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index 6214a854db7..c543befc454 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -13,6 +13,7 @@ const ( type Hit struct { Id int64 `json:"id"` + Uid string `json:"uid"` Title string `json:"title"` Uri string `json:"uri"` Url string `json:"url"` diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 43e9d03e649..a23f07cd895 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -266,6 +266,7 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard } hit = &search.Hit{ Id: item.Id, + Uid: item.Uid, Title: item.Title, Uri: "db/" + item.Slug, Url: url, diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 19e82614718..55b27b1e56a 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -146,7 +146,7 @@ func TestDashboardDataAccess(t *testing.T) { So(len(query.Result), ShouldEqual, 1) hit := query.Result[0] So(hit.Type, ShouldEqual, search.DashHitFolder) - So(hit.Url, ShouldEqual, fmt.Sprintf("/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) + So(hit.Url, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) }) Convey("Should be able to search for a dashboard folder's children", func() { diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index 011e020f588..af87c86a671 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -91,10 +91,10 @@ export class ManageDashboardsCtrl { for (const section of this.sections) { if (section.checked && section.id !== 0) { - selectedDashboards.folders.push(section.slug); + selectedDashboards.folders.push(section.uid); } else { const selected = _.filter(section.items, { checked: true }); - selectedDashboards.dashboards.push(..._.map(selected, 'slug')); + selectedDashboards.dashboards.push(..._.map(selected, 'uid')); } } @@ -185,7 +185,7 @@ export class ManageDashboardsCtrl { for (const section of this.sections) { const selected = _.filter(section.items, { checked: true }); - selectedDashboards.push(..._.map(selected, 'slug')); + selectedDashboards.push(..._.map(selected, 'uid')); } return selectedDashboards; diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 9ade4264bb3..b6791267d4f 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -257,11 +257,11 @@ export class BackendSrv { }); } - deleteDashboard(slug) { + deleteDashboard(uid) { let deferred = this.$q.defer(); - this.getDashboard('db', slug).then(fullDash => { - this.delete(`/api/dashboards/db/${slug}`) + this.getDashboardByUid(uid).then(fullDash => { + this.delete(`/api/dashboards/uid/${uid}`) .then(() => { deferred.resolve(fullDash); }) @@ -273,21 +273,21 @@ export class BackendSrv { return deferred.promise; } - deleteDashboards(dashboardSlugs) { + deleteDashboards(dashboardUids) { const tasks = []; - for (let slug of dashboardSlugs) { - tasks.push(this.createTask(this.deleteDashboard.bind(this), true, slug)); + for (let uid of dashboardUids) { + tasks.push(this.createTask(this.deleteDashboard.bind(this), true, uid)); } return this.executeInOrder(tasks, []); } - moveDashboards(dashboardSlugs, toFolder) { + moveDashboards(dashboardUids, toFolder) { const tasks = []; - for (let slug of dashboardSlugs) { - tasks.push(this.createTask(this.moveDashboard.bind(this), true, slug, toFolder)); + for (let uid of dashboardUids) { + tasks.push(this.createTask(this.moveDashboard.bind(this), true, uid, toFolder)); } return this.executeInOrder(tasks, []).then(result => { @@ -299,10 +299,10 @@ export class BackendSrv { }); } - private moveDashboard(slug, toFolder) { + private moveDashboard(uid, toFolder) { let deferred = this.$q.defer(); - this.getDashboard('db', slug).then(fullDash => { + this.getDashboardByUid(uid).then(fullDash => { const model = new DashboardModel(fullDash.dashboard, fullDash.meta); if ((!fullDash.meta.folderId && toFolder.id === 0) || fullDash.meta.folderId === toFolder.id) { diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index a0989e74aed..9092b30fd4b 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -128,11 +128,12 @@ export class SearchSrv { if (hit.type === 'dash-folder') { sections[hit.id] = { id: hit.id, + uid: hit.uid, title: hit.title, expanded: false, items: [], toggle: this.toggleFolder.bind(this), - url: `dashboards/folder/${hit.id}/${hit.slug}`, + url: hit.url, slug: hit.slug, icon: 'fa fa-folder', score: _.keys(sections).length, @@ -150,8 +151,9 @@ export class SearchSrv { if (hit.folderId) { section = { id: hit.folderId, + uid: hit.uid, title: hit.folderTitle, - url: `dashboards/folder/${hit.folderId}/${hit.folderSlug}`, + url: hit.url, slug: hit.slug, items: [], icon: 'fa fa-folder-open', diff --git a/public/app/core/specs/manage_dashboards.jest.ts b/public/app/core/specs/manage_dashboards.jest.ts index bd257c32f9b..ab67271ea8b 100644 --- a/public/app/core/specs/manage_dashboards.jest.ts +++ b/public/app/core/specs/manage_dashboards.jest.ts @@ -483,22 +483,22 @@ describe('ManageDashboards', () => { ctrl.sections = [ { id: 1, + uid: 'folder', title: 'folder', - items: [{ id: 2, checked: true, slug: 'folder-dash' }], + items: [{ id: 2, checked: true, uid: 'folder-dash' }], checked: true, - slug: 'folder', }, { id: 3, title: 'folder-2', - items: [{ id: 3, checked: true, slug: 'folder-2-dash' }], + items: [{ id: 3, checked: true, uid: 'folder-2-dash' }], checked: false, - slug: 'folder-2', + uid: 'folder-2', }, { id: 0, title: 'Root', - items: [{ id: 3, checked: true, slug: 'root-dash' }], + items: [{ id: 3, checked: true, uid: 'root-dash' }], checked: true, }, ]; @@ -535,14 +535,14 @@ describe('ManageDashboards', () => { { id: 1, title: 'folder', - items: [{ id: 2, checked: true, slug: 'dash' }], + items: [{ id: 2, checked: true, uid: 'dash' }], checked: false, - slug: 'folder', + uid: 'folder', }, { id: 0, title: 'Root', - items: [{ id: 3, checked: true, slug: 'dash-2' }], + items: [{ id: 3, checked: true, uid: 'dash-2' }], checked: false, }, ]; diff --git a/public/app/features/dashboard/create_folder_ctrl.ts b/public/app/features/dashboard/create_folder_ctrl.ts index 414054232c2..f3d9167278b 100644 --- a/public/app/features/dashboard/create_folder_ctrl.ts +++ b/public/app/features/dashboard/create_folder_ctrl.ts @@ -19,9 +19,7 @@ export class CreateFolderCtrl { return this.backendSrv.createDashboardFolder(this.title).then(result => { appEvents.emit('alert-success', ['Folder Created', 'OK']); - - var folderUrl = `dashboards/folder/${result.dashboard.id}/${result.meta.slug}`; - this.$location.url(folderUrl); + this.$location.url(result.meta.url); }); } diff --git a/public/app/features/dashboard/folder_dashboards_ctrl.ts b/public/app/features/dashboard/folder_dashboards_ctrl.ts index 9748ca889d1..5eb79c82f7e 100644 --- a/public/app/features/dashboard/folder_dashboards_ctrl.ts +++ b/public/app/features/dashboard/folder_dashboards_ctrl.ts @@ -3,15 +3,16 @@ import { FolderPageLoader } from './folder_page_loader'; export class FolderDashboardsCtrl { navModel: any; folderId: number; + uid: string; /** @ngInject */ constructor(private backendSrv, navModelSrv, private $routeParams) { - if (this.$routeParams.folderId && this.$routeParams.slug) { - this.folderId = $routeParams.folderId; + if (this.$routeParams.uid) { + this.uid = $routeParams.uid; - const loader = new FolderPageLoader(this.backendSrv, this.$routeParams); + const loader = new FolderPageLoader(this.backendSrv); - loader.load(this, this.folderId, 'manage-folder-dashboards'); + loader.load(this, this.uid, 'manage-folder-dashboards'); } } } diff --git a/public/app/features/dashboard/folder_page_loader.ts b/public/app/features/dashboard/folder_page_loader.ts index c2ec057ab8d..178b8f903dd 100755 --- a/public/app/features/dashboard/folder_page_loader.ts +++ b/public/app/features/dashboard/folder_page_loader.ts @@ -1,9 +1,9 @@ import _ from 'lodash'; export class FolderPageLoader { - constructor(private backendSrv, private $routeParams) {} + constructor(private backendSrv) {} - load(ctrl, folderId, activeChildId) { + load(ctrl, uid, activeChildId) { ctrl.navModel = { main: { icon: 'fa fa-folder-open', @@ -38,12 +38,13 @@ export class FolderPageLoader { }, }; - return this.backendSrv.getDashboard('db', this.$routeParams.slug).then(result => { + return this.backendSrv.getDashboardByUid(uid).then(result => { + ctrl.folderId = result.dashboard.id; const folderTitle = result.dashboard.title; ctrl.navModel.main.text = ''; ctrl.navModel.main.breadcrumbs = [{ title: 'Dashboards', url: 'dashboards' }, { title: folderTitle }]; - const folderUrl = this.createFolderUrl(folderId, result.meta.type, result.meta.slug); + const folderUrl = result.meta.url; const dashTab = _.find(ctrl.navModel.main.children, { id: 'manage-folder-dashboards', @@ -63,8 +64,4 @@ export class FolderPageLoader { return result; }); } - - createFolderUrl(folderId: number, type: string, slug: string) { - return `dashboards/folder/${folderId}/${slug}`; - } } diff --git a/public/app/features/dashboard/folder_permissions_ctrl.ts b/public/app/features/dashboard/folder_permissions_ctrl.ts index fc275046442..7a5e4c6af7d 100644 --- a/public/app/features/dashboard/folder_permissions_ctrl.ts +++ b/public/app/features/dashboard/folder_permissions_ctrl.ts @@ -3,13 +3,14 @@ import { FolderPageLoader } from './folder_page_loader'; export class FolderPermissionsCtrl { navModel: any; folderId: number; + uid: string; /** @ngInject */ constructor(private backendSrv, navModelSrv, private $routeParams) { - if (this.$routeParams.folderId && this.$routeParams.slug) { - this.folderId = $routeParams.folderId; + if (this.$routeParams.uid) { + this.uid = $routeParams.uid; - new FolderPageLoader(this.backendSrv, this.$routeParams).load(this, this.folderId, 'manage-folder-permissions'); + new FolderPageLoader(this.backendSrv).load(this, this.uid, 'manage-folder-permissions'); } } } diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts index 88abc5f9874..4f05ef29a42 100644 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ b/public/app/features/dashboard/folder_settings_ctrl.ts @@ -5,6 +5,7 @@ export class FolderSettingsCtrl { folderPageLoader: FolderPageLoader; navModel: any; folderId: number; + uid: string; canSave = false; dashboard: any; meta: any; @@ -13,11 +14,11 @@ export class FolderSettingsCtrl { /** @ngInject */ constructor(private backendSrv, navModelSrv, private $routeParams, private $location) { - if (this.$routeParams.folderId && this.$routeParams.slug) { - this.folderId = $routeParams.folderId; + if (this.$routeParams.uid) { + this.uid = $routeParams.uid; - this.folderPageLoader = new FolderPageLoader(this.backendSrv, this.$routeParams); - this.folderPageLoader.load(this, this.folderId, 'manage-folder-settings').then(result => { + this.folderPageLoader = new FolderPageLoader(this.backendSrv); + this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(result => { this.dashboard = result.dashboard; this.meta = result.meta; this.canSave = result.meta.canSave; @@ -38,9 +39,8 @@ export class FolderSettingsCtrl { return this.backendSrv .saveDashboard(this.dashboard, { overwrite: false }) .then(result => { - var folderUrl = this.folderPageLoader.createFolderUrl(this.folderId, this.meta.type, result.slug); - if (folderUrl !== this.$location.path()) { - this.$location.url(folderUrl + '/settings'); + if (result.url !== this.$location.path()) { + this.$location.url(result.url + '/settings'); } appEvents.emit('dashboard-saved'); @@ -65,7 +65,7 @@ export class FolderSettingsCtrl { icon: 'fa-trash', yesText: 'Delete', onConfirm: () => { - return this.backendSrv.deleteDashboard(this.meta.slug).then(() => { + return this.backendSrv.deleteDashboard(this.dashboard.uid).then(() => { appEvents.emit('alert-success', ['Folder Deleted', `${this.dashboard.title} has been deleted`]); this.$location.url('dashboards'); }); diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 4f678713ffc..b6d7c0b7c50 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -174,7 +174,7 @@ export class SettingsCtrl { } deleteDashboardConfirmed() { - this.backendSrv.deleteDashboard(this.dashboard.meta.slug).then(() => { + this.backendSrv.deleteDashboard(this.dashboard.uid).then(() => { appEvents.emit('alert-success', ['Dashboard Deleted', this.dashboard.title + ' has been deleted']); this.$location.url('/'); }); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 865f4ce1682..e183c341f5e 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -74,17 +74,17 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controller: 'CreateFolderCtrl', controllerAs: 'ctrl', }) - .when('/dashboards/folder/:folderId/:slug/permissions', { + .when('/dashboards/f/:uid/:slug/permissions', { templateUrl: 'public/app/features/dashboard/partials/folder_permissions.html', controller: 'FolderPermissionsCtrl', controllerAs: 'ctrl', }) - .when('/dashboards/folder/:folderId/:slug/settings', { + .when('/dashboards/f/:uid/:slug/settings', { templateUrl: 'public/app/features/dashboard/partials/folder_settings.html', controller: 'FolderSettingsCtrl', controllerAs: 'ctrl', }) - .when('/dashboards/folder/:folderId/:slug', { + .when('/dashboards/f/:uid/:slug', { templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html', controller: 'FolderDashboardsCtrl', controllerAs: 'ctrl', From 035b724725728c870e3898a1236993ee221b4d20 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 17:25:14 +0100 Subject: [PATCH 40/84] dashboards: remove slug property in dashboard search responses Removes slug property in dashboard search responses since this property isn't needed anymore and it haven't been released to any stable release. --- pkg/services/search/models.go | 1 - pkg/services/sqlstore/dashboard.go | 1 - public/app/core/services/search_srv.ts | 2 -- 3 files changed, 4 deletions(-) diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index c543befc454..f5d87b1f5c8 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -17,7 +17,6 @@ type Hit struct { Title string `json:"title"` Uri string `json:"uri"` Url string `json:"url"` - Slug string `json:"slug"` Type HitType `json:"type"` Tags []string `json:"tags"` IsStarred bool `json:"isStarred"` diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index a23f07cd895..c59b9574b74 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -270,7 +270,6 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard Title: item.Title, Uri: "db/" + item.Slug, Url: url, - Slug: item.Slug, Type: getHitType(item), FolderId: item.FolderId, FolderTitle: item.FolderTitle, diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index 9092b30fd4b..cb9862f8cce 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -134,7 +134,6 @@ export class SearchSrv { items: [], toggle: this.toggleFolder.bind(this), url: hit.url, - slug: hit.slug, icon: 'fa fa-folder', score: _.keys(sections).length, }; @@ -154,7 +153,6 @@ export class SearchSrv { uid: hit.uid, title: hit.folderTitle, url: hit.url, - slug: hit.slug, items: [], icon: 'fa fa-folder-open', toggle: this.toggleFolder.bind(this), From 8fc648ac4373dde512bb4943906a7cf1154cf59a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 18:10:28 +0100 Subject: [PATCH 41/84] dashboards: fix updating folder so that correct url is returned --- public/app/core/services/backend_srv.ts | 11 +++++++++++ public/app/features/dashboard/folder_settings_ctrl.ts | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index b6791267d4f..6d44967ef39 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -257,6 +257,17 @@ export class BackendSrv { }); } + updateDashboardFolder(dash, options) { + options = options || {}; + + return this.post('/api/dashboards/db/', { + dashboard: dash, + isFolder: true, + overwrite: options.overwrite === true, + message: options.message || '', + }); + } + deleteDashboard(uid) { let deferred = this.$q.defer(); diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts index 4f05ef29a42..ddc35166cad 100644 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ b/public/app/features/dashboard/folder_settings_ctrl.ts @@ -37,7 +37,7 @@ export class FolderSettingsCtrl { this.dashboard.title = this.title.trim(); return this.backendSrv - .saveDashboard(this.dashboard, { overwrite: false }) + .updateDashboardFolder(this.dashboard, { overwrite: false }) .then(result => { if (result.url !== this.$location.path()) { this.$location.url(result.url + '/settings'); @@ -84,7 +84,7 @@ export class FolderSettingsCtrl { yesText: 'Save & Overwrite', icon: 'fa-warning', onConfirm: () => { - this.backendSrv.saveDashboard(this.dashboard, { overwrite: true }); + this.backendSrv.updateDashboardFolder(this.dashboard, { overwrite: true }); }, }); } From c0c3f17d841570799d41c4811a6ba17607211c8e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 18:16:45 +0100 Subject: [PATCH 42/84] dashboards: when restoring a dashboard to an older version, set current uid --- pkg/api/dashboard.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 7b3977df0d8..f8cdc1324f8 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -473,6 +473,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard saveCmd.UserId = c.UserId saveCmd.Dashboard = version.Data saveCmd.Dashboard.Set("version", dash.Version) + saveCmd.Dashboard.Set("uid", dash.Uid) saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) return PostDashboard(c, saveCmd) From 40c83e3e2224bf24311e4fd688bfa50fc4c6e97c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 18:24:47 +0100 Subject: [PATCH 43/84] dashboards: update dashboard/folder url if browser url is not the same as from backend --- .../app/features/dashboard/folder_dashboards_ctrl.ts | 8 ++++++-- .../features/dashboard/folder_permissions_ctrl.ts | 8 ++++++-- .../app/features/dashboard/folder_settings_ctrl.ts | 12 ++++++++---- public/app/routes/dashboard_loaders.ts | 4 ++++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard/folder_dashboards_ctrl.ts b/public/app/features/dashboard/folder_dashboards_ctrl.ts index 5eb79c82f7e..dd0bf7772d8 100644 --- a/public/app/features/dashboard/folder_dashboards_ctrl.ts +++ b/public/app/features/dashboard/folder_dashboards_ctrl.ts @@ -6,13 +6,17 @@ export class FolderDashboardsCtrl { uid: string; /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $routeParams) { + constructor(private backendSrv, navModelSrv, private $routeParams, $location) { if (this.$routeParams.uid) { this.uid = $routeParams.uid; const loader = new FolderPageLoader(this.backendSrv); - loader.load(this, this.uid, 'manage-folder-dashboards'); + loader.load(this, this.uid, 'manage-folder-dashboards').then(folder => { + if ($location.path() !== folder.meta.url) { + $location.path(folder.meta.url).replace(); + } + }); } } } diff --git a/public/app/features/dashboard/folder_permissions_ctrl.ts b/public/app/features/dashboard/folder_permissions_ctrl.ts index 7a5e4c6af7d..fa1d71ef371 100644 --- a/public/app/features/dashboard/folder_permissions_ctrl.ts +++ b/public/app/features/dashboard/folder_permissions_ctrl.ts @@ -6,11 +6,15 @@ export class FolderPermissionsCtrl { uid: string; /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $routeParams) { + constructor(private backendSrv, navModelSrv, private $routeParams, $location) { if (this.$routeParams.uid) { this.uid = $routeParams.uid; - new FolderPageLoader(this.backendSrv).load(this, this.uid, 'manage-folder-permissions'); + new FolderPageLoader(this.backendSrv).load(this, this.uid, 'manage-folder-permissions').then(folder => { + if ($location.path() !== folder.meta.url) { + $location.path(`${folder.meta.url}/permissions`).replace(); + } + }); } } } diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts index ddc35166cad..004ba2efa9f 100644 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ b/public/app/features/dashboard/folder_settings_ctrl.ts @@ -18,10 +18,14 @@ export class FolderSettingsCtrl { this.uid = $routeParams.uid; this.folderPageLoader = new FolderPageLoader(this.backendSrv); - this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(result => { - this.dashboard = result.dashboard; - this.meta = result.meta; - this.canSave = result.meta.canSave; + this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => { + if ($location.path() !== folder.meta.url) { + $location.path(`${folder.meta.url}/settings`).replace(); + } + + this.dashboard = folder.dashboard; + this.meta = folder.meta; + this.canSave = folder.meta.canSave; this.title = this.dashboard.title; }); } diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index 4ad77512ad1..80e74819069 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -29,6 +29,10 @@ export class LoadDashboardCtrl { } dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) { + if ($location.path() !== result.meta.url) { + $location.path(result.meta.url).replace(); + } + if ($routeParams.keepRows) { result.meta.keepRows = true; } From 90933b06216ab6c766de776cb1791ddcb4dc9f54 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Jan 2018 23:14:48 +0100 Subject: [PATCH 44/84] dashboard: refactor logic for retrieving url for folder/dashboard --- pkg/api/dashboard.go | 16 +++------------- pkg/models/dashboards.go | 14 ++++++++++++++ pkg/services/sqlstore/dashboard.go | 8 +------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 7799ad868f9..fe7ea430f73 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -90,6 +90,7 @@ func GetDashboard(c *middleware.Context) Response { IsFolder: dash.IsFolder, FolderId: dash.FolderId, FolderTitle: "Root", + Url: dash.GetUrl(), } // lookup folder title @@ -101,12 +102,6 @@ func GetDashboard(c *middleware.Context) Response { meta.FolderTitle = query.Result.Title } - if dash.IsFolder { - meta.Url = m.GetFolderUrl(dash.Uid, dash.Slug) - } else { - meta.Url = m.GetDashboardUrl(dash.Uid, dash.Slug) - } - // make sure db version is in sync with json model version dash.Data.Set("version", dash.Version) @@ -238,12 +233,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { return ApiError(500, "Invalid alert data. Cannot save dashboard", err) } - var url string - if dash.IsFolder { - url = m.GetFolderUrl(dashboard.Uid, dashboard.Slug) - } else { - url = m.GetDashboardUrl(dashboard.Uid, dashboard.Slug) - } + dashboard.IsFolder = dash.IsFolder c.TimeRequest(metrics.M_Api_Dashboard_Save) return Json(200, util.DynMap{ @@ -252,7 +242,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { "version": dashboard.Version, "id": dashboard.Id, "uid": dashboard.Uid, - "url": url, + "url": dashboard.GetUrl(), }) } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 84ede04f226..1b0c3f8987d 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -158,6 +158,20 @@ func SlugifyTitle(title string) string { return slug.Make(strings.ToLower(title)) } +// GetUrl return the html url for a folder if it's folder, otherwise for a dashboard +func (dash *Dashboard) GetUrl() string { + return GetDashboardFolderUrl(dash.IsFolder, dash.Uid, dash.Slug) +} + +// GetDashboardFolderUrl return the html url for a folder if it's folder, otherwise for a dashboard +func GetDashboardFolderUrl(isFolder bool, uid string, slug string) string { + if isFolder { + return GetFolderUrl(uid, slug) + } + + return GetDashboardUrl(uid, slug) +} + // GetDashboardUrl return the html url for a dashboard func GetDashboardUrl(uid string, slug string) string { return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 84cfd1999c7..b12c1d70b9e 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -258,17 +258,11 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard for _, item := range res { hit, exists := hits[item.Id] if !exists { - var url string - if item.IsFolder { - url = m.GetFolderUrl(item.Uid, item.Slug) - } else { - url = m.GetDashboardUrl(item.Uid, item.Slug) - } hit = &search.Hit{ Id: item.Id, Title: item.Title, Uri: "db/" + item.Slug, - Url: url, + Url: m.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug), Slug: item.Slug, Type: getHitType(item), FolderId: item.FolderId, From b55ce1dd72c2493292d423d8d9434b743013f076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Feb 2018 10:55:29 +0100 Subject: [PATCH 45/84] docs: moved whats new article to master --- docs/sources/administration/permissions.md | 76 +++++++++++++ docs/sources/guides/whats-new-in-v5.md | 120 +++++++++++++++++++++ docs/sources/reference/admin.md | 42 -------- 3 files changed, 196 insertions(+), 42 deletions(-) create mode 100644 docs/sources/administration/permissions.md create mode 100644 docs/sources/guides/whats-new-in-v5.md delete mode 100644 docs/sources/reference/admin.md diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md new file mode 100644 index 00000000000..5cfc6ecac9c --- /dev/null +++ b/docs/sources/administration/permissions.md @@ -0,0 +1,76 @@ ++++ +title = "Permissions" +description = "Grafana user permissions" +keywords = ["grafana", "configuration", "documentation", "admin", "users", "permissions"] +type = "docs" +aliases = ["/reference/admin"] +[menu.docs] +name = "Permissions" +parent = "admin" +weight = 3 ++++ + +# Permissions + +Grafana users have permissions that are determined by their: + +- **Organization Role** (Admin, Editor, Viewer) +- Via **Team** memberships where the **Team** has been assigned specific permissions. +- Via permissions assigned directly to user (on folders or dashboards) +- The Grafana Admin (i.e. Super Admin) user flag. + +## Organization Roles + +Users can be belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do +in that organization. + +### Admin Role + +Can do everything scoped to the organization. For example: + +- Add & Edit data data sources. +- Add & Edit organization users & teams. +- Configure App plugins & set org settings. + +### Editor Role + +- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards. +- **Cannot** create or edit data sources nor invite new users. + +### Viewer Role + +- View any dashboard. This can be disabled on specific folders and dashboards. +- **Cannot** create or edit dashboards nor data sources. + +This role can be tweaked via Grafana server setting [viewers_can_edit]({{< relref "installation/configuration.md#viewers-can-edit" >}}). If you set this to true users +with **Viewer** can also make transient dashboard edits, meaning they can modify panels & queries but not save the changes (nor create new dashboards). +Useful for public Grafana installations where you want anonymous users to be able to edit panels & queries but not save or create new dashboards. + +## Grafana Admin + +This admin flag makes a user a `Super Admin`. This means they can access the `Server Admin` views where all users and organizations can be administrated. + +### Dashboard & Folder Permissions + +> Introduced in Grafana v5.0 + +{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} + +For dashboards and dashboard folders there is a **Permissions** page that make it possible to +remove the default role based permssions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. + +You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. + +Permission levels: + +- **Admin**: Can edit & create dashboards and edit permissions. +- **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions. +- **View**: Can only view existing dashboars/folders. + +#### Restricting access + +The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the +Access Control List (ACL). + +- You cannot override permissions for users with **Org Admin Role** +- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. \ No newline at end of file diff --git a/docs/sources/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md new file mode 100644 index 00000000000..4580dbffd66 --- /dev/null +++ b/docs/sources/guides/whats-new-in-v5.md @@ -0,0 +1,120 @@ ++++ +title = "What's New in Grafana v5.0" +description = "Feature & improvement highlights for Grafana v5.0" +keywords = ["grafana", "new", "documentation", "5.0"] +type = "docs" +[menu.docs] +name = "Version 5.0" +identifier = "v5.0" +parent = "whatsnew" +weight = -6 ++++ + +# What's New in Grafana v5.0 + +This is the most substantial update that Grafana has ever seen. This article will detail the major new features and enhancements. + +- [New Dashboard Layout Engine]({{< relref "#new-dashboard-layout-engine" >}}) enables a much easier drag, drop and resize experience and new types of layouts. +- [New UX]({{< relref "#new-ux-layout-engine" >}}). The UI has big improvements in both look and function. +- [New Light Theme]({{< relref "#new-light-theme" >}}) is now looking really nice. +- [Dashboard Folders]({{< relref "#dashboard-folders" >}}) helps you keep your dashboards organized. +- [Permissions]({{< relref "#dashboard-folders" >}}) on folders and dashboards helps manage larger Grafana installations. +- [Group users into teams]({{< relref "#teams" >}}) and use them in the new permission system. +- [Datasource provisioning]({{< relref "#data-sources" >}}) makes it possible to setup datasources via config files. +- [Dashboard provisioning]({{< relref "#dashboards" >}}) makes it possible to setup dashboards via config files. + +### Video showing new features + + +
+ +## New Dashboard Layout Engine + +{{< docs-imagebox img="/img/docs/v50/new_grid.png" max-width="1000px" class="docs-image--right">}} + +The new dashboard layout engine allows for much easier movement and sizing of panels, as other panels now move out of the way in +a very intuitive way. Panels are sized independently, so rows are no longer necessary to create layouts. This opens +up many new types of layouts where panels of different heights can be aligned easily. Checkout the new grid in the video +above or on the [play site](http://play.grafana.org). All your existing dashboards will automatically migrate to the +new position system and look close to identical. The new panel position makes dashboards saved in v5.0 not compatible +with older versions of Grafana. + +
+ +## New UX + +{{< docs-imagebox img="/img/docs/v50/new_ux_nav.png" max-width="1000px" class="docs-image--right" >}} + +Almost every page has seen significant UX improvements. All pages (except dashboard pages) have a new tab-based layout that improves navigation between pages. The side menu has also changed quite a bit. You can still hide the side menu completely if you click on the Grafana logo. + +
+ +### Dashboard Settings + +{{< docs-imagebox img="/img/docs/v50/dashboard_settings.png" max-width="1000px" class="docs-image--right" >}} +Dashboard pages have a new header toolbar where buttons and actions are now all moved to the right. All the dashboard +settings views have been combined with a side nav which allows you to easily move between different setting categories. + +
+ +## New Light Theme + +{{< docs-imagebox img="/img/docs/v50/new_white_theme.png" max-width="1000px" class="docs-image--right" >}} + +This theme has not seen a lot of love in recent years and we felt it was time to rework it and give it a major overhaul. We are very happy with the result. + +
+ +## Dashboard Folders + +{{< docs-imagebox img="/img/docs/v50/new_search.png" max-width="1000px" class="docs-image--right" >}} + +The big new feature that comes with Grafana v5.0 is dashboard folders. Now you can organize your dashboards in folders, +which is very useful if you have a lot of dashboards or multiple teams. + +- New search design adds expandable sections for each folder, starred and recently viewed dashboards. +- New manage dashboard pages enable batch actions and views for folder settings and permissions. +- Set permissions on folders and have dashboards inherit the permissions. + +## Teams + +A team is a new concept in Grafana v5. They are simply a group of users that can be then be used in the new permission system for dashboards and folders. Only an admin can create teams. +We hope to do more with teams in future releases like integration with LDAP and a team landing page. + +## Permissions + +{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="1000px" class="docs-image--right" >}} + +You can assign permissions to folders and dashboards. The default user role-based permissions can be removed and replaced with specific teams or users enabling more control over what a user can see and edit. + +
+ +# Provisioning from configuration + +In previous versions of Grafana, you could only use the API for provisioning data sources and dashboards. +But that required the service to be running before you started creating dashboards and you also needed to +set up credentials for the HTTP API. In 5.0 we decided to improve this experience by adding a new active +provisioning system that uses config files. This will make GitOps more natural as data sources and dashboards can +be defined via files that can be version controlled. We hope to extend this system to later add support for users, orgs +and alerts as well. + +### Data sources + +Data sources can now be setup using config files. These data sources are by default not editable from the Grafana GUI. +It's also possible to update and delete data sources from the config file. More info in the [data source provisioning docs](/administration/provisioning/#datasources). + +### Dashboards + +We also deprecated the [dashboard.json] in favor of our new dashboard provisioner that keeps dashboards on disk +in sync with dashboards in Grafana's database. The dashboard provisioner has multiple advantages over the old +[dashboard.json] feature. Instead of storing the dashboard in memory we now insert the dashboard into the database, +which makes it possible to star them, use one as the home dashboard, set permissions and other features in Grafana that +expects the dashboards to exist in the database. More info in the [dashboard provisioning docs](/administration/provisioning/#dashboards) + +# Dashboard model & API + +We are introducing a new identifier (`uid`) in the dashboard JSON model. The new identifier will be a 9-12 character long unique id. +We are also changing the route for getting dashboards to use this `uid` instead of the slug that the current route and API are using. +We will keep supporting the old route for backward compatibility. This will make it possible to change the title on dashboards without breaking links. +Sharing dashboards between instances becomes much easier since the uid is unique (unique enough). This might seem like a small change, +but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards. \ No newline at end of file diff --git a/docs/sources/reference/admin.md b/docs/sources/reference/admin.md deleted file mode 100644 index a6863b4ea71..00000000000 --- a/docs/sources/reference/admin.md +++ /dev/null @@ -1,42 +0,0 @@ -+++ -title = "Admin Roles" -description = "Users & Organization permission and administration" -keywords = ["grafana", "configuration", "documentation", "admin", "users", "permissions"] -type = "docs" -[menu.docs] -name = "Admin Roles" -parent = "admin" -weight = 3 -+++ - -# Administration - -Grafana has two levels of administrators: - -* Organizational administrators: These admins can manage users within specific organizations in a particular Grafana installation -* Grafana administrators: These super admins can manage users across all organizations in a Grafana installation. They can also change and access system-wide settings. - -## Organizational Administrators - -As an Organizational administrator, you can add `Data Sources`, add Users to your Organization and -modify Organization details and options. - -> *Note*: If Grafana is configured with `users.allow_org_create = true`, any User of any Organization will be able to -> start their own Organization and become the administrator of that Organization. - - -## Grafana Administrators - - -As a Grafana Administrator, you have complete access to any Organization or User in that instance of Grafana. -When performing actions as a Grafana admin, the sidebar will change it's appearance as below to indicate you are performing global server administration. - -From the Grafana Server Admin page, you can access the System Info page which summarizes all of the backend configuration settings of the Grafana server. - -## Why would I have multiple Organizations? - -Organizations in Grafana are best suited for a **multi-tenant deployment**. In a multi-tenant deployment, -Organizations can be used to provide a full Grafana experience to different sets of users from a single Grafana instance, -at the convenience of the Grafana Administrator. - -In most cases, a Grafana installation will only have **one** Organization. Since dashboards, data sources and other configuration items are not shared between organizations, there's no need to create multiple Organizations if you want all your users to have access to the same set of dashboards and data. From d08a829b690a714573351e35710a21a3c4f4d6aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Feb 2018 11:29:04 +0100 Subject: [PATCH 46/84] ux: fix for responsive breakpoints and solo mode showing sidemenu --- public/app/features/panel/solo_panel_ctrl.ts | 2 ++ public/sass/_old_responsive.scss | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index f141f89eb80..fc5da20f52b 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -1,4 +1,5 @@ import angular from 'angular'; +import appEvents from 'app/core/app_events'; export class SoloPanelCtrl { /** @ngInject */ @@ -7,6 +8,7 @@ export class SoloPanelCtrl { $scope.init = function() { contextSrv.sidemenu = false; + appEvents.emit('toggle-sidemenu'); var params = $location.search(); panelId = parseInt(params.panelId); diff --git a/public/sass/_old_responsive.scss b/public/sass/_old_responsive.scss index 164d6dd09c7..991b0f30aa1 100644 --- a/public/sass/_old_responsive.scss +++ b/public/sass/_old_responsive.scss @@ -18,8 +18,8 @@ // --------------------- @include media-breakpoint-down(xs) { - input[type="text"], - input[type="number"], + input[type='text'], + input[type='number'], textarea { font-size: 16px; } @@ -51,9 +51,15 @@ display: flex; } .navbar-page-btn { - max-width: none; + max-width: 450px; } .gf-timepicker-nav-btn { max-width: none; } } + +@include media-breakpoint-up(xl) { + .navbar-page-btn { + max-width: 600px; + } +} From 50bd9eee55a40dcaafc8651edc16362e70a15a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Feb 2018 13:16:39 +0100 Subject: [PATCH 47/84] docs: removed section with session table sql, that is not needed anymore --- docs/sources/installation/configuration.md | 25 ---------------------- 1 file changed, 25 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 5f458a48aeb..498ab7fc8df 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -671,31 +671,6 @@ session provider you have configured. - **memcache:** ex: 127.0.0.1:11211 - **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana` -If you use MySQL or Postgres as the session store you need to create the -session table manually. - -Mysql Example: - -```bash -CREATE TABLE `session` ( - `key` CHAR(16) NOT NULL, - `data` BLOB, - `expiry` INT(11) UNSIGNED NOT NULL, - PRIMARY KEY (`key`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; -``` - -Postgres Example: - -```bash -CREATE TABLE session ( - key CHAR(16) NOT NULL, - data BYTEA, - expiry INTEGER NOT NULL, - PRIMARY KEY (key) -); -``` - Postgres valid `sslmode` are `disable`, `require`, `verify-ca`, and `verify-full` (default). ### cookie_name From ee9c40818803ee97322f676cdb7a9ead99d28ea8 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Feb 2018 13:21:24 +0100 Subject: [PATCH 48/84] folders: changes needed due to merge --- .../ManageDashboards/FolderPermissions.tsx | 2 +- .../ManageDashboards/FolderSettings.jest.tsx | 4 ++-- .../ManageDashboards/FolderSettings.tsx | 6 ++++-- .../manage_dashboards/manage_dashboards.ts | 12 ++++++------ public/app/core/services/backend_srv.ts | 2 +- .../dashboard/partials/folder_dashboards.html | 2 +- public/app/stores/FolderStore/FolderStore.ts | 17 +++++++++-------- public/app/stores/NavStore/NavStore.jest.ts | 10 +++++----- public/app/stores/NavStore/NavStore.ts | 12 +++--------- public/test/mocks/common.ts | 1 + 10 files changed, 33 insertions(+), 35 deletions(-) diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 3214382732a..e6e788b7585 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -16,7 +16,7 @@ export class FolderPermissions extends Component { loadStore() { const { nav, folder, view } = this.props; - return folder.load(view.routeParams.get('slug') as string).then(res => { + return folder.load(view.routeParams.get('uid') as string).then(res => { return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions'); }); } diff --git a/public/app/containers/ManageDashboards/FolderSettings.jest.tsx b/public/app/containers/ManageDashboards/FolderSettings.jest.tsx index 3355b657f03..bf7b35ed05d 100644 --- a/public/app/containers/ManageDashboards/FolderSettings.jest.tsx +++ b/public/app/containers/ManageDashboards/FolderSettings.jest.tsx @@ -9,14 +9,14 @@ describe('FolderSettings', () => { let page; beforeAll(() => { - backendSrv.getDashboard.mockReturnValue( + backendSrv.getDashboardByUid.mockReturnValue( Promise.resolve({ dashboard: { id: 1, title: 'Folder Name', }, meta: { - slug: 'folder-name', + url: '/dashboards/f/uid/folder-name', canSave: true, }, }) diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx index ef3377622df..a6349764a14 100644 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ b/public/app/containers/ManageDashboards/FolderSettings.tsx @@ -20,10 +20,12 @@ export class FolderSettings extends React.Component { loadStore() { const { nav, folder, view } = this.props; - return folder.load(view.routeParams.get('slug') as string).then(res => { + return folder.load(view.routeParams.get('uid') as string).then(res => { this.formSnapshot = getSnapshot(folder); this.dashboard = res.dashboard; + view.updatePathAndQuery(`${res.meta.url}/settings`, {}, {}); + return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); }); } @@ -51,7 +53,7 @@ export class FolderSettings extends React.Component { folder .saveFolder(this.dashboard, { overwrite: false }) .then(newUrl => { - view.updatePathAndQuery(newUrl, '', ''); + view.updatePathAndQuery(newUrl, {}, {}); appEvents.emit('dashboard-saved'); appEvents.emit('alert-success', ['Folder saved']); diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index 9eb14a69a6e..d0f905f5f16 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -34,7 +34,7 @@ export class ManageDashboardsCtrl { // used when managing dashboards for a specific folder folderId?: number; - folderSlug?: string; + folderUid?: string; // if user can add new folders and/or add new dashboards canSave: boolean; @@ -74,11 +74,11 @@ export class ManageDashboardsCtrl { return this.initDashboardList(result); }) .then(() => { - if (!this.folderSlug) { + if (!this.folderUid) { return; } - return this.backendSrv.getDashboard('db', this.folderSlug).then(dash => { + return this.backendSrv.getDashboardByUid(this.folderUid).then(dash => { this.canSave = dash.meta.canSave; }); }); @@ -179,8 +179,8 @@ export class ManageDashboardsCtrl { }); } - private deleteFoldersAndDashboards(slugs) { - this.backendSrv.deleteDashboards(slugs).then(result => { + private deleteFoldersAndDashboards(uids) { + this.backendSrv.deleteDashboards(uids).then(result => { const folders = _.filter(result, dash => dash.meta.isFolder); const folderCount = folders.length; const dashboards = _.filter(result, dash => !dash.meta.isFolder); @@ -334,7 +334,7 @@ export function manageDashboardsDirective() { controllerAs: 'ctrl', scope: { folderId: '=', - folderSlug: '=', + folderUid: '=', }, }; } diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 6d44967ef39..0a8b305ea53 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -257,7 +257,7 @@ export class BackendSrv { }); } - updateDashboardFolder(dash, options) { + saveFolder(dash, options) { options = options || {}; return this.post('/api/dashboards/db/', { diff --git a/public/app/features/dashboard/partials/folder_dashboards.html b/public/app/features/dashboard/partials/folder_dashboards.html index a03c53233f5..1082bee402e 100644 --- a/public/app/features/dashboard/partials/folder_dashboards.html +++ b/public/app/features/dashboard/partials/folder_dashboards.html @@ -1,5 +1,5 @@
- +
diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts index d0216e32ad7..6f14e7221f8 100644 --- a/public/app/stores/FolderStore/FolderStore.ts +++ b/public/app/stores/FolderStore/FolderStore.ts @@ -2,8 +2,8 @@ import { types, getEnv, flow } from 'mobx-state-tree'; export const Folder = types.model('Folder', { id: types.identifier(types.number), - slug: types.string, title: types.string, + url: types.string, canSave: types.boolean, hasChanged: types.boolean, }); @@ -13,13 +13,13 @@ export const FolderStore = types folder: types.maybe(Folder), }) .actions(self => ({ - load: flow(function* load(slug: string) { + load: flow(function* load(uid: string) { const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.getDashboard('db', slug); + const res = yield backendSrv.getDashboardByUid(uid); self.folder = Folder.create({ id: res.dashboard.id, title: res.dashboard.title, - slug: res.meta.slug, + url: res.meta.url, canSave: res.meta.canSave, hasChanged: false, }); @@ -35,14 +35,15 @@ export const FolderStore = types const backendSrv = getEnv(self).backendSrv; dashboard.title = self.folder.title.trim(); - const res = yield backendSrv.saveDashboard(dashboard, options); - self.folder.slug = res.slug; - return `dashboards/folder/${self.folder.id}/${res.slug}/settings`; + const res = yield backendSrv.saveFolder(dashboard, options); + self.folder.url = res.url; + + return `${self.folder.url}/settings`; }), deleteFolder: flow(function* deleteFolder() { const backendSrv = getEnv(self).backendSrv; - return backendSrv.deleteDashboard(self.folder.slug); + return backendSrv.deleteDashboard(self.folder.url); }), })); diff --git a/public/app/stores/NavStore/NavStore.jest.ts b/public/app/stores/NavStore/NavStore.jest.ts index b1ad820910d..43d4496c858 100644 --- a/public/app/stores/NavStore/NavStore.jest.ts +++ b/public/app/stores/NavStore/NavStore.jest.ts @@ -3,12 +3,12 @@ import { NavStore } from './NavStore'; describe('NavStore', () => { const folderId = 1; const folderTitle = 'Folder Name'; - const folderSlug = 'folder-name'; + const folderUrl = '/dashboards/f/uid/folder-name'; const canAdmin = true; const folder = { id: folderId, - slug: folderSlug, + url: folderUrl, title: folderTitle, canAdmin: canAdmin, }; @@ -33,9 +33,9 @@ describe('NavStore', () => { it('Should set correct urls for each tab', () => { expect(store.main.children.length).toBe(3); - expect(store.main.children[0].url).toBe(`dashboards/folder/${folderId}/${folderSlug}`); - expect(store.main.children[1].url).toBe(`dashboards/folder/${folderId}/${folderSlug}/permissions`); - expect(store.main.children[2].url).toBe(`dashboards/folder/${folderId}/${folderSlug}/settings`); + expect(store.main.children[0].url).toBe(folderUrl); + expect(store.main.children[1].url).toBe(`${folderUrl}/permissions`); + expect(store.main.children[2].url).toBe(`${folderUrl}/settings`); }); it('Should set active tab', () => { diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts index d04c5bf0259..86348c00487 100644 --- a/public/app/stores/NavStore/NavStore.ts +++ b/public/app/stores/NavStore/NavStore.ts @@ -41,8 +41,6 @@ export const NavStore = types }, initFolderNav(folder: any, activeChildId: string) { - const folderUrl = createFolderUrl(folder.id, folder.slug); - let main = { icon: 'fa fa-folder-open', id: 'manage-folder', @@ -56,21 +54,21 @@ export const NavStore = types icon: 'fa fa-fw fa-th-large', id: 'manage-folder-dashboards', text: 'Dashboards', - url: folderUrl, + url: folder.url, }, { active: activeChildId === 'manage-folder-permissions', icon: 'fa fa-fw fa-lock', id: 'manage-folder-permissions', text: 'Permissions', - url: folderUrl + '/permissions', + url: `${folder.url}/permissions`, }, { active: activeChildId === 'manage-folder-settings', icon: 'fa fa-fw fa-cog', id: 'manage-folder-settings', text: 'Settings', - url: folderUrl + '/settings', + url: `${folder.url}/settings`, }, ], }; @@ -118,7 +116,3 @@ export const NavStore = types self.main = NavItem.create(main); }, })); - -function createFolderUrl(folderId: number, slug: string) { - return `dashboards/folder/${folderId}/${slug}`; -} diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index da63381ddf4..3f80227435d 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -1,6 +1,7 @@ export const backendSrv = { get: jest.fn(), getDashboard: jest.fn(), + getDashboardByUid: jest.fn(), post: jest.fn(), }; From 16e1640ba413746ae77f190f711a7acfd3e32eea Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 1 Feb 2018 15:23:45 +0300 Subject: [PATCH 49/84] repeat panel: process repeats when row is expanding (#10712) --- .../app/features/dashboard/dashboard_model.ts | 42 ++++++++++++++++--- .../features/dashboard/specs/repeat.jest.ts | 19 +++++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 4086edc100a..9cadb198fa3 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -279,6 +279,40 @@ export class DashboardModel { this.events.emit('repeats-processed'); } + cleanUpRowRepeats(rowPanels) { + let panelsToRemove = []; + for (let i = 0; i < rowPanels.length; i++) { + let panel = rowPanels[i]; + if (!panel.repeat && panel.repeatPanelId) { + panelsToRemove.push(panel); + } + } + _.pull(rowPanels, ...panelsToRemove); + _.pull(this.panels, ...panelsToRemove); + } + + processRowRepeats(row: PanelModel) { + if (this.snapshot || this.templating.list.length === 0) { + return; + } + + let rowPanels = row.panels; + if (!row.collapsed) { + let rowPanelIndex = _.findIndex(this.panels, p => p.id === row.id); + rowPanels = this.getRowPanels(rowPanelIndex); + } + + this.cleanUpRowRepeats(rowPanels); + + for (let i = 0; i < rowPanels.length; i++) { + let panel = rowPanels[i]; + if (panel.repeat) { + let panelIndex = _.findIndex(this.panels, p => p.id === panel.id); + this.repeatPanel(panel, panelIndex); + } + } + } + getPanelRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) { // if first clone return source if (valueIndex === 0) { @@ -569,7 +603,7 @@ export class DashboardModel { if (row.collapsed) { row.collapsed = false; - let hasRepeat = false; + let hasRepeat = _.some(row.panels, p => p.repeat); if (row.panels.length > 0) { // Use first panel to figure out if it was moved or pushed @@ -590,10 +624,6 @@ export class DashboardModel { // update insert post and y max insertPos += 1; yMax = Math.max(yMax, panel.gridPos.y + panel.gridPos.h); - - if (panel.repeat) { - hasRepeat = true; - } } const pushDownAmount = yMax - row.gridPos.y; @@ -606,7 +636,7 @@ export class DashboardModel { row.panels = []; if (hasRepeat) { - this.processRepeats(); + this.processRowRepeats(row); } } diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index 93555625b95..868db3b7246 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -629,4 +629,23 @@ describe('given dashboard with row and panel repeat', () => { region: { text: 'reg2', value: 'reg2' }, }); }); + + it('should repeat panels when row is expanding', function() { + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + expect(dashboard.panels.length).toBe(6); + + // toggle row + dashboard.toggleRow(dashboard.panels[0]); + dashboard.toggleRow(dashboard.panels[1]); + expect(dashboard.panels.length).toBe(2); + + // change variable + dashboard.templating.list[1].current.value = ['se1', 'se2', 'se3']; + + // toggle row back + dashboard.toggleRow(dashboard.panels[1]); + expect(dashboard.panels.length).toBe(4); + }); }); From cdf2fd64b7a281a0677b8d3cec7cac7ba4841b4e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Feb 2018 13:28:04 +0100 Subject: [PATCH 50/84] route params from angular to view store should be updated on routeChangeSuccess --- public/app/core/services/bridge_srv.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index f0166d89e9d..ed4abb9b894 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -34,10 +34,7 @@ export class BridgeSrv { }); this.$rootScope.$on('$routeChangeSuccess', (evt, data) => { - let angularUrl = this.$location.url(); - if (store.view.currentUrl !== angularUrl) { - store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); - } + store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); }); reaction( @@ -45,7 +42,9 @@ export class BridgeSrv { currentUrl => { let angularUrl = this.$location.url(); if (angularUrl !== currentUrl) { - this.$location.url(currentUrl); + this.$timeout(() => { + this.$location.url(currentUrl); + }); console.log('store updating angular $location.url', currentUrl); } } From c0f100f1b5fd1c7f45ab9f9f20d4150d8fe0cf12 Mon Sep 17 00:00:00 2001 From: Mikael Olenfalk Date: Thu, 1 Feb 2018 14:04:52 +0100 Subject: [PATCH 51/84] Improve logging in the phantomjs renderer (#10697) * Add add adapter between io.Writer and log.Logger * Add phantomjs output to grafana log * Unexport LogWriterImpl * Add test for LogWriter * Make it possible to get phantomjs debug output * Make it possible to get the configured log level --- pkg/components/renderer/renderer.go | 19 +++-- pkg/log/log.go | 28 ++++++- pkg/log/log_writer.go | 39 ++++++++++ pkg/log/log_writer_test.go | 116 ++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 pkg/log/log_writer.go create mode 100644 pkg/log/log_writer_test.go diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index 25d77557342..313f7892707 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -91,9 +91,15 @@ func RenderToPng(params *RenderOpts) (string, error) { timeout = 15 } + phantomDebugArg := "--debug=false" + if log.GetLogLevelFor("png-renderer") >= log.LvlDebug { + phantomDebugArg = "--debug=true" + } + cmdArgs := []string{ "--ignore-ssl-errors=true", "--web-security=false", + phantomDebugArg, scriptPath, "url=" + url, "width=" + params.Width, @@ -109,15 +115,13 @@ func RenderToPng(params *RenderOpts) (string, error) { } cmd := exec.Command(binPath, cmdArgs...) - stdout, err := cmd.StdoutPipe() + output, err := cmd.StdoutPipe() if err != nil { + rendererLog.Error("Could not acquire stdout pipe", err) return "", err } - stderr, err := cmd.StderrPipe() - if err != nil { - return "", err - } + cmd.Stderr = cmd.Stdout if params.Timezone != "" { baseEnviron := os.Environ() @@ -126,11 +130,12 @@ func RenderToPng(params *RenderOpts) (string, error) { err = cmd.Start() if err != nil { + rendererLog.Error("Could not start command", err) return "", err } - go io.Copy(os.Stdout, stdout) - go io.Copy(os.Stdout, stderr) + logWriter := log.NewLogWriter(rendererLog, log.LvlDebug, "[phantom] ") + go io.Copy(logWriter, output) done := make(chan error) go func() { diff --git a/pkg/log/log.go b/pkg/log/log.go index 88b90f0cf8e..0e6874e1b4b 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -21,6 +21,7 @@ import ( var Root log15.Logger var loggersToClose []DisposableHandler +var filters map[string]log15.Lvl func init() { loggersToClose = make([]DisposableHandler, 0) @@ -114,6 +115,25 @@ func Close() { loggersToClose = make([]DisposableHandler, 0) } +func GetLogLevelFor(name string) Lvl { + if level, ok := filters[name]; ok { + switch level { + case log15.LvlWarn: + return LvlWarn + case log15.LvlInfo: + return LvlInfo + case log15.LvlError: + return LvlError + case log15.LvlCrit: + return LvlCrit + default: + return LvlDebug + } + } + + return LvlInfo +} + var logLevels = map[string]log15.Lvl{ "trace": log15.LvlDebug, "debug": log15.LvlDebug, @@ -187,7 +207,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) { // Log level. _, level := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg) - modeFilters := getFilters(util.SplitString(sec.Key("filters").String())) + filters := getFilters(util.SplitString(sec.Key("filters").String())) format := getLogFormat(sec.Key("format").MustString("")) var handler log15.Handler @@ -219,12 +239,12 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) { } for key, value := range defaultFilters { - if _, exist := modeFilters[key]; !exist { - modeFilters[key] = value + if _, exist := filters[key]; !exist { + filters[key] = value } } - handler = LogFilterHandler(level, modeFilters, handler) + handler = LogFilterHandler(level, filters, handler) handlers = append(handlers, handler) } diff --git a/pkg/log/log_writer.go b/pkg/log/log_writer.go new file mode 100644 index 00000000000..2ff401a7f0a --- /dev/null +++ b/pkg/log/log_writer.go @@ -0,0 +1,39 @@ +package log + +import ( + "io" + "strings" +) + +type logWriterImpl struct { + log Logger + level Lvl + prefix string +} + +func NewLogWriter(log Logger, level Lvl, prefix string) io.Writer { + return &logWriterImpl{ + log: log, + level: level, + prefix: prefix, + } +} + +func (l *logWriterImpl) Write(p []byte) (n int, err error) { + message := l.prefix + strings.TrimSpace(string(p)) + + switch l.level { + case LvlCrit: + l.log.Crit(message) + case LvlError: + l.log.Error(message) + case LvlWarn: + l.log.Warn(message) + case LvlInfo: + l.log.Info(message) + default: + l.log.Debug(message) + } + + return len(p), nil +} diff --git a/pkg/log/log_writer_test.go b/pkg/log/log_writer_test.go new file mode 100644 index 00000000000..4537b4d6100 --- /dev/null +++ b/pkg/log/log_writer_test.go @@ -0,0 +1,116 @@ +package log + +import ( + "testing" + + "github.com/inconshreveable/log15" + . "github.com/smartystreets/goconvey/convey" +) + +type FakeLogger struct { + debug string + info string + warn string + err string + crit string +} + +func (f *FakeLogger) New(ctx ...interface{}) log15.Logger { + return nil +} + +func (f *FakeLogger) Debug(msg string, ctx ...interface{}) { + f.debug = msg +} + +func (f *FakeLogger) Info(msg string, ctx ...interface{}) { + f.info = msg +} + +func (f *FakeLogger) Warn(msg string, ctx ...interface{}) { + f.warn = msg +} + +func (f *FakeLogger) Error(msg string, ctx ...interface{}) { + f.err = msg +} + +func (f *FakeLogger) Crit(msg string, ctx ...interface{}) { + f.crit = msg +} + +func (f *FakeLogger) GetHandler() log15.Handler { + return nil +} + +func (f *FakeLogger) SetHandler(l log15.Handler) {} + +func TestLogWriter(t *testing.T) { + Convey("When writing to a LogWriter", t, func() { + Convey("Should write using the correct level [crit]", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlCrit, "") + n, err := crit.Write([]byte("crit")) + + So(n, ShouldEqual, 4) + So(err, ShouldBeNil) + So(fake.crit, ShouldEqual, "crit") + }) + + Convey("Should write using the correct level [error]", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlError, "") + n, err := crit.Write([]byte("error")) + + So(n, ShouldEqual, 5) + So(err, ShouldBeNil) + So(fake.err, ShouldEqual, "error") + }) + + Convey("Should write using the correct level [warn]", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlWarn, "") + n, err := crit.Write([]byte("warn")) + + So(n, ShouldEqual, 4) + So(err, ShouldBeNil) + So(fake.warn, ShouldEqual, "warn") + }) + + Convey("Should write using the correct level [info]", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlInfo, "") + n, err := crit.Write([]byte("info")) + + So(n, ShouldEqual, 4) + So(err, ShouldBeNil) + So(fake.info, ShouldEqual, "info") + }) + + Convey("Should write using the correct level [debug]", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlDebug, "") + n, err := crit.Write([]byte("debug")) + + So(n, ShouldEqual, 5) + So(err, ShouldBeNil) + So(fake.debug, ShouldEqual, "debug") + }) + + Convey("Should prefix the output with the prefix", func() { + fake := &FakeLogger{} + + crit := NewLogWriter(fake, LvlDebug, "prefix") + n, err := crit.Write([]byte("debug")) + + So(n, ShouldEqual, 5) // n is how much of input consumed + So(err, ShouldBeNil) + So(fake.debug, ShouldEqual, "prefixdebug") + }) + }) +} From 992fd37010061e752b97799eddb2fbe26434c9e3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 1 Feb 2018 13:32:00 +0100 Subject: [PATCH 52/84] alert: use new url format --- pkg/models/dashboards.go | 17 +++++++++- pkg/services/alerting/eval_context.go | 47 ++++++++++++++------------- pkg/services/alerting/notifier.go | 4 +-- pkg/services/sqlstore/dashboard.go | 21 +++++++++++- 4 files changed, 63 insertions(+), 26 deletions(-) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 51ba62419b2..5675f494411 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -171,11 +171,16 @@ func GetDashboardFolderUrl(isFolder bool, uid string, slug string) string { return GetDashboardUrl(uid, slug) } -// GetDashboardUrl return the html url for a dashboard +// Return the html url for a dashboard func GetDashboardUrl(uid string, slug string) string { return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug) } +// Return the full url for a dashboard +func GetFullDashboardUrl(uid string, slug string) string { + return fmt.Sprintf("%s%s", setting.AppUrl, GetDashboardUrl(uid, slug)) +} + // GetFolderUrl return the html url for a folder func GetFolderUrl(folderUid string, slug string) string { return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug) @@ -277,3 +282,13 @@ type DashboardPermissionForUser struct { Permission PermissionType `json:"permission"` PermissionName string `json:"permissionName"` } + +type DashboardRef struct { + Uid string + Slug string +} + +type GetDashboardUIDByIdQuery struct { + Id int64 + Result *DashboardRef +} diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index f5663deb8ca..60c5530d486 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -12,17 +12,19 @@ import ( ) type EvalContext struct { - Firing bool - IsTestRun bool - EvalMatches []*EvalMatch - Logs []*ResultLogEntry - Error error - ConditionEvals string - StartTime time.Time - EndTime time.Time - Rule *Rule - log log.Logger - dashboardSlug string + Firing bool + IsTestRun bool + EvalMatches []*EvalMatch + Logs []*ResultLogEntry + Error error + ConditionEvals string + StartTime time.Time + EndTime time.Time + Rule *Rule + log log.Logger + + dashboardRef *m.DashboardRef + ImagePublicUrl string ImageOnDiskPath string NoDataFound bool @@ -83,29 +85,30 @@ func (c *EvalContext) GetNotificationTitle() string { return "[" + c.GetStateModel().Text + "] " + c.Rule.Name } -func (c *EvalContext) GetDashboardSlug() (string, error) { - if c.dashboardSlug != "" { - return c.dashboardSlug, nil +func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) { + if c.dashboardRef != nil { + return c.dashboardRef, nil } - slugQuery := &m.GetDashboardSlugByIdQuery{Id: c.Rule.DashboardId} - if err := bus.Dispatch(slugQuery); err != nil { - return "", err + uidQuery := &m.GetDashboardUIDByIdQuery{Id: c.Rule.DashboardId} + if err := bus.Dispatch(uidQuery); err != nil { + return nil, err } - c.dashboardSlug = slugQuery.Result - return c.dashboardSlug, nil + c.dashboardRef = uidQuery.Result + return c.dashboardRef, nil } +const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d" + func (c *EvalContext) GetRuleUrl() (string, error) { if c.IsTestRun { return setting.AppUrl, nil } - if slug, err := c.GetDashboardSlug(); err != nil { + if ref, err := c.GetDashboardUID(); err != nil { return "", err } else { - ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d", setting.AppUrl, slug, c.Rule.PanelId, c.Rule.OrgId) - return ruleUrl, nil + return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil } } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 47c9e0c590e..af9ba52a52a 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -87,10 +87,10 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { IsAlertContext: true, } - if slug, err := context.GetDashboardSlug(); err != nil { + if ref, err := context.GetDashboardUID(); err != nil { return err } else { - renderOpts.Path = fmt.Sprintf("dashboard-solo/db/%s?&panelId=%d", slug, context.Rule.PanelId) + renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId) } if imagePath, err := renderer.RenderToPng(renderOpts); err != nil { diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index a4e63a50633..cf9624b2f22 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -19,6 +19,7 @@ func init() { bus.AddHandler("sql", SearchDashboards) bus.AddHandler("sql", GetDashboardTags) bus.AddHandler("sql", GetDashboardSlugById) + bus.AddHandler("sql", GetDashboardUIDById) bus.AddHandler("sql", GetDashboardsByPluginId) bus.AddHandler("sql", GetFoldersForSignedInUser) bus.AddHandler("sql", GetDashboardPermissionsForUser) @@ -159,6 +160,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { return err }) } + func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { for i := 0; i < 3; i++ { uid := generateNewUid() @@ -539,7 +541,7 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error { var rawSql = `SELECT slug from dashboard WHERE Id=?` var slug = DashboardSlugDTO{} - exists, err := x.Sql(rawSql, query.Id).Get(&slug) + exists, err := x.SQL(rawSql, query.Id).Get(&slug) if err != nil { return err @@ -561,3 +563,20 @@ func GetDashboardsBySlug(query *m.GetDashboardsBySlugQuery) error { query.Result = dashboards return nil } + +func GetDashboardUIDById(query *m.GetDashboardUIDByIdQuery) error { + var rawSql = `SELECT uid, slug from dashboard WHERE Id=?` + + us := &m.DashboardRef{} + + exists, err := x.SQL(rawSql, query.Id).Get(us) + + if err != nil { + return err + } else if exists == false { + return m.ErrDashboardNotFound + } + + query.Result = us + return nil +} From 74ca6f6dbfea03f5e12e01f2b82b2453a4b89a8e Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 1 Feb 2018 13:16:56 +0100 Subject: [PATCH 53/84] changes dashboard url in alertlist --- pkg/api/alerting.go | 3 ++- public/app/containers/AlertRuleList/AlertRuleList.jest.tsx | 2 +- public/app/containers/AlertRuleList/AlertRuleList.tsx | 2 +- .../AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 42ea091ef10..dee1b7451f6 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -105,7 +105,8 @@ func transformToDTOs(alerts []*models.Alert, c *middleware.Context) ([]*dtos.Ale for _, alert := range alertDTOs { for _, dash := range dashboardsQuery.Result { if alert.DashboardId == dash.Id { - alert.DashbboardUri = "db/" + dash.Slug + alert.DashbboardUri = models.GetDashboardUrl(dash.Uid, dash.Slug) + break } } } diff --git a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx b/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx index f5aa07b454a..eaeba48f0a6 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx @@ -23,7 +23,7 @@ describe('AlertRuleList', () => { .format(), evalData: {}, executionError: '', - dashboardUri: 'db/mygool', + dashboardUri: 'd/ufkcofof/my-goal', canEdit: true, }, ]) diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx index d2712706154..5ce1efd9ee3 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.tsx @@ -137,7 +137,7 @@ export class AlertRuleItem extends React.Component { 'fa-pause': !rule.isPaused, }); - let ruleUrl = `dashboard/${rule.dashboardUri}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`; + let ruleUrl = `${rule.dashboardUri}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; return (
  • diff --git a/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap b/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap index da5fa5f12c4..0914f050a0f 100644 --- a/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap +++ b/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap @@ -21,7 +21,7 @@ exports[`AlertRuleList should render 1 rule 1`] = ` className="alert-rule-item__name" > Date: Thu, 1 Feb 2018 13:49:34 +0100 Subject: [PATCH 54/84] make it easier for dashboards to generate ur; --- pkg/api/alerting.go | 2 +- pkg/models/dashboards.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index dee1b7451f6..1fc3d893681 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -105,7 +105,7 @@ func transformToDTOs(alerts []*models.Alert, c *middleware.Context) ([]*dtos.Ale for _, alert := range alertDTOs { for _, dash := range dashboardsQuery.Result { if alert.DashboardId == dash.Id { - alert.DashbboardUri = models.GetDashboardUrl(dash.Uid, dash.Slug) + alert.DashbboardUri = dash.GenerateUrl() break } } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 5675f494411..089a98c4f00 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -162,6 +162,11 @@ func (dash *Dashboard) GetUrl() string { return GetDashboardFolderUrl(dash.IsFolder, dash.Uid, dash.Slug) } +// Return the html url for a dashboard +func (dash *Dashboard) GenerateUrl() string { + return GetDashboardUrl(dash.Uid, dash.Slug) +} + // GetDashboardFolderUrl return the html url for a folder if it's folder, otherwise for a dashboard func GetDashboardFolderUrl(isFolder bool, uid string, slug string) string { if isFolder { From 90207bcb7dc23d2fe01481cb4d6ba4531291dc88 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 1 Feb 2018 14:13:42 +0100 Subject: [PATCH 55/84] register handler for get dashboards by slug --- pkg/services/sqlstore/dashboard.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index cf9624b2f22..fd07ccd5b2c 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -23,6 +23,7 @@ func init() { bus.AddHandler("sql", GetDashboardsByPluginId) bus.AddHandler("sql", GetFoldersForSignedInUser) bus.AddHandler("sql", GetDashboardPermissionsForUser) + bus.AddHandler("sql", GetDashboardsBySlug) } var generateNewUid func() string = util.GenerateShortUid @@ -554,7 +555,7 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error { } func GetDashboardsBySlug(query *m.GetDashboardsBySlugQuery) error { - var dashboards = make([]*m.Dashboard, 0) + var dashboards []*m.Dashboard if err := x.Where("org_id=? AND slug=?", query.OrgId, query.Slug).Find(&dashboards); err != nil { return err From cc55ab6bc82b03a527444d53e3d64354111b2676 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 1 Feb 2018 14:32:19 +0100 Subject: [PATCH 56/84] dashfolders: adds permission modal to dashboard settings --- .../ManageDashboards/FolderPermissions.tsx | 3 +- .../Permissions/DashboardPermissions.tsx | 31 ++++++-- .../Permissions/Permissions.jest.tsx | 73 ------------------- public/sass/components/_gf-form.scss | 2 +- 4 files changed, 28 insertions(+), 81 deletions(-) delete mode 100644 public/app/core/components/Permissions/Permissions.jest.tsx diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 7c9e55bcac3..637f811969e 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -53,8 +53,7 @@ export class FolderPermissions extends Component { onClick={this.handleAddPermission} disabled={permissions.isAddPermissionsVisible} > - - Add Permission + Add Permission
  • diff --git a/public/app/core/components/Permissions/DashboardPermissions.tsx b/public/app/core/components/Permissions/DashboardPermissions.tsx index 2636b0d4db4..a1b86e121bf 100644 --- a/public/app/core/components/Permissions/DashboardPermissions.tsx +++ b/public/app/core/components/Permissions/DashboardPermissions.tsx @@ -1,8 +1,11 @@ import React, { Component } from 'react'; +import { observer } from 'mobx-react'; import { store } from 'app/stores/store'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import AddPermissions from 'app/core/components/Permissions/AddPermissions'; +import SlideDown from 'app/core/components/Animations/SlideDown'; export interface IProps { dashboardId: number; @@ -11,26 +14,44 @@ export interface IProps { folderSlug: string; backendSrv: any; } - +@observer class DashboardPermissions extends Component { permissions: any; constructor(props) { super(props); + this.handleAddPermission = this.handleAddPermission.bind(this); this.permissions = store.permissions; } + handleAddPermission() { + this.permissions.toggleAddPermissions(); + } + render() { const { dashboardId, folderTitle, folderSlug, folderId, backendSrv } = this.props; return (
    -

    Permissions

    - - - +
    +

    Permissions

    + + + +
    + +
    + + + { -// let wrapper; - -// beforeAll(() => { -// backendSrv.get.mockReturnValue( -// Promise.resolve([ -// { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, -// { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, -// { -// id: 4, -// dashboardId: 1, -// userId: 2, -// userLogin: 'danlimerick', -// userEmail: 'dan.limerick@gmail.com', -// permission: 4, -// permissionName: 'Admin', -// }, -// ]) -// ); - -// backendSrv.post = jest.fn(); - -// const store = RootStore.create( -// {}, -// { -// backendSrv: backendSrv, -// } -// ); - -// wrapper = shallow(); -// return wrapper.instance().loadStore(1, true); -// }); - -// describe('when permission for a user is added', () => { -// it('should save permission to db', () => { -// const userItem = { -// id: 2, -// login: 'user2', -// }; - -// wrapper -// .instance() -// .userPicked(userItem) -// .then(() => { -// expect(backendSrv.post.mock.calls.length).toBe(1); -// expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); -// }); -// }); -// }); - -// describe('when permission for team is added', () => { -// it('should save permission to db', () => { -// const teamItem = { -// id: 2, -// name: 'ug1', -// }; - -// wrapper -// .instance() -// .teamPicked(teamItem) -// .then(() => { -// expect(backendSrv.post.mock.calls.length).toBe(1); -// expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); -// }); -// }); -// }); -// }); diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index a1e208ee1c2..6603cfa072b 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -400,7 +400,7 @@ select.gf-form-input ~ .gf-form-help-icon { .cta-form { position: relative; padding: 1rem; - background-color: $dark-4; + background-color: $empty-list-cta-bg; margin-bottom: 1rem; border-top: 3px solid $green; } From a77c6560331451a6e2a66ae902c55e00165d925c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 1 Feb 2018 14:48:11 +0100 Subject: [PATCH 57/84] dashfolders: adds test for permission store --- .../PermissionsStore/PermissionsStore.jest.ts | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts index dcbb1e2a8b5..97a9906d0e5 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts @@ -57,30 +57,30 @@ describe('PermissionsStore', () => { expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl'); }); - // describe('when duplicate team permissions are added', () => { - // beforeEach(() => { - // const newItem = { - // teamId: 10, - // team: 'tester-team', - // permission: 1, - // dashboardId: 1, - // }; - // store.resetNewType(); - // store.newItem.setTeam(newItem.teamId, newItem.team); - // store.newItem.setPermission(newItem.permission); - // store.addStoreItem(); + describe('when duplicate team permissions are added', () => { + beforeEach(() => { + const newItem = { + teamId: 10, + team: 'tester-team', + permission: 1, + dashboardId: 1, + }; + store.resetNewType(); + store.newItem.setTeam(newItem.teamId, newItem.team); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); - // store.newItem.setTeam(newItem.teamId, newItem.team); - // store.newItem.setPermission(newItem.permission); - // store.addStoreItem(); - // }); + store.newItem.setTeam(newItem.teamId, newItem.team); + store.newItem.setPermission(newItem.permission); + store.addStoreItem(); + }); - // it('should return a validation error', () => { - // expect(store.items.length).toBe(4); - // expect(store.error).toBe('This permission exists already.'); - // expect(backendSrv.post.mock.calls.length).toBe(1); - // }); - // }); + it('should return a validation error', () => { + expect(store.items.length).toBe(4); + expect(store.error).toBe('This permission exists already.'); + expect(backendSrv.post.mock.calls.length).toBe(1); + }); + }); describe('when duplicate user permissions are added', () => { beforeEach(() => { From 734a2e59aab3544a2af91d53f8c9d50d901811cf Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 1 Feb 2018 15:23:00 +0100 Subject: [PATCH 58/84] add gofmt as precommit hook --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index d7fb5d8e6f8..acb992a0936 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,10 @@ "*.scss": [ "prettier --write", "git add" + ], + "*.go": [ + "gofmt -w -s", + "git add" ] }, "prettier": { From c3181552f809d18b62fd6277a787037d932d79a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Feb 2018 15:45:15 +0100 Subject: [PATCH 59/84] ux: added max width to dashboard settings views --- public/sass/components/_dashboard_settings.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_dashboard_settings.scss b/public/sass/components/_dashboard_settings.scss index 8f2fd2e0fbb..11d943eb13c 100644 --- a/public/sass/components/_dashboard_settings.scss +++ b/public/sass/components/_dashboard_settings.scss @@ -23,6 +23,7 @@ min-width: 0; height: 100%; padding: 30px; + max-width: 1100px; } .dashboard-settings__aside { From 3db328f516d61caaa3ea7f6a2338395c4624f3de Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Feb 2018 16:30:48 +0100 Subject: [PATCH 60/84] fix for dashboard/folder url's when having a sub path in root_url config --- .../ManageDashboards/FolderPermissions.tsx | 1 + public/app/core/services/bridge_srv.ts | 23 +++++-------------- public/app/core/specs/bridge_srv.jest.ts | 22 ------------------ public/app/core/specs/location_util.jest.ts | 16 +++++++++++++ public/app/core/utils/location_util.ts | 14 +++++++++++ .../dashboard/folder_dashboards_ctrl.ts | 7 ++++-- public/app/features/panel/solo_panel_ctrl.ts | 4 +++- public/app/routes/dashboard_loaders.ts | 12 ++++++---- 8 files changed, 53 insertions(+), 46 deletions(-) delete mode 100644 public/app/core/specs/bridge_srv.jest.ts create mode 100644 public/app/core/specs/location_util.jest.ts create mode 100644 public/app/core/utils/location_util.ts diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index e6e788b7585..07700923b54 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -17,6 +17,7 @@ export class FolderPermissions extends Component { loadStore() { const { nav, folder, view } = this.props; return folder.load(view.routeParams.get('uid') as string).then(res => { + view.updatePathAndQuery(`${res.meta.url}/permissions`, {}, {}); return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions'); }); } diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index ed4abb9b894..4a5649a6c52 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -1,30 +1,18 @@ import coreModule from 'app/core/core_module'; -import config from 'app/core/config'; import appEvents from 'app/core/app_events'; import { store } from 'app/stores/store'; import { reaction } from 'mobx'; +import locationUtil from 'app/core/utils/location_util'; // Services that handles angular -> mobx store sync & other react <-> angular sync export class BridgeSrv { - private appSubUrl; private fullPageReloadRoutes; /** @ngInject */ constructor(private $location, private $timeout, private $window, private $rootScope, private $route) { - this.appSubUrl = config.appSubUrl; this.fullPageReloadRoutes = ['/logout']; } - // Angular's $location does not like and absolute urls - stripBaseFromUrl(url = '') { - const appSubUrl = this.appSubUrl; - const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; - const urlWithoutBase = - url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url; - - return urlWithoutBase; - } - init() { this.$rootScope.$on('$routeUpdate', (evt, data) => { let angularUrl = this.$location.url(); @@ -41,17 +29,18 @@ export class BridgeSrv { () => store.view.currentUrl, currentUrl => { let angularUrl = this.$location.url(); - if (angularUrl !== currentUrl) { + const url = locationUtil.stripBaseFromUrl(currentUrl); + if (angularUrl !== url) { this.$timeout(() => { - this.$location.url(currentUrl); + this.$location.url(url); }); - console.log('store updating angular $location.url', currentUrl); + console.log('store updating angular $location.url', url); } } ); appEvents.on('location-change', payload => { - const urlWithoutBase = this.stripBaseFromUrl(payload.href); + const urlWithoutBase = locationUtil.stripBaseFromUrl(payload.href); if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) { this.$window.location.href = payload.href; return; diff --git a/public/app/core/specs/bridge_srv.jest.ts b/public/app/core/specs/bridge_srv.jest.ts deleted file mode 100644 index 2e32f9bd600..00000000000 --- a/public/app/core/specs/bridge_srv.jest.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BridgeSrv } from 'app/core/services/bridge_srv'; - -jest.mock('app/core/config', () => { - return { - appSubUrl: '/subUrl', - }; -}); - -describe('BridgeSrv', () => { - let searchSrv; - - beforeEach(() => { - searchSrv = new BridgeSrv(null, null, null, null, null); - }); - - describe('With /subUrl as appSubUrl', () => { - it('/subUrl should be stripped', () => { - const urlWithoutMaster = searchSrv.stripBaseFromUrl('/subUrl/grafana/'); - expect(urlWithoutMaster).toBe('/grafana/'); - }); - }); -}); diff --git a/public/app/core/specs/location_util.jest.ts b/public/app/core/specs/location_util.jest.ts new file mode 100644 index 00000000000..c109ab7315a --- /dev/null +++ b/public/app/core/specs/location_util.jest.ts @@ -0,0 +1,16 @@ +import locationUtil from 'app/core/utils/location_util'; + +jest.mock('app/core/config', () => { + return { + appSubUrl: '/subUrl', + }; +}); + +describe('locationUtil', () => { + describe('With /subUrl as appSubUrl', () => { + it('/subUrl should be stripped', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/'); + expect(urlWithoutMaster).toBe('/grafana/'); + }); + }); +}); diff --git a/public/app/core/utils/location_util.ts b/public/app/core/utils/location_util.ts new file mode 100644 index 00000000000..f8d6aa4ee5f --- /dev/null +++ b/public/app/core/utils/location_util.ts @@ -0,0 +1,14 @@ +import config from 'app/core/config'; + +const _stripBaseFromUrl = url => { + const appSubUrl = config.appSubUrl; + const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; + const urlWithoutBase = + url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url; + + return urlWithoutBase; +}; + +export default { + stripBaseFromUrl: _stripBaseFromUrl, +}; diff --git a/public/app/features/dashboard/folder_dashboards_ctrl.ts b/public/app/features/dashboard/folder_dashboards_ctrl.ts index dd0bf7772d8..8ee942445ae 100644 --- a/public/app/features/dashboard/folder_dashboards_ctrl.ts +++ b/public/app/features/dashboard/folder_dashboards_ctrl.ts @@ -1,4 +1,5 @@ import { FolderPageLoader } from './folder_page_loader'; +import locationUtil from 'app/core/utils/location_util'; export class FolderDashboardsCtrl { navModel: any; @@ -13,8 +14,10 @@ export class FolderDashboardsCtrl { const loader = new FolderPageLoader(this.backendSrv); loader.load(this, this.uid, 'manage-folder-dashboards').then(folder => { - if ($location.path() !== folder.meta.url) { - $location.path(folder.meta.url).replace(); + const url = locationUtil.stripBaseFromUrl(folder.meta.url); + + if (url !== $location.path()) { + $location.path(url).replace(); } }); } diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index 575c9e4c3b9..6e1c8a4db65 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -1,4 +1,5 @@ import angular from 'angular'; +import locationUtil from 'app/core/utils/location_util'; export class SoloPanelCtrl { /** @ngInject */ @@ -17,7 +18,8 @@ export class SoloPanelCtrl { if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) { backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => { if (res) { - $location.path(res.meta.url.replace('/d/', '/d-solo/')); + const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); + $location.path(url).replace(); } }); return; diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index 80e74819069..e17ca6fb054 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -1,8 +1,9 @@ import coreModule from 'app/core/core_module'; +import locationUtil from 'app/core/utils/location_util'; export class LoadDashboardCtrl { /** @ngInject */ - constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) { + constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location, $browser) { $scope.appEvent('dashboard-fetch-start'); if (!$routeParams.uid && !$routeParams.slug) { @@ -22,15 +23,18 @@ export class LoadDashboardCtrl { if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) { backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => { if (res) { - $location.path(res.meta.url).replace(); + const url = locationUtil.stripBaseFromUrl(res.meta.url); + $location.path(url).replace(); } }); return; } dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) { - if ($location.path() !== result.meta.url) { - $location.path(result.meta.url).replace(); + const url = locationUtil.stripBaseFromUrl(result.meta.url); + + if (url !== $location.path()) { + $location.path(url).replace(); } if ($routeParams.keepRows) { From 8ced9f646220720f4b40fc70ba6448abb987298e Mon Sep 17 00:00:00 2001 From: Alexandre Rio Date: Thu, 1 Feb 2018 16:41:33 +0100 Subject: [PATCH 61/84] Update search datasource by name API path Correct path for search by name endpoint is /api/datasources/:name --- docs/sources/http_api/data_source.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md index 364b55b0cfc..468a812c709 100644 --- a/docs/sources/http_api/data_source.md +++ b/docs/sources/http_api/data_source.md @@ -90,7 +90,7 @@ Content-Type: application/json ## Get a single data source by Name -`GET /api/datasources/name/:name` +`GET /api/datasources/:name` **Example Request**: From 65bb6cc7287c37b16dc36ee2fe13f641c841ce4a Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 1 Feb 2018 16:53:39 +0100 Subject: [PATCH 62/84] docs: add examples for dashboard permissions --- docs/sources/administration/permissions.md | 44 ++++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md index daecb1807e8..7375d44e6f4 100644 --- a/docs/sources/administration/permissions.md +++ b/docs/sources/administration/permissions.md @@ -55,7 +55,7 @@ This admin flag makes a user a `Super Admin`. This means they can access the `Se {{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} For dashboards and dashboard folders there is a **Permissions** page that make it possible to -remove the default role based permssions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. +remove the default role based permissions for Editors and Viewers. It is here you can add and assign permissions to specific **Users** and **Teams**. You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. @@ -63,12 +63,42 @@ Permission levels: - **Admin**: Can edit & create dashboards and edit permissions. - **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions. -- **View**: Can only view existing dashboars/folders. +- **View**: Can only view existing dashboards/folders. -#### Restricting access +#### Restricting Access -The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the -Access Control List (ACL). +The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the Access Control List (ACL). -- You cannot override permissions for users with **Org Admin Role** -- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. +- You cannot override permissions for users with the **Org Admin Role**. Admins always have access to everything. +- A more specific permission with a lower permission level will not have any effect if a more general rule exists with higher permission level. You need to remove or lower the permission level of the more general rule. + +#### How Grafana Resolves Multiple Permissions - Examples + +##### Example 1 (`user1` has the Editor Role) + +Permissions for a dashboard: + +- `Everyone with Editor Role Can Edit` +- `user1 Can View` + +Result: `user1` has Edit permission as the highest permission always wins. + +##### Example 2 (`user1` has the Viewer Role and is a member of `team1`) + +Permissions for a dashboard: + +- `Everyone with Viewer Role Can View` +- `user1 Can Edit` +- `team1 Can Admin` + +Result: `user1` has Admin permission as the highest permission always wins. + +##### Example 3 + +Permissions for a dashboard: + +- `user1 Can Admin (inherited from parent folder)` +- `user1 Can Edit` + + +Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins. From 744f402a964f272c0e560e046a0f3aaa94bb9883 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Feb 2018 17:27:29 +0100 Subject: [PATCH 63/84] db: fix failing integration tests for mysql and postgresql --- pkg/services/sqlstore/dashboard.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 81dab375188..356cd1ad6c9 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -316,16 +316,19 @@ func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { params = append(params, query.SignedInUser.UserId) params = append(params, query.OrgId) - sql += `WHERE + sql += ` WHERE d.org_id = ? AND - d.is_folder = 1 AND + d.is_folder = ? AND ( - (d.has_acl = 1 AND da.permission > 1 AND (da.user_id = ? OR ugm.user_id = ? OR ou.id IS NOT NULL)) - OR (d.has_acl = 0 AND ouRole.id IS NOT NULL) + (d.has_acl = ? AND da.permission > 1 AND (da.user_id = ? OR ugm.user_id = ? OR ou.id IS NOT NULL)) + OR (d.has_acl = ? AND ouRole.id IS NOT NULL) )` params = append(params, query.OrgId) + params = append(params, dialect.BooleanStr(true)) + params = append(params, dialect.BooleanStr(true)) params = append(params, query.SignedInUser.UserId) params = append(params, query.SignedInUser.UserId) + params = append(params, dialect.BooleanStr(false)) if len(query.Title) > 0 { sql += " AND d.title " + dialect.LikeStr() + " ?" @@ -333,7 +336,6 @@ func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { } sql += ` ORDER BY d.title ASC` - err = x.Sql(sql, params...).Find(&query.Result) } @@ -430,9 +432,9 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery params = append(params, query.OrgId) sql += ` - LEFT JOIN (SELECT 1 AS permission, 'Viewer' AS 'role' - UNION SELECT 2 AS permission, 'Editor' AS 'role' - UNION SELECT 4 AS permission, 'Admin' AS 'role') pt ON ouRole.role = pt.role + LEFT JOIN (SELECT 1 AS permission, 'Viewer' AS role + UNION SELECT 2 AS permission, 'Editor' AS role + UNION SELECT 4 AS permission, 'Admin' AS role) pt ON ouRole.role = pt.role WHERE d.Id IN (?` + strings.Repeat(",?", len(query.DashboardIds)-1) + `) ` for _, id := range query.DashboardIds { @@ -447,13 +449,15 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery ) group by d.id order by d.id asc` - params = append(params, dialect.BooleanStr(true)) params = append(params, query.OrgId) + params = append(params, dialect.BooleanStr(true)) params = append(params, query.UserId) params = append(params, query.UserId) params = append(params, dialect.BooleanStr(false)) + x.ShowSQL(true) err := x.Sql(sql, params...).Find(&query.Result) + x.ShowSQL(false) for _, p := range query.Result { p.PermissionName = p.Permission.String() From d2827a000544fa6e3e8bb341f43ba5743d891719 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 2 Feb 2018 10:18:45 +0100 Subject: [PATCH 64/84] Light theme icon color (#10730) * working on new colors for icons in light theme * more work on icon colors * changed colors of icons for light theme --- public/img/icons_light_theme/icon_add_annotation.svg | 4 ++-- public/img/icons_light_theme/icon_add_annotation_alt.svg | 4 ++-- public/img/icons_light_theme/icon_add_folder.svg | 2 +- public/img/icons_light_theme/icon_add_link.svg | 2 +- .../img/icons_light_theme/icon_add_notification_channel.svg | 2 +- public/img/icons_light_theme/icon_add_panel.svg | 2 +- public/img/icons_light_theme/icon_add_variable.svg | 2 +- public/img/icons_light_theme/icon_alert.svg | 2 +- public/img/icons_light_theme/icon_alert_alt.svg | 2 +- public/img/icons_light_theme/icon_alert_off.svg | 2 +- public/img/icons_light_theme/icon_alert_rules.svg | 2 +- public/img/icons_light_theme/icon_annotation.svg | 2 +- public/img/icons_light_theme/icon_annotation_alt.svg | 2 +- public/img/icons_light_theme/icon_apikeys.svg | 2 +- public/img/icons_light_theme/icon_cog.svg | 2 +- public/img/icons_light_theme/icon_dashboard.svg | 2 +- public/img/icons_light_theme/icon_dashboard_fav.svg | 2 +- public/img/icons_light_theme/icon_dashboard_list.svg | 2 +- public/img/icons_light_theme/icon_data_sources.svg | 2 +- public/img/icons_light_theme/icon_home.svg | 2 +- public/img/icons_light_theme/icon_import_dashboard.svg | 2 +- public/img/icons_light_theme/icon_json.svg | 2 +- public/img/icons_light_theme/icon_link.svg | 2 +- public/img/icons_light_theme/icon_new_dashboard.svg | 2 +- public/img/icons_light_theme/icon_notification_channels.svg | 2 +- public/img/icons_light_theme/icon_org.svg | 2 +- public/img/icons_light_theme/icon_playlist.svg | 2 +- public/img/icons_light_theme/icon_plugins.svg | 2 +- public/img/icons_light_theme/icon_preferences.svg | 2 +- public/img/icons_light_theme/icon_question.svg | 2 +- public/img/icons_light_theme/icon_shield.svg | 2 +- public/img/icons_light_theme/icon_sitemap.svg | 2 +- public/img/icons_light_theme/icon_snapshots.svg | 2 +- public/img/icons_light_theme/icon_team.svg | 2 +- public/img/icons_light_theme/icon_user.svg | 2 +- public/img/icons_light_theme/icon_variable.svg | 2 +- public/img/icons_light_theme/icon_zoom_out.svg | 2 +- public/sass/_variables.light.scss | 3 ++- 38 files changed, 41 insertions(+), 40 deletions(-) diff --git a/public/img/icons_light_theme/icon_add_annotation.svg b/public/img/icons_light_theme/icon_add_annotation.svg index cb306bdd18b..1cf90df2bcb 100644 --- a/public/img/icons_light_theme/icon_add_annotation.svg +++ b/public/img/icons_light_theme/icon_add_annotation.svg @@ -4,7 +4,7 @@ width="64px" height="64px" viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve"> @@ -13,7 +13,7 @@ - + diff --git a/public/img/icons_light_theme/icon_add_annotation_alt.svg b/public/img/icons_light_theme/icon_add_annotation_alt.svg index 2565f188d57..990df3a9d60 100644 --- a/public/img/icons_light_theme/icon_add_annotation_alt.svg +++ b/public/img/icons_light_theme/icon_add_annotation_alt.svg @@ -3,7 +3,7 @@ @@ -14,7 +14,7 @@ - + diff --git a/public/img/icons_light_theme/icon_add_folder.svg b/public/img/icons_light_theme/icon_add_folder.svg index 81bbf349616..a127eeb4dee 100644 --- a/public/img/icons_light_theme/icon_add_folder.svg +++ b/public/img/icons_light_theme/icon_add_folder.svg @@ -3,7 +3,7 @@ diff --git a/public/img/icons_light_theme/icon_add_link.svg b/public/img/icons_light_theme/icon_add_link.svg index df31a3e99a3..cdeb4450bb4 100644 --- a/public/img/icons_light_theme/icon_add_link.svg +++ b/public/img/icons_light_theme/icon_add_link.svg @@ -4,7 +4,7 @@ width="64px" height="64px" viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve"> diff --git a/public/img/icons_light_theme/icon_add_notification_channel.svg b/public/img/icons_light_theme/icon_add_notification_channel.svg index 9059310dcdf..68fad3d13a5 100644 --- a/public/img/icons_light_theme/icon_add_notification_channel.svg +++ b/public/img/icons_light_theme/icon_add_notification_channel.svg @@ -4,7 +4,7 @@ width="64px" height="64px" viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve"> diff --git a/public/img/icons_light_theme/icon_add_panel.svg b/public/img/icons_light_theme/icon_add_panel.svg index c04c6f719e2..e5630d26bb6 100644 --- a/public/img/icons_light_theme/icon_add_panel.svg +++ b/public/img/icons_light_theme/icon_add_panel.svg @@ -3,7 +3,7 @@ diff --git a/public/img/icons_light_theme/icon_add_variable.svg b/public/img/icons_light_theme/icon_add_variable.svg index da50f124e2b..3ae26bf4d9f 100644 --- a/public/img/icons_light_theme/icon_add_variable.svg +++ b/public/img/icons_light_theme/icon_add_variable.svg @@ -3,7 +3,7 @@ diff --git a/public/img/icons_light_theme/icon_alert.svg b/public/img/icons_light_theme/icon_alert.svg index a3109c23fa1..5f8b16acaa2 100644 --- a/public/img/icons_light_theme/icon_alert.svg +++ b/public/img/icons_light_theme/icon_alert.svg @@ -3,7 +3,7 @@ diff --git a/public/img/icons_light_theme/icon_alert_alt.svg b/public/img/icons_light_theme/icon_alert_alt.svg index 0d105481a7d..542004bc4d3 100644 --- a/public/img/icons_light_theme/icon_alert_alt.svg +++ b/public/img/icons_light_theme/icon_alert_alt.svg @@ -3,7 +3,7 @@
    @@ -61,7 +61,7 @@ settings views have been combined with a side nav which allows you to easily mov {{< docs-imagebox img="/img/docs/v50/new_white_theme.png" max-width="1000px" class="docs-image--right" >}} -This theme has not seen a lot of love in recent years and we felt it was time to rework it and give it a major overhaul. We are very happy with the result. +This theme has not seen a lot of love in recent years and we felt it was time to give it a major overhaul. We are very happy with the result.
    @@ -78,7 +78,7 @@ which is very useful if you have a lot of dashboards or multiple teams. ## Teams -A team is a new concept in Grafana v5. They are simply a group of users that can be then be used in the new permission system for dashboards and folders. Only an admin can create teams. +A team is a new concept in Grafana v5. They are simply a group of users that can be used in the new permission system for dashboards and folders. Only an admin can create teams. We hope to do more with teams in future releases like integration with LDAP and a team landing page. ## Permissions @@ -93,7 +93,7 @@ You can assign permissions to folders and dashboards. The default user role-base In previous versions of Grafana, you could only use the API for provisioning data sources and dashboards. But that required the service to be running before you started creating dashboards and you also needed to -set up credentials for the HTTP API. In 5.0 we decided to improve this experience by adding a new active +set up credentials for the HTTP API. In v5.0 we decided to improve this experience by adding a new active provisioning system that uses config files. This will make GitOps more natural as data sources and dashboards can be defined via files that can be version controlled. We hope to extend this system to later add support for users, orgs and alerts as well. From 07fa2f17222fb81b83c96601367d4b8e967be886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 5 Feb 2018 09:42:41 +0100 Subject: [PATCH 75/84] fix: alert list links did not work, changed dashboardUri to Url, this is breaking api change in alert api (#10756) --- docs/sources/http_api/alerting.md | 8 ++++---- pkg/api/alerting.go | 2 +- pkg/api/dtos/alerting.go | 2 +- .../app/containers/AlertRuleList/AlertRuleList.jest.tsx | 2 +- public/app/containers/AlertRuleList/AlertRuleList.tsx | 2 +- public/app/plugins/panel/alertlist/module.html | 2 +- public/app/stores/AlertListStore/AlertListStore.jest.ts | 2 +- public/app/stores/AlertListStore/AlertRule.ts | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md index 221552414e9..3860ae490b1 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -62,7 +62,7 @@ Content-Type: application/json } "newStateDate": "2016-12-25", "executionError": "", - "dashboardUri": "http://grafana.com/dashboard/db/sensors" + "url": "http://grafana.com/dashboard/db/sensors" } ] ``` @@ -94,7 +94,7 @@ Content-Type: application/json "state": "alerting", "newStateDate": "2016-12-25", "executionError": "", - "dashboardUri": "http://grafana.com/dashboard/db/sensors" + "url": "http://grafana.com/dashboard/db/sensors" } ``` @@ -196,7 +196,7 @@ Content-Type: application/json ## Create alert notification -You can find the full list of [supported notifers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page. +You can find the full list of [supported notifers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page. `POST /api/alert-notifications` @@ -294,4 +294,4 @@ Content-Type: application/json { "message": "Notification deleted" } -``` \ No newline at end of file +``` diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 1fc3d893681..16f5f7ceb6f 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -105,7 +105,7 @@ func transformToDTOs(alerts []*models.Alert, c *middleware.Context) ([]*dtos.Ale for _, alert := range alertDTOs { for _, dash := range dashboardsQuery.Result { if alert.DashboardId == dash.Id { - alert.DashbboardUri = dash.GenerateUrl() + alert.Url = dash.GenerateUrl() break } } diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index c32edb6e51c..d30f2697f3f 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -19,7 +19,7 @@ type AlertRule struct { EvalDate time.Time `json:"evalDate"` EvalData *simplejson.Json `json:"evalData"` ExecutionError string `json:"executionError"` - DashbboardUri string `json:"dashboardUri"` + Url string `json:"url"` CanEdit bool `json:"canEdit"` } diff --git a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx b/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx index eaeba48f0a6..eac18a6c69d 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx @@ -23,7 +23,7 @@ describe('AlertRuleList', () => { .format(), evalData: {}, executionError: '', - dashboardUri: 'd/ufkcofof/my-goal', + url: 'd/ufkcofof/my-goal', canEdit: true, }, ]) diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx index 5ce1efd9ee3..6fb6e3b7d8f 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.tsx @@ -137,7 +137,7 @@ export class AlertRuleItem extends React.Component { 'fa-pause': !rule.isPaused, }); - let ruleUrl = `${rule.dashboardUri}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; + let ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; return (
  • diff --git a/public/app/plugins/panel/alertlist/module.html b/public/app/plugins/panel/alertlist/module.html index e795c4fa59f..b8055b5fc7f 100644 --- a/public/app/plugins/panel/alertlist/module.html +++ b/public/app/plugins/panel/alertlist/module.html @@ -12,7 +12,7 @@

    - + {{alert.name}}

    diff --git a/public/app/stores/AlertListStore/AlertListStore.jest.ts b/public/app/stores/AlertListStore/AlertListStore.jest.ts index 9092ed26df2..f0ab24b6cfc 100644 --- a/public/app/stores/AlertListStore/AlertListStore.jest.ts +++ b/public/app/stores/AlertListStore/AlertListStore.jest.ts @@ -14,7 +14,7 @@ function getRule(name, state, info) { .format(), evalData: {}, executionError: '', - dashboardUri: 'db/mygool', + url: 'db/mygool', stateText: state, stateIcon: 'fa', stateClass: 'asd', diff --git a/public/app/stores/AlertListStore/AlertRule.ts b/public/app/stores/AlertListStore/AlertRule.ts index 2933daeb7c5..00881650949 100644 --- a/public/app/stores/AlertListStore/AlertRule.ts +++ b/public/app/stores/AlertListStore/AlertRule.ts @@ -13,7 +13,7 @@ export const AlertRule = types stateClass: types.string, stateAge: types.string, info: types.optional(types.string, ''), - dashboardUri: types.string, + url: types.string, canEdit: types.boolean, }) .views(self => ({ From 0701188e64474167724467d07ec9eee99642f69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 5 Feb 2018 09:44:36 +0100 Subject: [PATCH 76/84] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7dd13dba98..2dacbb716e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when * **Pagerduty** The notifier now defaults to not auto resolve incidents. More details at [#10222](https://github.com/grafana/grafana/issues/10222) +* **HTTP API** +- `GET /api/alerts` property dashboardUri renamed to url and is now the full url (that is including app sub url). + ## New Dashboard Grid The new grid engine is a major upgrade for how you can position and move panels. It enables new layouts and a much easier dashboard building experience. The change is backward compatible. So you can upgrade your current version to 5.0 without breaking dashboards, but you cannot downgrade from 5.0 to previous versions. Grafana will automatically upgrade your dashboards to the new schema and position panels to match your existing layout. There might be minor differences in panel height. If you upgrade to 5.0 and for some reason want to rollback to the previous version you can restore dashboards to previous versions using dashboard history. But that should only be seen as an emergency solution. From cc0cc8dd735d54924510f690f56ae56942d6ad24 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Mon, 5 Feb 2018 10:24:48 +0100 Subject: [PATCH 77/84] changes to new urlformat for home dashboard (#10738) --- pkg/api/dashboard.go | 5 +++-- pkg/models/dashboards.go | 2 +- pkg/services/alerting/eval_context.go | 2 +- pkg/services/sqlstore/dashboard.go | 2 +- public/app/routes/dashboard_loaders.ts | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 0267e2097eb..4478a5af55e 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -293,10 +293,11 @@ func GetHomeDashboard(c *middleware.Context) Response { } if prefsQuery.Result.HomeDashboardId != 0 { - slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId} + slugQuery := m.GetDashboardRefByIdQuery{Id: prefsQuery.Result.HomeDashboardId} err := bus.Dispatch(&slugQuery) if err == nil { - dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result} + url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug) + dashRedirect := dtos.DashboardRedirect{RedirectUri: url} return Json(200, &dashRedirect) } else { log.Warn("Failed to get slug from database, %s", err.Error()) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 089a98c4f00..12216718b44 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -293,7 +293,7 @@ type DashboardRef struct { Slug string } -type GetDashboardUIDByIdQuery struct { +type GetDashboardRefByIdQuery struct { Id int64 Result *DashboardRef } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 60c5530d486..d598203d675 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -90,7 +90,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) { return c.dashboardRef, nil } - uidQuery := &m.GetDashboardUIDByIdQuery{Id: c.Rule.DashboardId} + uidQuery := &m.GetDashboardRefByIdQuery{Id: c.Rule.DashboardId} if err := bus.Dispatch(uidQuery); err != nil { return nil, err } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 16270b2ce1d..2e2537cacb9 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -569,7 +569,7 @@ func GetDashboardsBySlug(query *m.GetDashboardsBySlugQuery) error { return nil } -func GetDashboardUIDById(query *m.GetDashboardUIDByIdQuery) error { +func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error { var rawSql = `SELECT uid, slug from dashboard WHERE Id=?` us := &m.DashboardRef{} diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index e17ca6fb054..7d3099a54fd 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -9,7 +9,7 @@ export class LoadDashboardCtrl { if (!$routeParams.uid && !$routeParams.slug) { backendSrv.get('/api/dashboards/home').then(function(homeDash) { if (homeDash.redirectUri) { - $location.path('dashboard/' + homeDash.redirectUri); + $location.path(homeDash.redirectUri); } else { var meta = homeDash.meta; meta.canSave = meta.canShare = meta.canStar = false; From 6def21e83fc4334e03feae1756db03ce6d3abe24 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 5 Feb 2018 10:31:53 +0100 Subject: [PATCH 78/84] dashboard: fix loading of snapshot and scripted dashboard (#10755) Fixes #10753 --- public/app/routes/dashboard_loaders.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index 7d3099a54fd..a9ccd60ee4f 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -31,10 +31,12 @@ export class LoadDashboardCtrl { } dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) { - const url = locationUtil.stripBaseFromUrl(result.meta.url); + if (result.meta.url) { + const url = locationUtil.stripBaseFromUrl(result.meta.url); - if (url !== $location.path()) { - $location.path(url).replace(); + if (url !== $location.path()) { + $location.path(url).replace(); + } } if ($routeParams.keepRows) { From 7d3b990e917200ba7f53847cbc38423b22b6acd0 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 5 Feb 2018 11:09:05 +0100 Subject: [PATCH 79/84] permissions: fix link to folder from permissions list Also, closing Add Permissions CTA when DashboardPermissions and FolderPermissions unmounts. Fixes #10749" --- pkg/api/dashboard.go | 2 +- pkg/api/dtos/dashboard.go | 2 +- .../ManageDashboards/FolderPermissions.tsx | 5 +++++ public/app/core/angular_wrappers.ts | 8 +------- .../components/Permissions/DashboardPermissions.tsx | 13 ++++++++----- .../app/core/components/Permissions/FolderInfo.ts | 4 ++-- .../components/Permissions/PermissionsListItem.tsx | 2 +- .../app/features/dashboard/settings/settings.html | 4 +--- public/app/features/dashboard/settings/settings.ts | 8 ++++++++ 9 files changed, 28 insertions(+), 20 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 0267e2097eb..439f04f0799 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -99,7 +99,7 @@ func GetDashboard(c *middleware.Context) Response { return ApiError(500, "Dashboard folder could not be read", err) } meta.FolderTitle = query.Result.Title - meta.FolderSlug = query.Result.Slug + meta.FolderUrl = query.Result.GetUrl() } // make sure db version is in sync with json model version diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index 44fc68e4659..e4c66aebbda 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -27,7 +27,7 @@ type DashboardMeta struct { IsFolder bool `json:"isFolder"` FolderId int64 `json:"folderId"` FolderTitle string `json:"folderTitle"` - FolderSlug string `json:"folderSlug"` + FolderUrl string `json:"folderUrl"` } type DashboardFullWithMeta struct { diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index 93b9520739e..a62165d7194 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -17,6 +17,11 @@ export class FolderPermissions extends Component { this.loadStore(); } + componentWillUnmount() { + const { permissions } = this.props; + permissions.hideAddPermissions(); + } + loadStore() { const { nav, folder, view } = this.props; return folder.load(view.routeParams.get('uid') as string).then(res => { diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index bc3fb08d822..ace0eb00b07 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -20,11 +20,5 @@ export function registerAngularDirectives() { ['tagOptions', { watchDepth: 'reference' }], ]); react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'handlePicked']); - react2AngularDirective('dashboardPermissions', DashboardPermissions, [ - 'backendSrv', - 'dashboardId', - 'folderTitle', - 'folderSlug', - 'folderId', - ]); + react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']); } diff --git a/public/app/core/components/Permissions/DashboardPermissions.tsx b/public/app/core/components/Permissions/DashboardPermissions.tsx index a1b86e121bf..d6b293da4bb 100644 --- a/public/app/core/components/Permissions/DashboardPermissions.tsx +++ b/public/app/core/components/Permissions/DashboardPermissions.tsx @@ -6,12 +6,11 @@ import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; +import { FolderInfo } from './FolderInfo'; export interface IProps { dashboardId: number; - folderId: number; - folderTitle: string; - folderSlug: string; + folder?: FolderInfo; backendSrv: any; } @observer @@ -28,8 +27,12 @@ class DashboardPermissions extends Component { this.permissions.toggleAddPermissions(); } + componentWillUnmount() { + this.permissions.hideAddPermissions(); + } + render() { - const { dashboardId, folderTitle, folderSlug, folderId, backendSrv } = this.props; + const { dashboardId, folder, backendSrv } = this.props; return (
    @@ -56,7 +59,7 @@ class DashboardPermissions extends Component { permissions={this.permissions} isFolder={false} dashboardId={dashboardId} - folderInfo={{ title: folderTitle, slug: folderSlug, id: folderId }} + folderInfo={folder} backendSrv={backendSrv} />
    diff --git a/public/app/core/components/Permissions/FolderInfo.ts b/public/app/core/components/Permissions/FolderInfo.ts index 67ebb753df0..d4a6020bb71 100644 --- a/public/app/core/components/Permissions/FolderInfo.ts +++ b/public/app/core/components/Permissions/FolderInfo.ts @@ -1,5 +1,5 @@ export interface FolderInfo { - title: string; id: number; - slug: string; + title: string; + url: string; } diff --git a/public/app/core/components/Permissions/PermissionsListItem.tsx b/public/app/core/components/Permissions/PermissionsListItem.tsx index 291ee20e157..3140b8fcc0c 100644 --- a/public/app/core/components/Permissions/PermissionsListItem.tsx +++ b/public/app/core/components/Permissions/PermissionsListItem.tsx @@ -30,7 +30,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde folderInfo && ( Inherited from folder{' '} -
    + {folderInfo.title} {' '} diff --git a/public/app/features/dashboard/settings/settings.html b/public/app/features/dashboard/settings/settings.html index 47c3bae2ef6..af9358aecf7 100644 --- a/public/app/features/dashboard/settings/settings.html +++ b/public/app/features/dashboard/settings/settings.html @@ -99,9 +99,7 @@
    diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index c538f171fa0..09aa96e42ae 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -197,6 +197,14 @@ export class SettingsCtrl { this.dashboard.meta.folderTitle = folder.title; this.dashboard.meta.folderSlug = folder.slug; } + + getFolder() { + return { + id: this.dashboard.meta.folderId, + title: this.dashboard.meta.folderTitle, + url: this.dashboard.meta.folderUrl, + }; + } } export function dashboardSettings() { From 20feb123c9009450791f2d39215de873ff26829c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 5 Feb 2018 13:49:59 +0300 Subject: [PATCH 80/84] fix panel menu caret placement (#10759) --- public/sass/components/_dropdown.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_dropdown.scss b/public/sass/components/_dropdown.scss index d91a6d0a5a7..cc94a379e07 100644 --- a/public/sass/components/_dropdown.scss +++ b/public/sass/components/_dropdown.scss @@ -255,7 +255,7 @@ } // Caret to indicate there is a submenu -.dropdown-submenu > a::before { +.dropdown-submenu > a::after { display: block; content: ' '; float: right; From a879dd8c0c391bf8edea48d797340d8eb8979378 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 5 Feb 2018 13:23:24 +0100 Subject: [PATCH 81/84] dashboards: render correct link for folder when searching for dashboards (#10763) Fixes #10761 --- pkg/services/search/models.go | 3 ++- pkg/services/sqlstore/dashboard.go | 7 ++++++- pkg/services/sqlstore/dashboard_test.go | 5 +++++ pkg/services/sqlstore/search_builder.go | 1 + public/app/core/services/search_srv.ts | 4 ++-- public/app/core/specs/manage_dashboards.jest.ts | 11 +++-------- public/app/core/specs/search_srv.jest.ts | 7 +++++++ 7 files changed, 26 insertions(+), 12 deletions(-) diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index f5d87b1f5c8..6dea975d9fe 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -21,8 +21,9 @@ type Hit struct { Tags []string `json:"tags"` IsStarred bool `json:"isStarred"` FolderId int64 `json:"folderId,omitempty"` + FolderUid string `json:"folderUid,omitempty"` FolderTitle string `json:"folderTitle,omitempty"` - FolderSlug string `json:"folderSlug,omitempty"` + FolderUrl string `json:"folderUrl,omitempty"` } type HitList []*Hit diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 2e2537cacb9..af87c324216 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -245,6 +245,7 @@ type DashboardSearchProjection struct { Term string IsFolder bool FolderId int64 + FolderUid string FolderSlug string FolderTitle string } @@ -323,11 +324,15 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard Url: m.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug), Type: getHitType(item), FolderId: item.FolderId, + FolderUid: item.FolderUid, FolderTitle: item.FolderTitle, - FolderSlug: item.FolderSlug, Tags: []string{}, } + if item.FolderId > 0 { + hit.FolderUrl = m.GetFolderUrl(item.FolderUid, item.FolderSlug) + } + query.Result = append(query.Result, hit) hits[item.Id] = hit } diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 0a9a97dbe7a..97ab5472d94 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -147,6 +147,7 @@ func TestDashboardDataAccess(t *testing.T) { hit := query.Result[0] So(hit.Type, ShouldEqual, search.DashHitFolder) So(hit.Url, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) + So(hit.FolderTitle, ShouldEqual, "") }) Convey("Should be able to search for a dashboard folder's children", func() { @@ -163,6 +164,10 @@ func TestDashboardDataAccess(t *testing.T) { hit := query.Result[0] So(hit.Id, ShouldEqual, savedDash.Id) So(hit.Url, ShouldEqual, fmt.Sprintf("/d/%s/%s", savedDash.Uid, savedDash.Slug)) + So(hit.FolderId, ShouldEqual, savedFolder.Id) + So(hit.FolderUid, ShouldEqual, savedFolder.Uid) + So(hit.FolderTitle, ShouldEqual, savedFolder.Title) + So(hit.FolderUrl, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) }) Convey("Should be able to search for dashboard by dashboard ids", func() { diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go index 480e7fa99e6..627074d5453 100644 --- a/pkg/services/sqlstore/search_builder.go +++ b/pkg/services/sqlstore/search_builder.go @@ -107,6 +107,7 @@ func (sb *SearchBuilder) buildSelect() { dashboard_tag.term, dashboard.is_folder, dashboard.folder_id, + folder.uid as folder_uid, folder.slug as folder_slug, folder.title as folder_title FROM `) diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index 7bee5cdfec6..9f32e21f3f6 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -150,9 +150,9 @@ export class SearchSrv { if (hit.folderId) { section = { id: hit.folderId, - uid: hit.uid, + uid: hit.folderUid, title: hit.folderTitle, - url: hit.url, + url: hit.folderUrl, items: [], icon: 'fa fa-folder-open', toggle: this.toggleFolder.bind(this), diff --git a/public/app/core/specs/manage_dashboards.jest.ts b/public/app/core/specs/manage_dashboards.jest.ts index 8c640bc0630..4b51e6848d0 100644 --- a/public/app/core/specs/manage_dashboards.jest.ts +++ b/public/app/core/specs/manage_dashboards.jest.ts @@ -20,9 +20,6 @@ describe('ManageDashboards', () => { icon: 'fa fa-folder', tags: [], isStarred: false, - folderId: 410, - folderTitle: 'afolder', - folderSlug: 'afolder', }, ], tags: [], @@ -77,9 +74,6 @@ describe('ManageDashboards', () => { icon: 'fa fa-folder', tags: [], isStarred: false, - folderId: 410, - folderTitle: 'afolder', - folderSlug: 'afolder', }, ], tags: [], @@ -112,8 +106,9 @@ describe('ManageDashboards', () => { tags: [], isStarred: false, folderId: 410, - folderTitle: 'afolder', - folderSlug: 'afolder', + folderUid: 'uid', + folderTitle: 'Folder', + folderUrl: '/dashboards/f/uid/folder', }, { id: 500, diff --git a/public/app/core/specs/search_srv.jest.ts b/public/app/core/specs/search_srv.jest.ts index 444939d1f4a..550d11bbf9e 100644 --- a/public/app/core/specs/search_srv.jest.ts +++ b/public/app/core/specs/search_srv.jest.ts @@ -190,7 +190,9 @@ describe('SearchSrv', () => { title: 'dash in folder1 1', type: 'dash-db', folderId: 1, + folderUid: 'uid', folderTitle: 'folder1', + folderUrl: '/dashboards/f/uid/folder1', }, ]) ); @@ -206,6 +208,11 @@ describe('SearchSrv', () => { it('should group results by folder', () => { expect(results).toHaveLength(2); + expect(results[0].id).toEqual(0); + expect(results[1].id).toEqual(1); + expect(results[1].uid).toEqual('uid'); + expect(results[1].title).toEqual('folder1'); + expect(results[1].url).toEqual('/dashboards/f/uid/folder1'); }); }); From 8ce2074eb20a84f52303deacb220702925c23748 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 5 Feb 2018 14:05:06 +0100 Subject: [PATCH 82/84] docs: fix links in HTTP API Reference page Fixes #10745 --- docs/sources/http_api/index.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/sources/http_api/index.md b/docs/sources/http_api/index.md index 9c5d0e7d49a..cbfe004b14c 100644 --- a/docs/sources/http_api/index.md +++ b/docs/sources/http_api/index.md @@ -18,12 +18,15 @@ dashboards, creating users and updating data sources. ## Supported HTTP APIs: -* [Authentication API]({{< relref "auth.md" >}}) -* [Dashboard API]({{< relref "dashboard.md" >}}) -* [Data Source API]({{< relref "data_source.md" >}}) -* [Organisation API]({{< relref "org.md" >}}) -* [User API]({{< relref "user.md" >}}) -* [Admin API]({{< relref "admin.md" >}}) -* [Snapshot API]({{< relref "snapshot.md" >}}) -* [Preferences API]({{< relref "preferences.md" >}}) -* [Other API]({{< relref "other.md" >}}) +* [Authentication API]({{< relref "/http_api/auth.md" >}}) +* [Dashboard API]({{< relref "/http_api/dashboard.md" >}}) +* [Dashboard Versions API]({{< relref "http_api/dashboard_versions.md" >}}) +* [Data Source API]({{< relref "http_api/data_source.md" >}}) +* [Organisation API]({{< relref "http_api/org.md" >}}) +* [Snapshot API]({{< relref "http_api/snapshot.md" >}}) +* [Annotations API]({{< relref "http_api/annotations.md" >}}) +* [Alerting API]({{< relref "http_api/alerting.md" >}}) +* [User API]({{< relref "http_api/user.md" >}}) +* [Admin API]({{< relref "http_api/admin.md" >}}) +* [Preferences API]({{< relref "http_api/preferences.md" >}}) +* [Other API]({{< relref "http_api/other.md" >}}) From 04a94ce39638573d311750549af589ab071c8317 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Mon, 5 Feb 2018 14:11:19 +0100 Subject: [PATCH 83/84] adds unique index for org_id+folder_id+title on dashboards (#10766) --- pkg/services/sqlstore/migrations/dashboard_mig.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index edca733e174..bb2aea4bd51 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -167,4 +167,8 @@ func addDashboardMigration(mg *Migrator) { mg.AddMigration("Remove unique index org_id_slug", NewDropIndexMigration(dashboardV2, &Index{ Cols: []string{"org_id", "slug"}, Type: UniqueIndex, })) + + mg.AddMigration("Add unique index for dashboard_org_id_title_folder_id", NewAddIndexMigration(dashboardV2, &Index{ + Cols: []string{"org_id", "folder_id", "title"}, Type: UniqueIndex, + })) } From 2d1bd270fbe3965afab589fec897a84237c537ec Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 5 Feb 2018 13:28:24 +0000 Subject: [PATCH 84/84] Stale permissions (#10768) * dashfolders: hide permissions in settings if folder has changed and the dashboard has not been saved yet. Otherwise the use will see stale permissions from the original folder. * dashfolders: return folder url for inherited permissions --- pkg/api/dashboard_acl.go | 6 ++++ pkg/models/dashboard_acl.go | 5 +++ pkg/services/sqlstore/dashboard_acl.go | 32 +++++++++++++++---- .../ManageDashboards/FolderPermissions.tsx | 2 +- .../Permissions/AddPermissions.jest.tsx | 2 +- .../components/Permissions/AddPermissions.tsx | 7 ---- .../Permissions/DashboardPermissions.tsx | 2 +- .../features/dashboard/settings/settings.html | 5 ++- .../features/dashboard/settings/settings.ts | 8 ++++- 9 files changed, 51 insertions(+), 18 deletions(-) diff --git a/pkg/api/dashboard_acl.go b/pkg/api/dashboard_acl.go index 6eb11047723..b5d912d25f1 100644 --- a/pkg/api/dashboard_acl.go +++ b/pkg/api/dashboard_acl.go @@ -24,6 +24,12 @@ func GetDashboardAclList(c *middleware.Context) Response { return ApiError(500, "Failed to get dashboard acl", err) } + for _, perm := range acl { + if perm.Slug != "" { + perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug) + } + } + return Json(200, acl) } diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go index fa7ad00de7f..933487650e3 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/models/dashboard_acl.go @@ -59,6 +59,11 @@ type DashboardAclInfoDTO struct { Role *RoleType `json:"role,omitempty"` Permission PermissionType `json:"permission"` PermissionName string `json:"permissionName"` + Uid string `json:"uid"` + Title string `json:"title"` + Slug string `json:"slug"` + IsFolder bool `json:"isFolder"` + Url string `json:"url"` } // diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index 9027f74f33a..829182a8195 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -113,6 +113,7 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { }) } +// RemoveDashboardAcl removes a specified permission from the dashboard acl func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error { return inTransaction(func(sess *DBSession) error { var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE org_id =? and id=?" @@ -125,6 +126,11 @@ func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error { }) } +// GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three +// different places. +// 1) Permissions for the dashboard +// 2) permissions for its parent folder +// 3) if no specific permissions have been set for the dashboard or its parent folder then get the default permissions func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { var err error @@ -141,7 +147,11 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { da.updated, '' as user_login, '' as user_email, - '' as team + '' as team, + '' as title, + '' as slug, + '' as uid,` + + dialect.BooleanStr(false) + ` AS is_folder FROM dashboard_acl as da WHERE da.dashboard_id = -1` query.Result = make([]*m.DashboardAclInfoDTO, 0) @@ -155,6 +165,7 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { )`, query.DashboardId, query.DashboardId) rawSQL := ` + -- get permissions for the dashboard and its parent folder SELECT da.id, da.org_id, @@ -167,13 +178,18 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { da.updated, u.login AS user_login, u.email AS user_email, - ug.name AS team + ug.name AS team, + d.title, + d.slug, + d.uid, + d.is_folder FROM` + dialect.Quote("dashboard_acl") + ` as da LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id LEFT OUTER JOIN team ug on ug.id = da.team_id + LEFT OUTER JOIN dashboard d on da.dashboard_id = d.id WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ? - -- Also include default permission if has_acl = 0 + -- Also include default permissions if folder or dashboard field "has_acl" is false UNION SELECT @@ -188,10 +204,14 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { da.updated, '' as user_login, '' as user_email, - '' as team - FROM dashboard_acl as da, + '' as team, + folder.title, + folder.slug, + folder.uid, + folder.is_folder + FROM dashboard_acl as da, dashboard as dash - LEFT JOIN dashboard folder on dash.folder_id = folder.id + LEFT OUTER JOIN dashboard folder on dash.folder_id = folder.id WHERE dash.id = ? AND ( dash.has_acl = ` + dialect.BooleanStr(false) + ` or diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index a62165d7194..500072c1da0 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -63,7 +63,7 @@ export class FolderPermissions extends Component {
    - +
  • diff --git a/public/app/core/components/Permissions/AddPermissions.jest.tsx b/public/app/core/components/Permissions/AddPermissions.jest.tsx index 98a00a8bd7b..48ff20a16aa 100644 --- a/public/app/core/components/Permissions/AddPermissions.jest.tsx +++ b/public/app/core/components/Permissions/AddPermissions.jest.tsx @@ -26,7 +26,7 @@ describe('AddPermissions', () => { } ); - wrapper = shallow(); + wrapper = shallow(); instance = wrapper.instance(); return store.permissions.load(1, true, false); }); diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx index 3ce4360ff86..94afa7c1180 100644 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -9,7 +9,6 @@ import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore' export interface IProps { permissions: any; backendSrv: any; - dashboardId: any; } @observer class AddPermissions extends Component { @@ -31,12 +30,6 @@ class AddPermissions extends Component { const { value } = evt.target; const { permissions } = this.props; - // if (value === 'Viewer' || value === 'Editor') { - // // permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId); - // // this.resetNewType(); - // return; - // } - permissions.setNewType(value); } diff --git a/public/app/core/components/Permissions/DashboardPermissions.tsx b/public/app/core/components/Permissions/DashboardPermissions.tsx index d6b293da4bb..12339cc7c34 100644 --- a/public/app/core/components/Permissions/DashboardPermissions.tsx +++ b/public/app/core/components/Permissions/DashboardPermissions.tsx @@ -53,7 +53,7 @@ class DashboardPermissions extends Component {
    - +
    - +
    +
    You have changed folder, please save to view permissions.
    +
    diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 09aa96e42ae..e9d5c6180be 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -14,6 +14,7 @@ export class SettingsCtrl { canSave: boolean; canDelete: boolean; sections: any[]; + hasUnsavedFolderChange: boolean; /** @ngInject */ constructor(private $scope, private $location, private $rootScope, private backendSrv, private dashboardSrv) { @@ -38,6 +39,7 @@ export class SettingsCtrl { this.$rootScope.onAppEvent('$routeUpdate', this.onRouteUpdated.bind(this), $scope); this.$rootScope.appEvent('dash-scroll', { animate: false, pos: 0 }); + this.$rootScope.onAppEvent('dashboard-saved', this.onPostSave.bind(this), $scope); } buildSectionList() { @@ -135,6 +137,10 @@ export class SettingsCtrl { this.dashboardSrv.saveDashboard(); } + onPostSave() { + this.hasUnsavedFolderChange = false; + } + hideSettings() { var urlParams = this.$location.search(); delete urlParams.editview; @@ -195,7 +201,7 @@ export class SettingsCtrl { onFolderChange(folder) { this.dashboard.meta.folderId = folder.id; this.dashboard.meta.folderTitle = folder.title; - this.dashboard.meta.folderSlug = folder.slug; + this.hasUnsavedFolderChange = true; } getFolder() {