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.
+
+
-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.
diff --git a/package.json b/package.json
index 80fa9a699f8..acb992a0936 100644
--- a/package.json
+++ b/package.json
@@ -115,6 +115,10 @@
"*.scss": [
"prettier --write",
"git add"
+ ],
+ "*.go": [
+ "gofmt -w -s",
+ "git add"
]
},
"prettier": {
@@ -153,6 +157,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/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")
+ })
+ })
+}
diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go
index fd07ccd5b2c..16270b2ce1d 100644
--- a/pkg/services/sqlstore/dashboard.go
+++ b/pkg/services/sqlstore/dashboard.go
@@ -374,16 +374,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() + " ?"
@@ -391,7 +394,6 @@ func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error {
}
sql += ` ORDER BY d.title ASC`
-
err = x.Sql(sql, params...).Find(&query.Result)
}
@@ -488,9 +490,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 {
@@ -505,13 +507,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()
diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx
index 07700923b54..93b9520739e 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';
+import SlideDown from 'app/core/components/Animations/SlideDown';
@inject('nav', 'folder', 'view', 'permissions')
@observer
export class FolderPermissions extends Component