diff --git a/.gitignore b/.gitignore index 0e6affc6120..cfa3a68c057 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ profile.cov /examples/*/dist /packaging/**/*.rpm /packaging/**/*.deb + +# Ignore OSX indexing +.DS_Store diff --git a/docs/sources/features/datasources/influxdb.md b/docs/sources/features/datasources/influxdb.md index 3f86b9ee703..8f4f5981124 100644 --- a/docs/sources/features/datasources/influxdb.md +++ b/docs/sources/features/datasources/influxdb.md @@ -88,7 +88,7 @@ You can switch to raw query mode by clicking hamburger icon and then `Switch edi - $m = replaced with measurement name - $measurement = replaced with measurement name - $col = replaced with column name -- $tag_exampletag = replaced with the value of the `exampletag` tag. To use your tag as an alias in the ALIAS BY field then the tag must be used to group by in the query. +- $tag_exampletag = replaced with the value of the `exampletag` tag. The syntax is `$tag_yourTagName` (must start with `$tag_`). To use your tag as an alias in the ALIAS BY field then the tag must be used to group by in the query. - You can also use [[tag_hostname]] pattern replacement syntax. For example, in the ALIAS BY field using this text `Host: [[tag_hostname]]` would substitute in the `hostname` tag value for each legend value and an example legend value would be: `Host: server1`. ### Table query / raw data diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 2072e6a4f7b..02e821050fa 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -29,8 +29,7 @@ data from a MySQL compatible database. The database user you specify when you add the data source should only be granted SELECT permissions on the specified database & tables you want to query. Grafana does not validate that the query is safe. The query could include any SQL statement. For example, statements like `USE otherdb;` and `DROP TABLE user;` would be -executed. To protect against this we **Highly** recommmend you create a specific mysql user with -restricted permissions. +executed. To protect against this we **Highly** recommmend you create a specific mysql user with restricted permissions. Example: @@ -49,11 +48,9 @@ Macro example | Description ------------ | ------------- *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > FROM_UNIXTIME(1494410783) AND dateColumn < FROM_UNIXTIME(1494497183)* -We plan to add many more macros. If you have suggestions for what macros you would like to see, please -[open an issue](https://github.com/grafana/grafana) in our GitHub repo. +We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo. -The query editor has a link named `Generated SQL` that show up after a query as been executed, while in panel edit mode. Click -on it and it will expand and show the raw interpolated SQL string that was executed. +The query editor has a link named `Generated SQL` that show up after a query as been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed. ## Table queries @@ -109,8 +106,71 @@ This is something we plan to add. ## Templating -You can use variables in your queries but there are currently no support for defining `Query` variables -that target a MySQL data source. +This feature is currently available in the nightly builds and will be included in the 5.0.0 release. + +Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns makes it easy to change the data being displayed in your dashboard. + +Checkout the [Templating]({{< relref "reference/templating.md" >}}) documentation for an introduction to the templating feature and the different types of template variables. + +### Query Variable + +If you add a template variable of the type `Query`, you can write a MySQL query that can +return things like measurement names, key names or key values that are shown as a dropdown select box. + +For example, you can have a variable that contains all values for the `hostname` column in a table if you specify a query like this in the templating variable *Query* setting. + +```sql +SELECT hostname FROM my_host +``` + +A query can returns multiple columns and Grafana will automatically create a list from them. For example, the query below will return a list with values from `hostname` and `hostname2`. + +```sql +SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city +``` + +Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value: + +```sql +SELECT hostname AS __text, id AS __value FROM my_host +``` + +You can also create nested variables. For example if you had another variable named `region`. Then you could have +the hosts variable only show hosts from the current selected region with a query like this (if `region` is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values): + +```sql +SELECT hostname FROM my_host WHERE region IN($region) +``` + +### Using Variables in Queries + +Template variables are quoted automatically so if it is a string value do not wrap them in quotes in where clauses. If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values. + +There are two syntaxes: + +`$` Example with a template variable named `hostname`: + +```sql +SELECT + UNIX_TIMESTAMP(atimestamp) as time_sec, + aint as value, + avarchar as metric +FROM my_table +WHERE $__timeFilter(atimestamp) and hostname in($hostname) +ORDER BY atimestamp ASC +``` + +`[[varname]]` Example with a template variable named `hostname`: + +```sql +SELECT + UNIX_TIMESTAMP(atimestamp) as time_sec, + aint as value, + avarchar as metric +FROM my_table +WHERE $__timeFilter(atimestamp) and hostname in([[hostname]]) +ORDER BY atimestamp ASC +``` ## Alerting diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md index fa20fd3e9b3..761ac938cd8 100644 --- a/docs/sources/http_api/user.md +++ b/docs/sources/http_api/user.md @@ -20,9 +20,9 @@ parent = "http_api" GET /api/users HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= -Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. +Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: @@ -55,10 +55,12 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter GET /api/users/search?perpage=10&page=1&query=mygraf HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. The `totalCount` field in the response can be used for pagination of the user list E.g. if `totalCount` is equal to 100 users and the `perpage` parameter is set to 10 then there are 10 pages of users. The `query` parameter is optional and it will return results where the query value is contained in one of the `name`, `login` or `email` fields. Query values with spaces need to be url encoded e.g. `query=Jane%20Doe`. +Requires basic authentication and that the authenticated user is a Grafana Admin. + **Example Response**: HTTP/1.1 200 @@ -94,7 +96,9 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter GET /api/users/1 HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= + +Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: @@ -126,7 +130,9 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter GET /api/users/lookup?loginOrEmail=admin HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= + +Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: @@ -152,7 +158,7 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter PUT /api/users/2 HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= { "email":"user@mygraf.com", @@ -161,6 +167,8 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter "theme":"light" } +Requires basic authentication and that the authenticated user is a Grafana Admin. + **Example Response**: HTTP/1.1 200 @@ -178,7 +186,9 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter GET /api/users/1/orgs HTTP/1.1 Accept: application/json Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + Authorization: Basic YWRtaW46YWRtaW4= + +Requires basic authentication and that the authenticated user is a Grafana Admin. **Example Response**: @@ -246,11 +256,29 @@ Changes the password for the user {"message":"User password changed"} -## Switch user context +## Switch user context for a specified user -`POST /api/user/using/:organisationId` +`POST /api/users/:userId/using/:organizationId` -Switch user context to the given organisation. +Switch user context to the given organization. Requires basic authentication and that the authenticated user is a Grafana Admin. + +**Example Request**: + + POST /api/users/7/using/2 HTTP/1.1 + Authorization: Basic YWRtaW46YWRtaW4= + +**Example Response**: + + HTTP/1.1 200 + Content-Type: application/json + + {"message":"Active organization changed"} + +## Switch user context for signed in user + +`POST /api/user/using/:organizationId` + +Switch user context to the given organization. **Example Request**: diff --git a/package.json b/package.json index ab23d652f56..323aa5d70b2 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "eventemitter3": "^2.0.2", "gaze": "^1.1.2", "gridstack": "https://github.com/grafana/gridstack.js#grafana", + "gemini-scrollbar": "https://github.com/grafana/gemini-scrollbar#grafana", "grunt-jscs": "3.0.1", "grunt-sass-lint": "^0.2.2", "grunt-sync": "^0.6.2", diff --git a/pkg/api/api.go b/pkg/api/api.go index c91779cb193..f16f85e7c8f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -41,6 +41,7 @@ func (hs *HttpServer) registerRoutes() { r.Get("/org/users/", reqSignedIn, Index) r.Get("/org/apikeys/", reqSignedIn, Index) r.Get("/dashboard/import/", reqSignedIn, Index) + r.Get("/configuration", reqGrafanaAdmin, Index) r.Get("/admin", reqGrafanaAdmin, Index) r.Get("/admin/settings", reqGrafanaAdmin, Index) r.Get("/admin/users", reqGrafanaAdmin, Index) diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 41ce857db4f..1b862a9bdae 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -65,7 +65,7 @@ func New(hash string) *Avatar { return &Avatar{ hash: hash, reqParams: url.Values{ - "d": {"404"}, + "d": {"retro"}, "size": {"200"}, "r": {"pg"}}.Encode(), } @@ -146,7 +146,7 @@ func CacheServer() http.Handler { } func newNotFound() *Avatar { - avatar := &Avatar{} + avatar := &Avatar{notFound: true} // load transparent png into buffer path := filepath.Join(setting.StaticRootPath, "img", "transparent.png") diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index b813c78f2bb..ea910901492 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -7,7 +7,7 @@ type IndexViewData struct { AppSubUrl string GoogleAnalyticsId string GoogleTagManagerId string - MainNavLinks []*NavLink + NavTree []*NavLink BuildVersion string BuildCommit string NewGrafanaVersionExists bool @@ -20,10 +20,14 @@ type PluginCss struct { } type NavLink struct { - Text string `json:"text,omitempty"` - Icon string `json:"icon,omitempty"` - Img string `json:"img,omitempty"` - Url string `json:"url,omitempty"` - Divider bool `json:"divider,omitempty"` - Children []*NavLink `json:"children,omitempty"` + Id string `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Img string `json:"img,omitempty"` + Url string `json:"url,omitempty"` + Target string `json:"target,omitempty"` + Divider bool `json:"divider,omitempty"` + HideFromMenu bool `json:"hideFromMenu,omitempty"` + Children []*NavLink `json:"children,omitempty"` } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index d1c346e9539..b155858c58c 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -27,6 +27,7 @@ type CurrentUser struct { Email string `json:"email"` Name string `json:"name"` LightTheme bool `json:"lightTheme"` + OrgCount int `json:"orgCount"` OrgId int64 `json:"orgId"` OrgName string `json:"orgName"` OrgRole m.RoleType `json:"orgRole"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 2693d5f9683..53d72d411ef 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -141,7 +141,6 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro "alertingEnabled": setting.AlertingEnabled, "googleAnalyticsId": setting.GoogleAnalyticsId, "disableLoginForm": setting.DisableLoginForm, - "disableSignoutMenu": setting.DisableSignoutMenu, "externalUserMngInfo": setting.ExternalUserMngInfo, "externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl, "externalUserMngLinkName": setting.ExternalUserMngLinkName, diff --git a/pkg/api/index.go b/pkg/api/index.go index db8e51db9af..aef0bc98fb1 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -50,6 +50,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Login: c.Login, Email: c.Email, Name: c.Name, + OrgCount: c.OrgCount, OrgId: c.OrgId, OrgName: c.OrgName, OrgRole: c.OrgRole, @@ -85,10 +86,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { } if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ - Text: "New", + data.NavTree = append(data.NavTree, &dtos.NavLink{ + Text: "Create", Icon: "fa fa-fw fa-plus", - Url: "", + Url: "#", Children: []*dtos.NavLink{ {Text: "Dashboard", Icon: "fa fa-fw fa-plus", Url: setting.AppSubUrl + "/dashboard/new"}, {Text: "Folder", Icon: "fa fa-fw fa-plus", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"}, @@ -99,53 +100,56 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { dashboardChildNavs := []*dtos.NavLink{ {Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home"}, - {Text: "Playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"}, - {Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-snapshot"}, + {Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"}, + {Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-snapshot"}, } - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ + data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Dashboards", + Id: "dashboards", Icon: "icon-gf icon-gf-dashboard", Url: setting.AppSubUrl + "/", Children: dashboardChildNavs, }) - if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) { - alertChildNavs := []*dtos.NavLink{ - {Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list", Icon: "fa fa-fw fa-list-ul"}, - {Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "fa fa-fw fa-bell-o"}, + if c.IsSignedIn { + profileNode := &dtos.NavLink{ + Text: c.SignedInUser.Login, + Id: "profile", + Img: data.User.GravatarUrl, + Url: setting.AppSubUrl + "/profile", + HideFromMenu: true, + Children: []*dtos.NavLink{ + {Text: "Your profile", Url: setting.AppSubUrl + "/profile", Icon: "fa fa-fw fa-sliders"}, + {Text: "Change Password", Id: "change-password", Url: setting.AppSubUrl + "/profile/password", Icon: "fa fa-fw fa-lock", HideFromMenu: true}, + }, } - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ + if !setting.DisableSignoutMenu { + // add sign out first + profileNode.Children = append([]*dtos.NavLink{ + {Text: "Sign out", Url: setting.AppSubUrl + "/logout", Icon: "fa fa-fw fa-sign-out", Target: "_self"}, + }, profileNode.Children...) + } + + data.NavTree = append(data.NavTree, profileNode) + } + + if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) { + alertChildNavs := []*dtos.NavLink{ + {Text: "Alert List", Id: "alert-list", Url: setting.AppSubUrl + "/alerting/list", Icon: "fa fa-fw fa-list-ul"}, + {Text: "Notification channels", Id: "channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "fa fa-fw fa-bell-o"}, + } + + data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Alerting", + Id: "alerting", Icon: "icon-gf icon-gf-alert", Url: setting.AppSubUrl + "/alerting/list", Children: alertChildNavs, }) } - if c.OrgRole == m.ROLE_ADMIN { - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ - Text: "Data Sources", - Icon: "icon-gf icon-gf-datasources", - Url: setting.AppSubUrl + "/datasources", - Children: []*dtos.NavLink{ - {Text: "List", Url: setting.AppSubUrl + "/datasources", Icon: "icon-gf icon-gf-datasources"}, - {Text: "New", Url: setting.AppSubUrl + "/datasources", Icon: "fa fa-fw fa-plus"}, - }, - }) - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ - Text: "Plugins", - Icon: "icon-gf icon-gf-apps", - Url: setting.AppSubUrl + "/plugins", - Children: []*dtos.NavLink{ - {Text: "Panels", Url: setting.AppSubUrl + "/plugins?type=panel", Icon: "fa fa-fw fa-stop"}, - {Text: "Data sources", Url: setting.AppSubUrl + "/plugins?type=datasource", Icon: "icon-gf icon-gf-datasources"}, - {Text: "Apps", Url: setting.AppSubUrl + "/plugins?type=app", Icon: "icon-gf icon-gf-apps"}, - }, - }) - } - enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId) if err != nil { return nil, err @@ -155,6 +159,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if plugin.Pinned { appLink := &dtos.NavLink{ Text: plugin.Name, + Id: "plugin-page-" + plugin.Id, Url: plugin.DefaultNavUrl, Img: plugin.Info.Logos.Small, } @@ -187,25 +192,97 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { } if len(appLink.Children) > 0 { - data.MainNavLinks = append(data.MainNavLinks, appLink) + data.NavTree = append(data.NavTree, appLink) } } } - if c.IsGrafanaAdmin { - data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ - Text: "Admin", + if c.OrgRole == m.ROLE_ADMIN { + cfgNode := &dtos.NavLink{ + Id: "cfg", + Text: "Configuration", Icon: "fa fa-fw fa-cogs", - Url: setting.AppSubUrl + "/admin", + Url: setting.AppSubUrl + "/configuration", Children: []*dtos.NavLink{ - {Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"}, - {Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"}, - {Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"}, - {Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"}, + { + Text: "Data Sources", + Icon: "icon-gf icon-gf-datasources", + Description: "Add and configure data sources", + Id: "datasources", + Url: setting.AppSubUrl + "/datasources", + Children: []*dtos.NavLink{ + {Text: "List", Url: setting.AppSubUrl + "/datasources", Icon: "icon-gf icon-gf-datasources"}, + {Text: "New", Url: setting.AppSubUrl + "/datasources", Icon: "fa fa-fw fa-plus"}, + }, + }, + { + Text: "Preferences", + Id: "org", + Description: "Organization preferences", + Icon: "fa fa-fw fa-sliders", + Url: setting.AppSubUrl + "/org", + }, + { + Text: "Plugins", + Id: "plugins", + Description: "View and configure plugins", + Icon: "icon-gf icon-gf-apps", + Url: setting.AppSubUrl + "/plugins", + Children: []*dtos.NavLink{ + {Text: "Panels", Url: setting.AppSubUrl + "/plugins?type=panel", Icon: "fa fa-fw fa-stop"}, + {Text: "Data sources", Url: setting.AppSubUrl + "/plugins?type=datasource", Icon: "icon-gf icon-gf-datasources"}, + {Text: "Apps", Url: setting.AppSubUrl + "/plugins?type=app", Icon: "icon-gf icon-gf-apps"}, + }, + }, + { + Text: "Users", + Id: "users", + Description: "Manage users & user groups", + Icon: "fa fa-fw fa-users", + Url: setting.AppSubUrl + "/org/users", + }, + { + Text: "API Keys", + Id: "apikeys", + Description: "Create & manage API keys", + Icon: "fa fa-fw fa-key", + Url: setting.AppSubUrl + "/org/apikeys", + }, }, - }) + } + + if c.IsGrafanaAdmin { + cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{ + Text: "Server Admin", + Id: "admin", + Icon: "fa fa-fw fa-shield", + Url: setting.AppSubUrl + "/admin", + Children: []*dtos.NavLink{ + {Text: "Global Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users"}, + {Text: "Global Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs"}, + {Text: "Server Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings"}, + {Text: "Server Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats"}, + {Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/admin/styleguide"}, + }, + }) + } + + data.NavTree = append(data.NavTree, cfgNode) } + data.NavTree = append(data.NavTree, &dtos.NavLink{ + Text: "Help", + Id: "help", + Url: "#", + Icon: "fa fa-fw fa-question", + HideFromMenu: true, + Children: []*dtos.NavLink{ + {Text: "Keyboard shortcuts", Url: "/shortcuts", Icon: "fa fa-fw fa-keyboard-o", Target: "_self"}, + {Text: "Community site", Url: "http://community.grafana.com", Icon: "fa fa-fw fa-comment", Target: "_blank"}, + {Text: "Documentation", Url: "http://docs.grafana.org", Icon: "fa fa-fw fa-file", Target: "_blank"}, + }, + }) + return &data, nil } diff --git a/pkg/models/user.go b/pkg/models/user.go index 805bac2628f..bb12bc0038d 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -160,6 +160,7 @@ type SignedInUser struct { Name string Email string ApiKeyId int64 + OrgCount int IsGrafanaAdmin bool HelpFlags1 HelpFlags1 LastSeenAt time.Time diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 14f9ddb08c6..00eb0c4bc6b 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -350,6 +350,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { u.name as name, u.help_flags1 as help_flags1, u.last_seen_at as last_seen_at, + (SELECT COUNT(*) FROM org_user where org_user.user_id = u.id) as org_count, org.name as org_name, org_user.role as org_role, org.id as org_id diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index e38f37b96af..09ded090089 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -183,7 +183,7 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) values := make([]interface{}, len(types)) for i, stype := range types { - e.log.Info("type", "type", stype) + e.log.Debug("type", "type", stype) switch stype.DatabaseTypeName() { case mysql.FieldTypeNameTiny: values[i] = new(int8) diff --git a/public/app/core/components/gf_page.ts b/public/app/core/components/gf_page.ts new file mode 100644 index 00000000000..d5ac3e55c88 --- /dev/null +++ b/public/app/core/components/gf_page.ts @@ -0,0 +1,42 @@ +/// + +import coreModule from 'app/core/core_module'; + +const template = ` +
+ +
+ + +
+
+
+
+`; + +export function gfPageDirective() { + return { + restrict: 'E', + template: template, + scope: { + "model": "=", + }, + transclude: { + 'header': '?gfPageHeader', + 'body': 'gfPageBody', + }, + link: function(scope, elem, attrs) { + console.log(scope); + } + }; +} + +coreModule.directive('gfPage', gfPageDirective); diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index e058e02286a..ff8f0d1aadb 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -65,36 +65,24 @@ export class GrafanaCtrl { } /** @ngInject */ -export function grafanaAppDirective(playlistSrv, contextSrv) { +export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope) { return { restrict: 'E', controller: GrafanaCtrl, link: (scope, elem) => { - var ignoreSideMenuHide; + var sidemenuOpen; var body = $('body'); // see https://github.com/zenorocha/clipboard.js/issues/155 $.fn.modal.Constructor.prototype.enforceFocus = function() {}; - // handle sidemenu open state - scope.$watch('contextSrv.sidemenu', newVal => { - if (newVal !== undefined) { - body.toggleClass('sidemenu-open', scope.contextSrv.sidemenu); - if (!newVal) { - contextSrv.setPinnedState(false); - } - } - if (contextSrv.sidemenu) { - ignoreSideMenuHide = true; - setTimeout(() => { - ignoreSideMenuHide = false; - }, 300); - } - }); + sidemenuOpen = scope.contextSrv.sidemenu; + body.toggleClass('sidemenu-open', sidemenuOpen); - scope.$watch('contextSrv.pinned', newVal => { - if (newVal !== undefined) { - body.toggleClass('sidemenu-pinned', newVal); + scope.$watch('contextSrv.sidemenu', newVal => { + if (sidemenuOpen !== scope.contextSrv.sidemenu) { + sidemenuOpen = scope.contextSrv.sidemenu; + body.toggleClass('sidemenu-open', scope.contextSrv.sidemenu); } }); @@ -130,6 +118,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { var lastActivity = new Date().getTime(); var activeUser = true; var inActiveTimeLimit = 60 * 1000; + var sidemenuHidden = false; function checkForInActiveUser() { if (!activeUser) { @@ -143,6 +132,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { if ((new Date().getTime() - lastActivity) > inActiveTimeLimit) { activeUser = false; body.addClass('user-activity-low'); + // hide sidemenu + if (sidemenuOpen) { + sidemenuHidden = true; + body.removeClass('sidemenu-open'); + $timeout(function() { + $rootScope.$broadcast("render"); + }, 100); + } } } @@ -151,6 +148,15 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { if (!activeUser) { activeUser = true; body.removeClass('user-activity-low'); + + // restore sidemenu + if (sidemenuHidden) { + sidemenuHidden = false; + body.addClass('sidemenu-open'); + $timeout(function() { + $rootScope.$broadcast("render"); + }, 100); + } } } @@ -199,15 +205,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { } } - // hide sidemenu - if (!ignoreSideMenuHide && !contextSrv.pinned && body.find('.sidemenu').length > 0) { - if (target.parents('.sidemenu').length === 0) { - scope.$apply(function() { - scope.contextSrv.toggleSideMenu(); - }); - } - } - // hide popovers var popover = elem.find('.popover'); if (popover.length > 0 && target.parents('.graph-legend').length === 0) { diff --git a/public/app/core/components/help/help.html b/public/app/core/components/help/help.html index c07d57a0ffc..45faa560e40 100644 --- a/public/app/core/components/help/help.html +++ b/public/app/core/components/help/help.html @@ -4,15 +4,6 @@ Shortcuts - - - - - - - - - diff --git a/public/app/core/components/navbar/navbar.html b/public/app/core/components/navbar/navbar.html index 1294fefddca..d33378e33fc 100644 --- a/public/app/core/components/navbar/navbar.html +++ b/public/app/core/components/navbar/navbar.html @@ -1,36 +1,26 @@ -