Merge branch 'master' into ldap

This commit is contained in:
Torkel Ödegaard 2015-06-03 14:54:48 +02:00
commit 2c7d33cdfa
124 changed files with 3339 additions and 1427 deletions

View File

@ -6,20 +6,26 @@
- [Issue #1888](https://github.com/grafana/grafana/issues/1144). Templating: Repeat panel or row for each selected template variable value
- [Issue #1888](https://github.com/grafana/grafana/issues/1944). Dashboard: Custom Navigation links & dynamic links to related dashboards
- [Issue #590](https://github.com/grafana/grafana/issues/590). Graph: Define series color using regex rule
- [Issue #2096](https://github.com/grafana/grafana/issues/2096). Dashboard list panel: Now supports search by multiple tags
**User or Organization admin**
- [Issue #1899](https://github.com/grafana/grafana/issues/1899). Organization: You can now update the organization user role directly (without removing and readding the organization user).
- [Issue #2088](https://github.com/grafana/grafana/issues/2088). Roles: New user role `Read Only Editor` that replaces the old `Viewer` role behavior
**Backend**
- [Issue #2095](https://github.com/grafana/grafana/issues/2095). Search: Search now supports filtering by multiple dashboard tags
- [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski
- [Issue #2052](https://github.com/grafana/grafana/issues/2052). Github OAuth: You can now configure a Github organization requirement, thx @indrekj
- [Issue #1891](https://github.com/grafana/grafana/issues/1891). Security: New config option to disable the use of gravatar for profile images
- [Issue #1921](https://github.com/grafana/grafana/issues/1921). Auth: Support for user authentication via reverse proxy header (like X-Authenticated-User, or X-WEBAUTH-USER)
- [Issue #960](https://github.com/grafana/grafana/issues/960). Search: Backend can now index a folder with json files, will be available in search (saving back to folder is not supported, this feature is meant for static generated json dashboards)
**Breaking changes**
- [Issue #1826](https://github.com/grafana/grafana/issues/1826). User role 'Viewer' are now prohibited from entering edit mode (and doing other transient dashboard edits). A new role `Read Only Editor` will replace the old Viewer behavior
- [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing
- Backend render URL changed from `/render/dashboard/solo` `render/dashboard-solo/` (in order to have consistent dashboard url `/dashboard/:type/:slug`)
- Search HTTP API response has changed (simplified), tags list moved to seperate HTTP resource URI
- Datasource HTTP api breaking change, ADD datasource is now POST /api/datasources/, update is now PUT /api/datasources/:id
# 2.0.3 (unreleased - 2.0.x branch)

View File

@ -87,7 +87,7 @@ go get github.com/grafana/grafana
```
cd $GOPATH/src/github.com/grafana/grafana
go run build.go setup (only needed once to install godep)
godep restore (will pull down all golang lib dependecies in your current GOPATH)
godep restore (will pull down all golang lib dependencies in your current GOPATH)
go build .
```

View File

@ -153,6 +153,7 @@ token_url = https://github.com/login/oauth/access_token
api_url = https://api.github.com/user
team_ids =
allowed_domains =
allowed_organizations =
#################################### Google Auth ##########################
[auth.google]

View File

@ -146,12 +146,13 @@
;allow_sign_up = false
;client_id = some_id
;client_secret = some_secret
;scopes = user:email
;scopes = user:email,read:org
;auth_url = https://github.com/login/oauth/authorize
;token_url = https://github.com/login/oauth/access_token
;api_url = https://api.github.com/user
;team_ids =
;allowed_domains =
;allowed_organizations =
#################################### Google Auth ##########################
[auth.google]

View File

@ -61,7 +61,7 @@ pages:
- ['datasources/influxdb.md', 'Data Sources', 'InfluxDB']
- ['datasources/opentsdb.md', 'Data Sources', 'OpenTSDB']
- ['project/building_from_source.md', 'Project', 'Building from souce']
- ['project/building_from_source.md', 'Project', 'Building from source']
- ['project/cla.md', 'Project', 'Contributor License Agreement']
- ['jsearch.md', '**HIDDEN**']

View File

@ -27,7 +27,7 @@ Open a graph in edit mode by click the title.
![](/img/v2/opentsdb_query_editor.png)
For details on opentsdb metric queries checkout the offical [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html)
For details on opentsdb metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html)

View File

@ -19,7 +19,7 @@ The image above shows you the top header for a dashboard.
1. Side menubar toggle: This toggles the side menu, allowing you to focus on the data presented in the dashboard. The side menu provides access to features unrelated to a Dashboard such as Users, Organizations, and Data Sources.
2. Dashboard dropdown: This dropdown shows you which Dashboard you are currently viewing, and allows you to easily switch to a new Dashboard. From here you can also create a new Dashboard, Import existing Dashboards, and manage Dashboard playlists.
3. Star Dashboard: Star (or unstar) the current Dashboar. Starred Dashboards will show up on your own Home Dashboard by default, and are a convenient way to mark Dashboards that you're interested in.
3. Star Dashboard: Star (or unstar) the current Dashboard. Starred Dashboards will show up on your own Home Dashboard by default, and are a convenient way to mark Dashboards that you're interested in.
4. Share Dashboard: Share the current dashboard by creating a link or create a static Snapshot of it. Make sure the Dashboard is saved before sharing.
5. Save dashboard: The current Dashboard will be saved with the current Dashboard name.
6. Settings: Manage Dashboard settings and features such as Templating and Annotations.
@ -28,7 +28,7 @@ The image above shows you the top header for a dashboard.
Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows.
By adjusting the display properties of Panels and Rows, you can customize the perfect Dashboard for your exact needs.
Each panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB).
This allows you to create a single dashboard that unifies the data across your organization. Panels use the time range specificed
This allows you to create a single dashboard that unifies the data across your organization. Panels use the time range specified
in the main Time Picker in the upper right, but they can also have relative time overrides.
<img src="/img/v2/dashboard_annotated.png" class="no-shadow">

View File

@ -296,12 +296,12 @@ Secret. Specify these in the Grafana configuration file. For example:
scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
auth_url = https://accounts.google.com/o/oauth2/auth
token_url = https://accounts.google.com/o/oauth2/token
allowed_domains = mycompany.com
allowed_domains = mycompany.com mycompany.org
allow_sign_up = false
Restart the Grafana back-end. You should now see a Google login button
on the login page. You can now login or sign up with your Google
accounts. The `allowed_domains` option is optional.
accounts. The `allowed_domains` option is optional, and domains were separated by space.
You may allow users to sign-up via Google authentication by setting the
`allow_sign_up` option to `true`. When this option is set to `true`, any

View File

@ -7,7 +7,7 @@ page_keywords: grafana, installation, mac, osx, guide
# Installing on Mac
There is currently no binary build for Mac. But read the [build from
source](../project/building_from_source) page for instructions on how to
source](/project/building_from_source) page for instructions on how to
build it yourself.

View File

@ -72,4 +72,4 @@ You only need to add the options you want to override. Config files are applied
## Create a pull requests
Before or after your create a pull requests, sign the [contributor license aggrement](/docs/contributing/cla.html).
Before or after your create a pull requests, sign the [contributor license agreement](/docs/contributing/cla.html).

View File

@ -18,9 +18,9 @@ dropdown. This will open the `Annotations` edit view. Click the `Add` tab to add
Graphite supports two ways to query annotations.
- A regular metric query, use the `Graphite target expression` text input for this
- Graphite events query, use the `Graphite event tags` text input, especify an tag or wildcard (leave empty should also work)
- Graphite events query, use the `Graphite event tags` text input, specify an tag or wildcard (leave empty should also work)
## Elasticsearch annoations
## Elasticsearch annotations
![](/img/v2/annotations_es.png)
Grafana can query any Elasticsearch index for annotation events. The index name can be the name of an alias or an index wildcard pattern.

View File

@ -62,7 +62,7 @@ The ``Left Y`` and ``Right Y`` can be customized using:
- ``Unit`` - The display unit for the Y value
- ``Grid Max`` - The maximum Y value. (default auto)
- ``Grid Min`` - The minium Y value. (default auto)
- ``Grid Min`` - The minimum Y value. (default auto)
- ``Label`` - The Y axis label (default "")
Axes can also be hidden by unchecking the appropriate box from `Show Axis`.

View File

@ -84,8 +84,8 @@ Status Codes:
- **401** Unauthorized
- **412** Precondition failed
The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the verison that was sent). The
same status code is also used if another dashboar exists with the same title. The response body will look like this:
The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the version that was sent). The
same status code is also used if another dashboard exists with the same title. The response body will look like this:
HTTP/1.1 412 Precondition Failed
Content-Type: application/json; charset=UTF-8
@ -141,12 +141,236 @@ Will return the dashboard given the dashboard slug. Slug is the url friendly ver
The above will delete the dashboard with the specified slug. The slug is the url friendly (unique) version of the dashboard title.
### Gets the home dashboard
`GET /api/dashboards/home`
### Tags for Dashboard
`GET /api/dashboards/tags`
### Dashboard from JSON file
`GET /file/:file`
### Search Dashboards
`GET /api/search/`
Status Codes:
- **query** Search Query
- **tags** Tags to use
- **starred** Flag indicating if only starred Dashboards should be returned
- **tagcloud** - Flag indicating if a tagcloud should be returned
**Example Request**:
GET /api/search?query=MyDashboard&starred=true&tag=prod HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
## Data sources
### Get all datasources
`GET /api/datasources`
### Get a single data sources by Id
`GET /api/datasources/:datasourceId`
### Create data source
## Organizations
`PUT /api/datasources`
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{"message":"Datasource added"}
### Edit an existing data source
`POST /api/datasources`
### Delete an existing data source
`DELETE /api/datasources/:datasourceId`
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{"message":"Data source deleted"}
### Available data source types
`GET /api/datasources/plugins`
## Data source proxy calls
`GET /api/datasources/proxy/:datasourceId/*`
Proxies all calls to the actual datasource.
## Organisation
### Get current Organisation
`GET /api/org`
### Get all users within the actual organisation
`GET /api/org/users`
### Add a new user to the actual organisation
`POST /api/org/users`
Adds a global user to the actual organisation.
### Updates the given user
`PATCH /api/org/users/:userId`
### Delete user in actual organisation
`DELETE /api/org/users/:userId`
### Get all Users
`GET /api/org/users`
## Organisations
### Search all Organisations
`GET /api/orgs`
### Update Organisation
`PUT /api/orgs/:orgId`
### Get Users in Organisation
`GET /api/orgs/:orgId/users`
### Add User in Organisation
`POST /api/orgs/:orgId/users`
### Update Users in Organisation
`PATCH /api/orgs/:orgId/users/:userId`
### Delete User in Organisation
`DELETE /api/orgs/:orgId/users/:userId`
## Users
### Search Users
`GET /api/users`
### Get single user by Id
`GET /api/users/:id`
### User Update
`PUT /api/users/:id`
### Get Organisations for user
`GET /api/users/:id/orgs`
## User
### Change Password
`PUT /api/user/password`
Changes the password for the user
### Actual User
`GET /api/user`
The above will return the current user.
### Switch user context
`POST /api/user/using/:organisationId`
Switch user context to the given organisation.
### Organisations of the actual User
`GET /api/user/orgs`
The above will return a list of all organisations of the current user.
### Star a dashboard
`POST /api/user/stars/dashboard/:dashboardId`
Stars the given Dashboard for the actual user.
### Unstar a dashboard
`DELETE /api/user/stars/dashboard/:dashboardId`
Deletes the staring of the given Dashboard for the actual user.
## Snapshots
### Create new snapshot
`POST /api/snapshots`
### Get Snapshot by Id
`GET /api/snapshots/:key`
### Delete Snapshot by Id
`DELETE /api/snapshots-delete/:key`
## Frontend Settings
### Get Settings
`GET /api/frontend/settings`
## Login
### Renew session based on remember cookie
`GET /api/login/ping`
## Admin
### Settings
`GET /api/admin/settings`
### Global Users
`POST /api/admin/users`
### Password for User
`PUT /api/admin/users/:id/password`
### Permissions
`PUT /api/admin/users/:id/permissions`
### Delete global User
`DELETE /api/admin/users/:id`

View File

@ -12,7 +12,7 @@ With scripted dashboards you can dynamically create your dashboards using javasc
under `public/dashboards/` there is a file named `scripted.js`. This file contains an example of a scripted dashboard. You can access it by using the url:
`http://grafana_url/dashboard/script/scripted.js?rows=3&name=myName`
If you open scripted.js you can see how it reads url paramters from ARGS variable and then adds rows and panels.
If you open scripted.js you can see how it reads url parameters from ARGS variable and then adds rows and panels.
## Example

View File

@ -24,7 +24,7 @@ All of this applies to all Panels in the Dashboard (except those with Panel Time
It's possible to customize the options displayed for relative time and the auto-refresh options.
From Dashboard setttings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis. Entries are comma seperated and accept a number followed by one of the following units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years).
From Dashboard settings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis. Entries are comma separated and accept a number followed by one of the following units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years).
![](/img/v1/timepicker_editor.png)

View File

@ -1,3 +1,3 @@
{
"version": "2.0.1",
"version": "2.0.2"
}

View File

@ -17,7 +17,7 @@ func AdminGetSettings(c *middleware.Context) {
for _, key := range section.Keys() {
keyName := key.Name()
value := key.Value()
if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") {
if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config") && strings.Contains(value, "@")) {
value = "************"
}

View File

@ -9,36 +9,6 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func AdminSearchUsers(c *middleware.Context) {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to fetch users", err)
return
}
c.JSON(200, query.Result)
}
func AdminGetUser(c *middleware.Context) {
userId := c.ParamsInt64(":id")
query := m.GetUserByIdQuery{Id: userId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to fetch user", err)
return
}
result := dtos.AdminUserListItem{
Name: query.Result.Name,
Email: query.Result.Email,
Login: query.Result.Login,
IsGrafanaAdmin: query.Result.IsAdmin,
}
c.JSON(200, result)
}
func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
cmd := m.CreateUserCommand{
Login: form.Login,
@ -70,32 +40,6 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
c.JsonOK("User created")
}
func AdminUpdateUser(c *middleware.Context, form dtos.AdminUpdateUserForm) {
userId := c.ParamsInt64(":id")
cmd := m.UpdateUserCommand{
UserId: userId,
Login: form.Login,
Email: form.Email,
Name: form.Name,
}
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email
if len(cmd.Login) == 0 {
c.JsonApiErr(400, "Validation error, need specify either username or email", nil)
return
}
}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "failed to update user", err)
return
}
c.JsonOK("User updated")
}
func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) {
userId := c.ParamsInt64(":id")

View File

@ -13,7 +13,7 @@ func Register(r *macaron.Macaron) {
reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
reqAccountAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
regOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
bind := binding.Bind
// not logged in views
@ -53,48 +53,71 @@ func Register(r *macaron.Macaron) {
// authed api
r.Group("/api", func() {
// user
// user (signed in)
r.Group("/user", func() {
r.Get("/", GetUser)
r.Put("/", bind(m.UpdateUserCommand{}), UpdateUser)
r.Post("/using/:id", UserSetUsingOrg)
r.Get("/orgs", GetUserOrgList)
r.Post("/stars/dashboard/:id", StarDashboard)
r.Delete("/stars/dashboard/:id", UnstarDashboard)
r.Put("/password", bind(m.ChangeUserPasswordCommand{}), ChangeUserPassword)
r.Get("/", wrap(GetSignedInUser))
r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
r.Post("/using/:id", wrap(UserSetUsingOrg))
r.Get("/orgs", wrap(GetSignedInUserOrgList))
r.Post("/stars/dashboard/:id", wrap(StarDashboard))
r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
})
// account
// users (admin permission required)
r.Group("/users", func() {
r.Get("/", wrap(SearchUsers))
r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList))
r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
}, reqGrafanaAdmin)
// current org
r.Group("/org", func() {
r.Get("/", GetOrg)
r.Post("/", bind(m.CreateOrgCommand{}), CreateOrg)
r.Put("/", bind(m.UpdateOrgCommand{}), UpdateOrg)
r.Post("/users", bind(m.AddOrgUserCommand{}), AddOrgUser)
r.Get("/users", GetOrgUsers)
r.Patch("/users/:id", bind(m.UpdateOrgUserCommand{}), UpdateOrgUser)
r.Delete("/users/:id", RemoveOrgUser)
}, reqAccountAdmin)
r.Get("/", wrap(GetOrgCurrent))
r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrgCurrent))
r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
r.Get("/users", wrap(GetOrgUsersForCurrentOrg))
r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
r.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
}, regOrgAdmin)
// create new org
r.Post("/orgs", bind(m.CreateOrgCommand{}), wrap(CreateOrg))
// search all orgs
r.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
// orgs (admin routes)
r.Group("/orgs/:orgId", func() {
r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrg))
r.Get("/users", wrap(GetOrgUsers))
r.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser))
r.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
r.Delete("/users/:userId", wrap(RemoveOrgUser))
}, reqGrafanaAdmin)
// auth api keys
r.Group("/auth/keys", func() {
r.Get("/", GetApiKeys)
r.Post("/", bind(m.AddApiKeyCommand{}), AddApiKey)
r.Delete("/:id", DeleteApiKey)
}, reqAccountAdmin)
r.Get("/", wrap(GetApiKeys))
r.Post("/", bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
r.Delete("/:id", wrap(DeleteApiKey))
}, regOrgAdmin)
// Data sources
r.Group("/datasources", func() {
r.Combo("/").
Get(GetDataSources).
Put(bind(m.AddDataSourceCommand{}), AddDataSource).
Post(bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
r.Get("/", GetDataSources)
r.Post("/", bind(m.AddDataSourceCommand{}), AddDataSource)
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
r.Delete("/:id", DeleteDataSource)
r.Get("/:id", GetDataSourceById)
r.Get("/plugins", GetDataSourcePlugins)
}, reqAccountAdmin)
}, regOrgAdmin)
r.Get("/frontend/settings/", GetFrontendSettings)
r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
r.Any("/datasources/proxy/:id", reqSignedIn, ProxyDataSourceRequest)
// Dashboard
r.Group("/dashboards", func() {
@ -115,10 +138,7 @@ func Register(r *macaron.Macaron) {
// admin api
r.Group("/api/admin", func() {
r.Get("/settings", AdminGetSettings)
r.Get("/users", AdminSearchUsers)
r.Get("/users/:id", AdminGetUser)
r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser)
r.Put("/users/:id/details", bind(dtos.AdminUpdateUserForm{}), AdminUpdateUser)
r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
r.Delete("/users/:id", AdminDeleteUser)
@ -127,5 +147,5 @@ func Register(r *macaron.Macaron) {
// rendering
r.Get("/render/*", reqSignedIn, RenderToPng)
r.NotFound(NotFound)
r.NotFound(NotFoundHandler)
}

View File

@ -8,12 +8,11 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
func GetApiKeys(c *middleware.Context) {
func GetApiKeys(c *middleware.Context) Response {
query := m.GetApiKeysQuery{OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to list api keys", err)
return
return ApiError(500, "Failed to list api keys", err)
}
result := make([]*m.ApiKeyDTO, len(query.Result))
@ -24,27 +23,26 @@ func GetApiKeys(c *middleware.Context) {
Role: t.Role,
}
}
c.JSON(200, result)
return Json(200, result)
}
func DeleteApiKey(c *middleware.Context) {
func DeleteApiKey(c *middleware.Context) Response {
id := c.ParamsInt64(":id")
cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId}
err := bus.Dispatch(cmd)
if err != nil {
c.JsonApiErr(500, "Failed to delete API key", err)
return
return ApiError(500, "Failed to delete API key", err)
}
c.JsonOK("API key deleted")
return ApiSuccess("API key deleted")
}
func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) {
func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) Response {
if !cmd.Role.IsValid() {
c.JsonApiErr(400, "Invalid role specified", nil)
return
return ApiError(400, "Invalid role specified", nil)
}
cmd.OrgId = c.OrgId
@ -53,14 +51,12 @@ func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) {
cmd.Key = newKeyInfo.HashedKey
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to add API key", err)
return
return ApiError(500, "Failed to add API key", err)
}
result := &dtos.NewApiKeyResult{
Name: cmd.Result.Name,
Key: newKeyInfo.ClientSecret,
}
Key: newKeyInfo.ClientSecret}
c.JSON(200, result)
return Json(200, result)
}

122
pkg/api/common.go Normal file
View File

@ -0,0 +1,122 @@
package api
import (
"encoding/json"
"net/http"
"github.com/Unknwon/macaron"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting"
)
var (
NotFound = ApiError(404, "Not found", nil)
ServerError = ApiError(500, "Server error", nil)
)
type Response interface {
WriteTo(out http.ResponseWriter)
}
type NormalResponse struct {
status int
body []byte
header http.Header
}
func wrap(action interface{}) macaron.Handler {
return func(c *middleware.Context) {
var res Response
val, err := c.Invoke(action)
if err == nil && val != nil && len(val) > 0 {
res = val[0].Interface().(Response)
} else {
res = ServerError
}
res.WriteTo(c.Resp)
}
}
func (r *NormalResponse) WriteTo(out http.ResponseWriter) {
header := out.Header()
for k, v := range r.header {
header[k] = v
}
out.WriteHeader(r.status)
out.Write(r.body)
}
func (r *NormalResponse) Cache(ttl string) *NormalResponse {
return r.Header("Cache-Control", "public,max-age="+ttl)
}
func (r *NormalResponse) Header(key, value string) *NormalResponse {
r.header.Set(key, value)
return r
}
// functions to create responses
func Empty(status int) *NormalResponse {
return Respond(status, nil)
}
func Json(status int, body interface{}) *NormalResponse {
return Respond(status, body).Header("Content-Type", "application/json")
}
func ApiSuccess(message string) *NormalResponse {
resp := make(map[string]interface{})
resp["message"] = message
return Respond(200, resp)
}
func ApiError(status int, message string, err error) *NormalResponse {
resp := make(map[string]interface{})
if err != nil {
log.Error(4, "%s: %v", message, err)
if setting.Env != setting.PROD {
resp["error"] = err.Error()
}
}
switch status {
case 404:
resp["message"] = "Not Found"
metrics.M_Api_Status_500.Inc(1)
case 500:
metrics.M_Api_Status_404.Inc(1)
resp["message"] = "Internal Server Error"
}
if message != "" {
resp["message"] = message
}
return Json(status, resp)
}
func Respond(status int, body interface{}) *NormalResponse {
var b []byte
var err error
switch t := body.(type) {
case []byte:
b = t
case string:
b = []byte(t)
default:
if b, err = json.Marshal(body); err != nil {
return ApiError(500, "body json marshal", err)
}
}
return &NormalResponse{
body: b,
status: status,
header: make(http.Header),
}
}

View File

@ -55,6 +55,7 @@ func GetDashboard(c *middleware.Context) {
Type: m.DashTypeDB,
CanStar: c.IsSignedIn,
CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR,
CanEdit: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR || c.OrgRole == m.ROLE_READ_ONLY_EDITOR,
},
}

View File

@ -1,9 +1,12 @@
package api
import (
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
@ -11,6 +14,16 @@ import (
"github.com/grafana/grafana/pkg/util"
)
var dataProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy {
target, _ := url.Parse(ds.Url)
@ -56,5 +69,6 @@ func ProxyDataSourceRequest(c *middleware.Context) {
proxyPath := c.Params("*")
proxy := NewReverseProxy(&query.Result, proxyPath)
proxy.Transport = dataProxyTransport
proxy.ServeHTTP(c.RW(), c.Req.Request)
}

View File

@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util"
)
func GetDataSources(c *middleware.Context) {
@ -94,11 +95,12 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
return
}
c.JsonOK("Datasource added")
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id})
}
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id")
err := bus.Dispatch(&cmd)
if err != nil {

View File

@ -34,6 +34,7 @@ type DashboardMeta struct {
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanStar bool `json:"canStar"`
Slug string `json:"slug"`
Expires time.Time `json:"expires"`

View File

@ -54,6 +54,10 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
defaultDatasource = ds.Name
}
if len(ds.JsonData) > 0 {
dsMap["jsonData"] = ds.JsonData
}
if ds.Access == m.DS_ACCESS_DIRECT {
if ds.BasicAuth {
dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword)
@ -95,6 +99,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
"defaultDatasource": defaultDatasource,
"datasources": datasources,
"appSubUrl": setting.AppSubUrl,
"viewerRoleMode": setting.ViewerRoleMode,
"buildInfo": map[string]interface{}{
"version": setting.BuildVersion,
"commit": setting.BuildCommit,

View File

@ -59,7 +59,7 @@ func Index(c *middleware.Context) {
c.HTML(200, "index")
}
func NotFound(c *middleware.Context) {
func NotFoundHandler(c *middleware.Context) {
if c.IsApiRequest() {
c.JsonApiErr(404, "Not found", nil)
return

View File

@ -48,6 +48,8 @@ func OAuthLogin(ctx *middleware.Context) {
if err != nil {
if err == social.ErrMissingTeamMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled"))
} else if err == social.ErrMissingOrganizationMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled"))
} else {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
}

View File

@ -8,17 +8,25 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
func GetOrg(c *middleware.Context) {
query := m.GetOrgByIdQuery{Id: c.OrgId}
// GET /api/org
func GetOrgCurrent(c *middleware.Context) Response {
return getOrgHelper(c.OrgId)
}
// GET /api/orgs/:orgId
func GetOrgById(c *middleware.Context) Response {
return getOrgHelper(c.ParamsInt64(":orgId"))
}
func getOrgHelper(orgId int64) Response {
query := m.GetOrgByIdQuery{Id: orgId}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrOrgNotFound {
c.JsonApiErr(404, "Organization not found", err)
return
return ApiError(404, "Organization not found", err)
}
c.JsonApiErr(500, "Failed to get organization", err)
return
return ApiError(500, "Failed to get organization", err)
}
org := m.OrgDTO{
@ -26,33 +34,56 @@ func GetOrg(c *middleware.Context) {
Name: query.Result.Name,
}
c.JSON(200, &org)
return Json(200, &org)
}
func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) {
// POST /api/orgs
func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response {
if !setting.AllowUserOrgCreate && !c.IsGrafanaAdmin {
c.JsonApiErr(401, "Access denied", nil)
return
return ApiError(401, "Access denied", nil)
}
cmd.UserId = c.UserId
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to create organization", err)
return
return ApiError(500, "Failed to create organization", err)
}
metrics.M_Api_Org_Create.Inc(1)
c.JsonOK("Organization created")
return ApiSuccess("Organization created")
}
func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) {
// PUT /api/org
func UpdateOrgCurrent(c *middleware.Context, cmd m.UpdateOrgCommand) Response {
cmd.OrgId = c.OrgId
return updateOrgHelper(cmd)
}
// PUT /api/orgs/:orgId
func UpdateOrg(c *middleware.Context, cmd m.UpdateOrgCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
return updateOrgHelper(cmd)
}
func updateOrgHelper(cmd m.UpdateOrgCommand) Response {
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to update organization", err)
return
return ApiError(500, "Failed to update organization", err)
}
c.JsonOK("Organization updated")
return ApiSuccess("Organization updated")
}
func SearchOrgs(c *middleware.Context) Response {
query := m.SearchOrgsQuery{
Query: c.Query("query"),
Name: c.Query("name"),
Page: 0,
Limit: 1000,
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to search orgs", err)
}
return Json(200, query.Result)
}

View File

@ -6,77 +6,115 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) {
// POST /api/org/users
func AddOrgUserToCurrentOrg(c *middleware.Context, cmd m.AddOrgUserCommand) Response {
cmd.OrgId = c.OrgId
return addOrgUserHelper(cmd)
}
// POST /api/orgs/:orgId/users
func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
return addOrgUserHelper(cmd)
}
func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
if !cmd.Role.IsValid() {
c.JsonApiErr(400, "Invalid role specified", nil)
return
return ApiError(400, "Invalid role specified", nil)
}
userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
err := bus.Dispatch(&userQuery)
if err != nil {
c.JsonApiErr(404, "User not found", nil)
return
return ApiError(404, "User not found", nil)
}
userToAdd := userQuery.Result
if userToAdd.Id == c.UserId {
c.JsonApiErr(400, "Cannot add yourself as user", nil)
return
}
// if userToAdd.Id == c.UserId {
// return ApiError(400, "Cannot add yourself as user", nil)
// }
cmd.OrgId = c.OrgId
cmd.UserId = userToAdd.Id
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Could not add user to organization", err)
return
return ApiError(500, "Could not add user to organization", err)
}
c.JsonOK("User added to organization")
return ApiSuccess("User added to organization")
}
func GetOrgUsers(c *middleware.Context) {
query := m.GetOrgUsersQuery{OrgId: c.OrgId}
// GET /api/org/users
func GetOrgUsersForCurrentOrg(c *middleware.Context) Response {
return getOrgUsersHelper(c.OrgId)
}
// GET /api/orgs/:orgId/users
func GetOrgUsers(c *middleware.Context) Response {
return getOrgUsersHelper(c.ParamsInt64(":orgId"))
}
func getOrgUsersHelper(orgId int64) Response {
query := m.GetOrgUsersQuery{OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to get account user", err)
return
return ApiError(500, "Failed to get account user", err)
}
c.JSON(200, query.Result)
return Json(200, query.Result)
}
func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) {
if !cmd.Role.IsValid() {
c.JsonApiErr(400, "Invalid role specified", nil)
return
}
cmd.UserId = c.ParamsInt64(":id")
// PATCH /api/org/users/:userId
func UpdateOrgUserForCurrentOrg(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed update org user", err)
return
}
c.JsonOK("Organization user updated")
cmd.UserId = c.ParamsInt64(":userId")
return updateOrgUserHelper(cmd)
}
func RemoveOrgUser(c *middleware.Context) {
userId := c.ParamsInt64(":id")
// PATCH /api/orgs/:orgId/users/:userId
func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response {
cmd.OrgId = c.ParamsInt64(":orgId")
cmd.UserId = c.ParamsInt64(":userId")
return updateOrgUserHelper(cmd)
}
cmd := m.RemoveOrgUserCommand{OrgId: c.OrgId, UserId: userId}
func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
if !cmd.Role.IsValid() {
return ApiError(400, "Invalid role specified", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastOrgAdmin {
c.JsonApiErr(400, "Cannot remove last organization admin", nil)
return
return ApiError(400, "Cannot change role so that there is no organization admin left", nil)
}
c.JsonApiErr(500, "Failed to remove user from organization", err)
return ApiError(500, "Failed update org user", err)
}
c.JsonOK("User removed from organization")
return ApiSuccess("Organization user updated")
}
// DELETE /api/org/users/:userId
func RemoveOrgUserForCurrentOrg(c *middleware.Context) Response {
userId := c.ParamsInt64(":userId")
return removeOrgUserHelper(c.OrgId, userId)
}
// DELETE /api/orgs/:orgId/users/:userId
func RemoveOrgUser(c *middleware.Context) Response {
userId := c.ParamsInt64(":userId")
orgId := c.ParamsInt64(":orgId")
return removeOrgUserHelper(orgId, userId)
}
func removeOrgUserHelper(orgId int64, userId int64) Response {
cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastOrgAdmin {
return ApiError(400, "Cannot remove last organization admin", nil)
}
return ApiError(500, "Failed to remove user from organization", err)
}
return ApiSuccess("User removed from organization")
}

View File

@ -8,17 +8,17 @@ import (
func Search(c *middleware.Context) {
query := c.Query("query")
tag := c.Query("tag")
tags := c.QueryStrings("tag")
starred := c.Query("starred")
limit := c.QueryInt("limit")
if limit == 0 {
limit = 200
limit = 1000
}
searchQuery := search.Query{
Title: query,
Tag: tag,
Tags: tags,
UserId: c.UserId,
Limit: limit,
IsStarred: starred == "true",

View File

@ -6,45 +6,35 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
func StarDashboard(c *middleware.Context) {
func StarDashboard(c *middleware.Context) Response {
if !c.IsSignedIn {
c.JsonApiErr(412, "You need to sign in to star dashboards", nil)
return
return ApiError(412, "You need to sign in to star dashboards", nil)
}
var cmd = m.StarDashboardCommand{
UserId: c.UserId,
DashboardId: c.ParamsInt64(":id"),
}
cmd := m.StarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
if cmd.DashboardId <= 0 {
c.JsonApiErr(400, "Missing dashboard id", nil)
return
return ApiError(400, "Missing dashboard id", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to star dashboard", err)
return
return ApiError(500, "Failed to star dashboard", err)
}
c.JsonOK("Dashboard starred!")
return ApiSuccess("Dashboard starred!")
}
func UnstarDashboard(c *middleware.Context) {
var cmd = m.UnstarDashboardCommand{
UserId: c.UserId,
DashboardId: c.ParamsInt64(":id"),
}
func UnstarDashboard(c *middleware.Context) Response {
cmd := m.UnstarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
if cmd.DashboardId <= 0 {
c.JsonApiErr(400, "Missing dashboard id", nil)
return
return ApiError(400, "Missing dashboard id", nil)
}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to unstar dashboard", err)
return
return ApiError(500, "Failed to unstar dashboard", err)
}
c.JsonOK("Dashboard unstarred")
return ApiSuccess("Dashboard unstarred")
}

View File

@ -7,44 +7,71 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func GetUser(c *middleware.Context) {
query := m.GetUserProfileQuery{UserId: c.UserId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to get user", err)
return
}
c.JSON(200, query.Result)
// GET /api/user (current authenticated user)
func GetSignedInUser(c *middleware.Context) Response {
return getUserUserProfile(c.UserId)
}
func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) {
// GET /api/user/:id
func GetUserById(c *middleware.Context) Response {
return getUserUserProfile(c.ParamsInt64(":id"))
}
func getUserUserProfile(userId int64) Response {
query := m.GetUserProfileQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to get user", err)
}
return Json(200, query.Result)
}
// POST /api/user
func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
cmd.UserId = c.UserId
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(400, "Failed to update user", err)
return
}
c.JsonOK("User updated")
return handleUpdateUser(cmd)
}
func GetUserOrgList(c *middleware.Context) {
query := m.GetUserOrgListQuery{UserId: c.UserId}
// POST /api/users/:id
func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
cmd.UserId = c.ParamsInt64(":id")
return handleUpdateUser(cmd)
}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to get user organizations", err)
return
}
for _, ac := range query.Result {
if ac.OrgId == c.OrgId {
ac.IsUsing = true
break
func handleUpdateUser(cmd m.UpdateUserCommand) Response {
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email
if len(cmd.Login) == 0 {
return ApiError(400, "Validation error, need specify either username or email", nil)
}
}
c.JSON(200, query.Result)
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "failed to update user", err)
}
return ApiSuccess("User updated")
}
// GET /api/user/orgs
func GetSignedInUserOrgList(c *middleware.Context) Response {
return getUserOrgList(c.UserId)
}
// GET /api/user/:id/orgs
func GetUserOrgList(c *middleware.Context) Response {
return getUserOrgList(c.ParamsInt64(":id"))
}
func getUserOrgList(userId int64) Response {
query := m.GetUserOrgListQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Faile to get user organziations", err)
}
return Json(200, query.Result)
}
func validateUsingOrg(userId int64, orgId int64) bool {
@ -65,53 +92,55 @@ func validateUsingOrg(userId int64, orgId int64) bool {
return valid
}
func UserSetUsingOrg(c *middleware.Context) {
// POST /api/user/using/:id
func UserSetUsingOrg(c *middleware.Context) Response {
orgId := c.ParamsInt64(":id")
if !validateUsingOrg(c.UserId, orgId) {
c.JsonApiErr(401, "Not a valid organization", nil)
return
return ApiError(401, "Not a valid organization", nil)
}
cmd := m.SetUsingOrgCommand{
UserId: c.UserId,
OrgId: orgId,
}
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed change active organization", err)
return
return ApiError(500, "Failed change active organization", err)
}
c.JsonOK("Active organization changed")
return ApiSuccess("Active organization changed")
}
func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) {
func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response {
userQuery := m.GetUserByIdQuery{Id: c.UserId}
if err := bus.Dispatch(&userQuery); err != nil {
c.JsonApiErr(500, "Could not read user from database", err)
return
return ApiError(500, "Could not read user from database", err)
}
passwordHashed := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt)
if passwordHashed != userQuery.Result.Password {
c.JsonApiErr(401, "Invalid old password", nil)
return
return ApiError(401, "Invalid old password", nil)
}
if len(cmd.NewPassword) < 4 {
c.JsonApiErr(400, "New password too short", nil)
return
return ApiError(400, "New password too short", nil)
}
cmd.UserId = c.UserId
cmd.NewPassword = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt)
if err := bus.Dispatch(&cmd); err != nil {
c.JsonApiErr(500, "Failed to change user password", err)
return
return ApiError(500, "Failed to change user password", err)
}
c.JsonOK("User password changed")
return ApiSuccess("User password changed")
}
// GET /api/users
func SearchUsers(c *middleware.Context) Response {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to fetch users", err)
}
return Json(200, query.Result)
}

View File

@ -26,7 +26,7 @@ func RenderToPng(params *RenderOpts) (string, error) {
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
pngPath = pngPath + ".png"
cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width,
cmd := exec.Command(binPath, "--ignore-ssl-errors=true", "--ssl-protocol=any", scriptPath, "url="+params.Url, "width="+params.Width,
"height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName,
"domain="+setting.Domain, "sessionid="+params.SessionId)
stdout, err := cmd.StdoutPipe()

View File

@ -69,7 +69,6 @@ type AddDataSourceCommand struct {
// Also acts as api DTO
type UpdateDataSourceCommand struct {
Id int64 `json:"id" binding:"Required"`
Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"`
Access DsAccess `json:"access" binding:"Required"`
@ -84,6 +83,7 @@ type UpdateDataSourceCommand struct {
JsonData map[string]interface{} `json:"jsonData"`
OrgId int64 `json:"-"`
Id int64 `json:"-"`
}
type DeleteDataSourceCommand struct {

View File

@ -48,8 +48,13 @@ type GetOrgByNameQuery struct {
Result *Org
}
type GetOrgListQuery struct {
Result []*Org
type SearchOrgsQuery struct {
Query string
Name string
Limit int
Page int
Result []*OrgDTO
}
type OrgDTO struct {
@ -58,8 +63,7 @@ type OrgDTO struct {
}
type UserOrgDTO struct {
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Role RoleType `json:"role"`
IsUsing bool `json:"isUsing"`
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Role RoleType `json:"role"`
}

View File

@ -15,13 +15,14 @@ var (
type RoleType string
const (
ROLE_VIEWER RoleType = "Viewer"
ROLE_EDITOR RoleType = "Editor"
ROLE_ADMIN RoleType = "Admin"
ROLE_VIEWER RoleType = "Viewer"
ROLE_EDITOR RoleType = "Editor"
ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor"
ROLE_ADMIN RoleType = "Admin"
)
func (r RoleType) IsValid() bool {
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
}
type OrgUser struct {

View File

@ -133,6 +133,7 @@ type UserProfileDTO struct {
Name string `json:"name"`
Login string `json:"login"`
Theme string `json:"theme"`
OrgId int64 `json:"orgId"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
}

View File

@ -33,9 +33,7 @@ func searchHandler(query *Query) error {
dashQuery := FindPersistedDashboardsQuery{
Title: query.Title,
Tag: query.Tag,
UserId: query.UserId,
Limit: query.Limit,
IsStarred: query.IsStarred,
OrgId: query.OrgId,
}
@ -55,8 +53,30 @@ func searchHandler(query *Query) error {
hits = append(hits, jsonHits...)
}
// filter out results with tag filter
if len(query.Tags) > 0 {
filtered := HitList{}
for _, hit := range hits {
if hasRequiredTags(query.Tags, hit.Tags) {
filtered = append(filtered, hit)
}
}
hits = filtered
}
// sort main result array
sort.Sort(hits)
if len(hits) > query.Limit {
hits = hits[0:query.Limit]
}
// sort tags
for _, hit := range hits {
sort.Strings(hit.Tags)
}
// add isStarred info
if err := setIsStarredFlagOnSearchResults(query.UserId, hits); err != nil {
return err
}
@ -65,6 +85,25 @@ func searchHandler(query *Query) error {
return nil
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
func hasRequiredTags(queryTags, hitTags []string) bool {
for _, queryTag := range queryTags {
if !stringInSlice(queryTag, hitTags) {
return false
}
}
return true
}
func setIsStarredFlagOnSearchResults(userId int64, hits []*Hit) error {
query := m.GetUserStarsQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {

View File

@ -0,0 +1,61 @@
package search
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestSearch(t *testing.T) {
Convey("Given search query", t, func() {
jsonDashIndex = NewJsonDashIndex("../../public/dashboards/")
query := Query{Limit: 2000}
bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error {
query.Result = HitList{
&Hit{Id: 16, Title: "CCAA", Tags: []string{"BB", "AA"}},
&Hit{Id: 10, Title: "AABB", Tags: []string{"CC", "AA"}},
&Hit{Id: 15, Title: "BBAA", Tags: []string{"EE", "AA", "BB"}},
}
return nil
})
bus.AddHandler("test", func(query *m.GetUserStarsQuery) error {
query.Result = map[int64]bool{10: true, 12: true}
return nil
})
Convey("That is empty", func() {
err := searchHandler(&query)
So(err, ShouldBeNil)
Convey("should return sorted results", func() {
So(query.Result[0].Title, ShouldEqual, "AABB")
So(query.Result[1].Title, ShouldEqual, "BBAA")
So(query.Result[2].Title, ShouldEqual, "CCAA")
})
Convey("should return sorted tags", func() {
So(query.Result[1].Tags[0], ShouldEqual, "AA")
So(query.Result[1].Tags[1], ShouldEqual, "BB")
So(query.Result[1].Tags[2], ShouldEqual, "EE")
})
})
Convey("That filters by tag", func() {
query.Tags = []string{"BB", "AA"}
err := searchHandler(&query)
So(err, ShouldBeNil)
Convey("should return correct results", func() {
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].Title, ShouldEqual, "BBAA")
So(query.Result[1].Title, ShouldEqual, "CCAA")
})
})
})
}

View File

@ -47,18 +47,15 @@ func (index *JsonDashIndex) updateLoop() {
func (index *JsonDashIndex) Search(query *Query) ([]*Hit, error) {
results := make([]*Hit, 0)
if query.IsStarred {
return results, nil
}
for _, item := range index.items {
if len(results) > query.Limit {
break
}
// filter out results with tag filter
if query.Tag != "" {
if !strings.Contains(item.TagsCsv, query.Tag) {
continue
}
}
// add results with matchig title filter
if strings.Contains(item.TitleLower, query.Title) {
results = append(results, &Hit{

View File

@ -17,19 +17,26 @@ func TestJsonDashIndex(t *testing.T) {
})
Convey("Should be able to search index", func() {
res, err := index.Search(&Query{Title: "", Tag: "", Limit: 20})
res, err := index.Search(&Query{Title: "", Limit: 20})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 3)
})
Convey("Should be able to search index by title", func() {
res, err := index.Search(&Query{Title: "home", Tag: "", Limit: 20})
res, err := index.Search(&Query{Title: "home", Limit: 20})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 1)
So(res[0].Title, ShouldEqual, "Home")
})
Convey("Should not return when starred is filtered", func() {
res, err := index.Search(&Query{Title: "", IsStarred: true})
So(err, ShouldBeNil)
So(len(res), ShouldEqual, 0)
})
})
}

View File

@ -26,7 +26,7 @@ func (s HitList) Less(i, j int) bool { return s[i].Title < s[j].Title }
type Query struct {
Title string
Tag string
Tags []string
OrgId int64
UserId int64
Limit int
@ -37,10 +37,8 @@ type Query struct {
type FindPersistedDashboardsQuery struct {
Title string
Tag string
OrgId int64
UserId int64
Limit int
IsStarred bool
Result HitList

View File

@ -109,25 +109,26 @@ func Setup() error {
}
func publish(routingKey string, msgString []byte) {
err := channel.Publish(
exchange, //exchange
routingKey, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: msgString,
},
)
if err != nil {
for {
err := channel.Publish(
exchange, //exchange
routingKey, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: msgString,
},
)
if err == nil {
return
}
// failures are most likely because the connection was lost.
// the connection will be re-established, so just keep
// retrying every 2seconds until we successfully publish.
time.Sleep(2 * time.Second)
fmt.Println("publish failed, retrying.")
publish(routingKey, msgString)
}
return
}
func eventListener(event interface{}) error {

View File

@ -150,16 +150,7 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {
params = append(params, "%"+query.Title+"%")
}
if len(query.Tag) > 0 {
sql.WriteString(" AND dashboard_tag.term=?")
params = append(params, query.Tag)
}
if query.Limit == 0 || query.Limit > 10000 {
query.Limit = 300
}
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT %d", query.Limit))
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
var res []DashboardSearchProjection
err := x.Sql(sql.String(), params...).Find(&res)

View File

@ -99,18 +99,6 @@ func TestDashboardDataAccess(t *testing.T) {
So(len(hit.Tags), ShouldEqual, 2)
})
Convey("Should be able to search for dashboards using tags", func() {
query1 := search.FindPersistedDashboardsQuery{Tag: "webapp", OrgId: 1}
query2 := search.FindPersistedDashboardsQuery{Tag: "tagdoesnotexist", OrgId: 1}
err := SearchDashboards(&query1)
err = SearchDashboards(&query2)
So(err, ShouldBeNil)
So(len(query1.Result), ShouldEqual, 1)
So(len(query2.Result), ShouldEqual, 0)
})
Convey("Should not be able to save dashboard with same name", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,

View File

@ -14,12 +14,23 @@ func init() {
bus.AddHandler("sql", CreateOrg)
bus.AddHandler("sql", UpdateOrg)
bus.AddHandler("sql", GetOrgByName)
bus.AddHandler("sql", GetOrgList)
bus.AddHandler("sql", SearchOrgs)
bus.AddHandler("sql", DeleteOrg)
}
func GetOrgList(query *m.GetOrgListQuery) error {
return x.Find(&query.Result)
func SearchOrgs(query *m.SearchOrgsQuery) error {
query.Result = make([]*m.OrgDTO, 0)
sess := x.Table("org")
if query.Query != "" {
sess.Where("name LIKE ?", query.Query+"%")
}
if query.Name != "" {
sess.Where("name=?", query.Name)
}
sess.Limit(query.Limit, query.Limit*query.Page)
sess.Cols("id", "name")
err := sess.Find(&query.Result)
return err
}
func GetOrgById(query *m.GetOrgByIdQuery) error {

View File

@ -142,11 +142,18 @@ func TestAccountDataAccess(t *testing.T) {
})
})
Convey("Cannot delete last admin account user", func() {
Convey("Cannot delete last admin org user", func() {
cmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac1.Id}
err := RemoveOrgUser(&cmd)
So(err, ShouldEqual, m.ErrLastOrgAdmin)
})
Convey("Cannot update role so no one is admin user", func() {
cmd := m.UpdateOrgUserCommand{OrgId: ac1.OrgId, UserId: ac1.Id, Role: m.ROLE_VIEWER}
err := UpdateOrgUser(&cmd)
So(err, ShouldEqual, m.ErrLastOrgAdmin)
})
})
})
})

View File

@ -48,7 +48,11 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error {
orgUser.Role = cmd.Role
orgUser.Updated = time.Now()
_, err = sess.Id(orgUser.Id).Update(&orgUser)
return err
if err != nil {
return err
}
return validateOneAdminLeftInOrg(cmd.OrgId, sess)
})
}
@ -72,16 +76,20 @@ func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
return err
}
// validate that there is an admin user left
res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and role='Admin'", cmd.OrgId)
if err != nil {
return err
}
if len(res) == 0 {
return m.ErrLastOrgAdmin
}
return err
return validateOneAdminLeftInOrg(cmd.OrgId, sess)
})
}
func validateOneAdminLeftInOrg(orgId int64, sess *xorm.Session) error {
// validate that there is an admin user left
res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and role='Admin'", orgId)
if err != nil {
return err
}
if len(res) == 0 {
return m.ErrLastOrgAdmin
}
return err
}

View File

@ -231,10 +231,12 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
}
query.Result = m.UserProfileDTO{
Name: user.Name,
Email: user.Email,
Login: user.Login,
Theme: user.Theme,
Name: user.Name,
Email: user.Email,
Login: user.Login,
Theme: user.Theme,
IsGrafanaAdmin: user.IsAdmin,
OrgId: user.OrgId,
}
return err
@ -282,6 +284,11 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
return m.ErrUserNotFound
}
if user.OrgRole == "" {
user.OrgId = -1
user.OrgName = "Org missing"
}
query.Result = &user
return err
}

View File

@ -79,6 +79,7 @@ var (
AllowUserOrgCreate bool
AutoAssignOrg bool
AutoAssignOrgRole string
ViewerRoleMode string
// Http auth
AdminUser string
@ -383,6 +384,7 @@ func NewConfigContext(args *CommandLineArgs) {
AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
ViewerRoleMode = users.Key("viewer_role_mode").In("default", []string{"default", "strinct"})
// anonymous access
AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)

View File

@ -78,12 +78,14 @@ func NewOAuthService() {
if name == "github" {
setting.OAuthService.GitHub = true
teamIds := sec.Key("team_ids").Ints(",")
allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
SocialMap["github"] = &SocialGithub{
Config: &config,
allowedDomains: info.AllowedDomains,
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
teamIds: teamIds,
Config: &config,
allowedDomains: info.AllowedDomains,
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
teamIds: teamIds,
allowedOrganizations: allowedOrganizations,
}
}
@ -115,16 +117,21 @@ func isEmailAllowed(email string, allowedDomains []string) bool {
type SocialGithub struct {
*oauth2.Config
allowedDomains []string
apiUrl string
allowSignup bool
teamIds []int
allowedDomains []string
allowedOrganizations []string
apiUrl string
allowSignup bool
teamIds []int
}
var (
ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
)
var (
ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
)
func (s *SocialGithub) Type() int {
return int(models.GITHUB)
}
@ -137,26 +144,131 @@ func (s *SocialGithub) IsSignupAllowed() bool {
return s.allowSignup
}
func (s *SocialGithub) IsTeamMember(client *http.Client, username string, teamId int) bool {
var data struct {
Url string `json:"url"`
State string `json:"state"`
func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
if len(s.teamIds) == 0 {
return true
}
membershipUrl := fmt.Sprintf("https://api.github.com/teams/%d/memberships/%s", teamId, username)
r, err := client.Get(membershipUrl)
teamMemberships, err := s.FetchTeamMemberships(client)
if err != nil {
return false
}
defer r.Body.Close()
for _, teamId := range s.teamIds {
for _, membershipId := range teamMemberships {
if teamId == membershipId {
return true
}
}
}
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
return false
}
func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool {
if len(s.allowedOrganizations) == 0 {
return true
}
organizations, err := s.FetchOrganizations(client)
if err != nil {
return false
}
active := data.State == "active"
return active
for _, allowedOrganization := range s.allowedOrganizations {
for _, organization := range organizations {
if organization == allowedOrganization {
return true
}
}
}
return false
}
func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
type Record struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
emailsUrl := fmt.Sprintf("https://api.github.com/user/emails")
r, err := client.Get(emailsUrl)
if err != nil {
return "", err
}
defer r.Body.Close()
var records []Record
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
return "", err
}
var email = ""
for _, record := range records {
if record.Primary {
email = record.Email
}
}
return email, nil
}
func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) {
type Record struct {
Id int `json:"id"`
}
membershipUrl := fmt.Sprintf("https://api.github.com/user/teams")
r, err := client.Get(membershipUrl)
if err != nil {
return nil, err
}
defer r.Body.Close()
var records []Record
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
return nil, err
}
var ids = make([]int, len(records))
for i, record := range records {
ids[i] = record.Id
}
return ids, nil
}
func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) {
type Record struct {
Login string `json:"login"`
}
url := fmt.Sprintf("https://api.github.com/user/orgs")
r, err := client.Get(url)
if err != nil {
return nil, err
}
defer r.Body.Close()
var records []Record
if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
return nil, err
}
var logins = make([]string, len(records))
for i, record := range records {
logins[i] = record.Login
}
return logins, nil
}
func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
@ -185,17 +297,22 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
Email: data.Email,
}
if len(s.teamIds) > 0 {
for _, teamId := range s.teamIds {
if s.IsTeamMember(client, data.Name, teamId) {
return userInfo, nil
}
}
if !s.IsTeamMember(client) {
return nil, ErrMissingTeamMembership
} else {
return userInfo, nil
}
if !s.IsOrganizationMember(client) {
return nil, ErrMissingOrganizationMembership
}
if userInfo.Email == "" {
userInfo.Email, err = s.FetchPrivateEmail(client)
if err != nil {
return nil, err
}
}
return userInfo, nil
}
// ________ .__

View File

@ -1,5 +1,5 @@
define(['jquery'],
function ($) {
define(['jquery', 'angular', 'lodash'],
function ($, angular, _) {
'use strict';
/**
@ -14,6 +14,7 @@ function ($) {
return function (x, y, opts) {
opts = $.extend(true, {}, defaults, opts);
return this.each(function () {
var $tooltip = $(this), width, height;
@ -22,6 +23,17 @@ function ($) {
$("#tooltip").remove();
$tooltip.appendTo(document.body);
if (opts.compile) {
angular.element(document).injector().invoke(function($compile, $rootScope) {
var tmpScope = $rootScope.$new(true);
_.extend(tmpScope, opts.scopeData);
$compile($tooltip)(tmpScope);
tmpScope.$digest();
//tmpScope.$destroy();
});
}
width = $tooltip.outerWidth(true);
height = $tooltip.outerHeight(true);

View File

@ -383,8 +383,9 @@ function($, _, moment) {
kbn.valueFormats.mbytes = kbn.formatFuncCreator(1024, [' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']);
kbn.valueFormats.gbytes = kbn.formatFuncCreator(1024, [' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB']);
kbn.valueFormats.bps = kbn.formatFuncCreator(1000, [' bps', ' Kbps', ' Mbps', ' Gbps', ' Tbps', ' Pbps', ' Ebps', ' Zbps', ' Ybps']);
kbn.valueFormats.pps = kbn.formatFuncCreator(1000, [' pps', ' Kpps', ' Mpps', ' Gpps', ' Tpps', ' Ppps', ' Epps', ' Zpps', ' Ypps']);
kbn.valueFormats.Bps = kbn.formatFuncCreator(1000, [' Bps', ' KBps', ' MBps', ' GBps', ' TBps', ' PBps', ' EBps', ' ZBps', ' YBps']);
kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Qaudr', ' Quint', ' Sext', ' Sept']);
kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']);
kbn.valueFormats.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', ' GJ', ' TJ', ' PJ', ' EJ', ' ZJ', ' YJ']);
kbn.valueFormats.amp = kbn.formatFuncCreator(1000, [' A', ' kA', ' MA', ' GA', ' TA', ' PA', ' EA', ' ZA', ' YA']);
kbn.valueFormats.volt = kbn.formatFuncCreator(1000, [' V', ' kV', ' MV', ' GV', ' TV', ' PV', ' EV', ' ZV', ' YV']);
@ -564,6 +565,7 @@ function($, _, moment) {
{
text: 'data rate',
submenu: [
{text: 'packets/sec', value: 'pps'},
{text: 'bits/sec', value: 'bps'},
{text: 'bytes/sec', value: 'Bps'},
]

View File

@ -16,8 +16,8 @@ function () {
this.addMenuItem('view', 'icon-eye-open', 'toggleFullscreen(false); dismiss();');
}
this.addMenuItem('edit', 'icon-cog', 'editPanel(); dismiss();');
this.addMenuItem('duplicate', 'icon-copy', 'duplicatePanel()');
this.addMenuItem('edit', 'icon-cog', 'editPanel(); dismiss();', 'Editor');
this.addMenuItem('duplicate', 'icon-copy', 'duplicatePanel()', 'Editor');
this.addMenuItem('share', 'icon-share', 'sharePanel(); dismiss();');
this.addEditorTab('General', 'app/partials/panelgeneral.html');
@ -29,12 +29,12 @@ function () {
this.addExtendedMenuItem('Panel JSON', '', 'editPanelJson(); dismiss();');
}
PanelMeta.prototype.addMenuItem = function(text, icon, click) {
this.menu.push({text: text, icon: icon, click: click});
PanelMeta.prototype.addMenuItem = function(text, icon, click, role) {
this.menu.push({text: text, icon: icon, click: click, role: role});
};
PanelMeta.prototype.addExtendedMenuItem = function(text, icon, click) {
this.extendedMenu.push({text: text, icon: icon, click: click});
PanelMeta.prototype.addExtendedMenuItem = function(text, icon, click, role) {
this.extendedMenu.push({text: text, icon: icon, click: click, role: role});
};
PanelMeta.prototype.addEditorTab = function(title, src) {

View File

@ -13,8 +13,8 @@ function (angular, _, config) {
$scope.init = function() {
$scope.giveSearchFocus = 0;
$scope.selectedIndex = -1;
$scope.results = {dashboards: [], tags: [], metrics: []};
$scope.query = { query: '', tag: '', starred: false };
$scope.results = [];
$scope.query = { query: '', tag: [], starred: false };
$scope.currentSearchId = 0;
if ($scope.dashboardViewState.fullscreen) {
@ -26,7 +26,6 @@ function (angular, _, config) {
$scope.query.query = '';
$scope.search();
}, 100);
};
$scope.keyDown = function (evt) {
@ -83,12 +82,11 @@ function (angular, _, config) {
$scope.queryHasNoFilters = function() {
var query = $scope.query;
return query.query === '' && query.starred === false && query.tag === '';
return query.query === '' && query.starred === false && query.tag.length === 0;
};
$scope.filterByTag = function(tag, evt) {
$scope.query.tag = tag;
$scope.query.tagcloud = false;
$scope.query.tag.push(tag);
$scope.search();
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
if (evt) {
@ -97,6 +95,14 @@ function (angular, _, config) {
}
};
$scope.removeTag = function(tag, evt) {
$scope.query.tag = _.without($scope.query.tag, tag);
$scope.search();
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
evt.stopPropagation();
evt.preventDefault();
};
$scope.getTags = function() {
return backendSrv.get('/api/dashboards/tags').then(function(results) {
$scope.tagsMode = true;
@ -123,32 +129,4 @@ function (angular, _, config) {
});
module.directive('tagColorFromName', function() {
function djb2(str) {
var hash = 5381;
for (var i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
}
return hash;
}
return {
scope: { tag: "=" },
link: function (scope, element) {
var name = scope.tag;
var hash = djb2(name.toLowerCase());
var colors = [
"#E24D42","#1F78C1","#BA43A9","#705DA0","#466803",
"#508642","#447EBC","#C15C17","#890F02","#757575",
"#0A437C","#6D1F62","#584477","#629E51","#2F4F4F",
"#BF1B00","#806EB7","#8a2eb8", "#699e00","#000000",
"#3F6833","#2F575E","#99440A","#E0752D","#0E4AB4",
"#58140C","#052B51","#511749","#3F2B5B",
];
var color = colors[Math.abs(hash % colors.length)];
element.css("background-color", color);
}
};
});
});

View File

@ -5,16 +5,17 @@ define([
'./ngBlur',
'./dashEditLink',
'./ngModelOnBlur',
'./tip',
'./misc',
'./confirmClick',
'./configModal',
'./spectrumPicker',
'./bootstrap-tagsinput',
'./tags',
'./bodyClass',
'./variableValueSelect',
'./graphiteSegment',
'./metric.segment',
'./grafanaVersionCheck',
'./dropdown.typeahead',
'./topnav',
'./giveFocus',
'./annotationTooltip',
], function () {});

View File

@ -0,0 +1,49 @@
define([
'angular',
'jquery',
'lodash'
],
function (angular, $, _) {
'use strict';
angular
.module('grafana.directives')
.directive('annotationTooltip', function($sanitize, dashboardSrv, $compile) {
return {
link: function (scope, element) {
var event = scope.event;
var title = $sanitize(event.title);
var dashboard = dashboardSrv.getCurrent();
var time = '<i>' + dashboard.formatDate(event.min) + '</i>';
var tooltip = '<div class="graph-tooltip small"><div class="graph-tooltip-time">' + title + ' ' + time + '</div> ' ;
if (event.text) {
var text = $sanitize(event.text);
tooltip += text.replace(/\n/g, '<br>') + '<br>';
}
var tags = event.tags;
if (_.isString(event.tags)) {
tags = event.tags.split(',');
if (tags.length === 1) {
tags = event.tags.split(' ');
}
}
if (tags && tags.length) {
scope.tags = tags;
tooltip += '<span class="label label-tag" ng-repeat="tag in tags" tag-color-from-name="tag">{{tag}}</span><br/>';
}
tooltip += "</div>";
var $tooltip = $(tooltip);
$tooltip.appendTo(element);
$compile(element.contents())(scope);
}
};
});
});

View File

@ -1,134 +0,0 @@
define([
'angular',
'jquery',
'bootstrap-tagsinput'
],
function (angular, $) {
'use strict';
angular
.module('grafana.directives')
.directive('bootstrapTagsinput', function() {
function getItemProperty(scope, property) {
if (!property) {
return undefined;
}
if (angular.isFunction(scope.$parent[property])) {
return scope.$parent[property];
}
return function(item) {
return item[property];
};
}
return {
restrict: 'EA',
scope: {
model: '=ngModel'
},
template: '<select multiple></select>',
replace: false,
link: function(scope, element, attrs) {
if (!angular.isArray(scope.model)) {
scope.model = [];
}
var select = $('select', element);
if (attrs.placeholder) {
select.attr('placeholder', attrs.placeholder);
}
select.tagsinput({
typeahead : {
source : angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null
},
itemValue: getItemProperty(scope, attrs.itemvalue),
itemText : getItemProperty(scope, attrs.itemtext),
tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ?
scope.$parent[attrs.tagclass] : function() { return attrs.tagclass; }
});
select.on('itemAdded', function(event) {
if (scope.model.indexOf(event.item) === -1) {
scope.model.push(event.item);
}
});
select.on('itemRemoved', function(event) {
var idx = scope.model.indexOf(event.item);
if (idx !== -1) {
scope.model.splice(idx, 1);
}
});
scope.$watch("model", function() {
if (!angular.isArray(scope.model)) {
scope.model = [];
}
select.tagsinput('removeAll');
for (var i = 0; i < scope.model.length; i++) {
select.tagsinput('add', scope.model[i]);
}
}, true);
}
};
});
angular
.module('grafana.directives')
.directive('gfDropdown', function ($parse, $compile, $timeout) {
function buildTemplate(items, placement) {
var upclass = placement === 'top' ? 'dropup' : '';
var ul = [
'<ul class="dropdown-menu ' + upclass + '" role="menu" aria-labelledby="drop1">',
'</ul>'
];
angular.forEach(items, function (item, index) {
if (item.divider) {
return ul.splice(index + 1, 0, '<li class="divider"></li>');
}
var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
'<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
(item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
(item.configModal ? ' dash-editor-link="' + item.configModal + '"' : "") +
'>' + (item.text || '') + '</a>';
if (item.submenu && item.submenu.length) {
li += buildTemplate(item.submenu).join('\n');
}
li += '</li>';
ul.splice(index + 1, 0, li);
});
return ul;
}
return {
restrict: 'EA',
scope: true,
link: function postLink(scope, iElement, iAttrs) {
var getter = $parse(iAttrs.gfDropdown), items = getter(scope);
$timeout(function () {
var placement = iElement.data('placement');
var dropdown = angular.element(buildTemplate(items, placement).join(''));
dropdown.insertAfter(iElement);
$compile(iElement.next('ul.dropdown-menu'))(scope);
});
iElement.addClass('dropdown-toggle').attr('data-toggle', 'dropdown');
}
};
});
});

View File

@ -9,14 +9,21 @@ function (angular, app, _, $) {
angular
.module('grafana.directives')
.directive('graphiteSegment', function($compile, $sce) {
.directive('metricSegment', function($compile, $sce) {
var inputTemplate = '<input type="text" data-provide="typeahead" ' +
' class="tight-form-clear-input input-medium"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="tight-form-item" tabindex="1" focus-me="segment.focus" ng-bind-html="segment.html"></a>';
var buttonTemplate = '<a class="tight-form-item" ng-class="segment.cssClass" ' +
'tabindex="1" focus-me="segment.focus" ng-bind-html="segment.html"></a>';
return {
scope: {
segment: "=",
getAltSegments: "&",
onValueChanged: "&"
},
link: function($scope, elem) {
var $input = $(inputTemplate);
var $button = $(buttonTemplate);
@ -46,7 +53,7 @@ function (angular, app, _, $) {
segment.expandable = true;
segment.fake = false;
}
$scope.segmentValueChanged(segment, $scope.$index);
$scope.onValueChanged();
});
};
@ -61,7 +68,7 @@ function (angular, app, _, $) {
else {
// need to have long delay because the blur
// happens long before the click event on the typeahead options
cancelBlur = setTimeout($scope.switchToLink, 350);
cancelBlur = setTimeout($scope.switchToLink, 50);
}
};
@ -69,7 +76,8 @@ function (angular, app, _, $) {
if (options) { return options; }
$scope.$apply(function() {
$scope.getAltSegments($scope.$index).then(function() {
$scope.getAltSegments().then(function(altSegments) {
$scope.altSegments = altSegments;
options = _.map($scope.altSegments, function(alt) { return alt.value; });
// add custom values

View File

@ -78,4 +78,53 @@ function (angular, kbn) {
};
});
angular
.module('grafana.directives')
.directive('gfDropdown', function ($parse, $compile, $timeout) {
function buildTemplate(items, placement) {
var upclass = placement === 'top' ? 'dropup' : '';
var ul = [
'<ul class="dropdown-menu ' + upclass + '" role="menu" aria-labelledby="drop1">',
'</ul>'
];
angular.forEach(items, function (item, index) {
if (item.divider) {
return ul.splice(index + 1, 0, '<li class="divider"></li>');
}
var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
'<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
(item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
(item.configModal ? ' dash-editor-link="' + item.configModal + '"' : "") +
'>' + (item.text || '') + '</a>';
if (item.submenu && item.submenu.length) {
li += buildTemplate(item.submenu).join('\n');
}
li += '</li>';
ul.splice(index + 1, 0, li);
});
return ul;
}
return {
restrict: 'EA',
scope: true,
link: function postLink(scope, iElement, iAttrs) {
var getter = $parse(iAttrs.gfDropdown), items = getter(scope);
$timeout(function () {
var placement = iElement.data('placement');
var dropdown = angular.element(buildTemplate(items, placement).join(''));
dropdown.insertAfter(iElement);
$compile(iElement.next('ul.dropdown-menu'))(scope);
});
iElement.addClass('dropdown-toggle').attr('data-toggle', 'dropdown');
}
};
});
});

View File

@ -0,0 +1,137 @@
define([
'angular',
'jquery',
'bootstrap-tagsinput'
],
function (angular, $) {
'use strict';
function djb2(str) {
var hash = 5381;
for (var i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
}
return hash;
}
function setColor(name, element) {
var hash = djb2(name.toLowerCase());
var colors = [
"#E24D42","#1F78C1","#BA43A9","#705DA0","#466803",
"#508642","#447EBC","#C15C17","#890F02","#757575",
"#0A437C","#6D1F62","#584477","#629E51","#2F4F4F",
"#BF1B00","#806EB7","#8a2eb8", "#699e00","#000000",
"#3F6833","#2F575E","#99440A","#E0752D","#0E4AB4",
"#58140C","#052B51","#511749","#3F2B5B",
];
var borderColors = [
"#FF7368","#459EE7","#E069CF","#9683C6","#6C8E29",
"#76AC68","#6AA4E2","#E7823D","#AF3528","#9B9B9B",
"#3069A2","#934588","#7E6A9D","#88C477","#557575",
"#E54126","#A694DD","#B054DE", "#8FC426","#262626",
"#658E59","#557D84","#BF6A30","#FF9B53","#3470DA",
"#7E3A32","#2B5177","#773D6F","#655181",
];
var color = colors[Math.abs(hash % colors.length)];
var borderColor = borderColors[Math.abs(hash % borderColors.length)];
element.css("background-color", color);
element.css("border-color", borderColor);
}
angular
.module('grafana.directives')
.directive('tagColorFromName', function() {
return {
scope: { tagColorFromName: "=" },
link: function (scope, element) {
setColor(scope.tagColorFromName, element);
}
};
});
angular
.module('grafana.directives')
.directive('bootstrapTagsinput', function() {
function getItemProperty(scope, property) {
if (!property) {
return undefined;
}
if (angular.isFunction(scope.$parent[property])) {
return scope.$parent[property];
}
return function(item) {
return item[property];
};
}
return {
restrict: 'EA',
scope: {
model: '=ngModel',
onTagsUpdated: "&",
},
template: '<select multiple></select>',
replace: false,
link: function(scope, element, attrs) {
if (!angular.isArray(scope.model)) {
scope.model = [];
}
var select = $('select', element);
if (attrs.placeholder) {
select.attr('placeholder', attrs.placeholder);
}
select.tagsinput({
typeahead: {
source: angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null
},
itemValue: getItemProperty(scope, attrs.itemvalue),
itemText : getItemProperty(scope, attrs.itemtext),
tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ?
scope.$parent[attrs.tagclass] : function() { return attrs.tagclass; }
});
select.on('itemAdded', function(event) {
if (scope.model.indexOf(event.item) === -1) {
scope.model.push(event.item);
if (scope.onTagsUpdated) {
scope.onTagsUpdated();
}
}
var tagElement = select.next().children("span").filter(function() { return $(this).text() === event.item; });
setColor(event.item, tagElement);
});
select.on('itemRemoved', function(event) {
var idx = scope.model.indexOf(event.item);
if (idx !== -1) {
scope.model.splice(idx, 1);
if (scope.onTagsUpdated) {
scope.onTagsUpdated();
}
}
});
scope.$watch("model", function() {
if (!angular.isArray(scope.model)) {
scope.model = [];
}
select.tagsinput('removeAll');
for (var i = 0; i < scope.model.length; i++) {
select.tagsinput('add', scope.model[i]);
}
}, true);
}
};
});
});

View File

@ -7,151 +7,270 @@ define([
function (angular, app, _) {
'use strict';
angular
.module('grafana.controllers')
.controller('SelectDropdownCtrl', function($q) {
var vm = this;
vm.show = function() {
vm.oldVariableText = vm.variable.current.text;
vm.highlightIndex = -1;
var currentValues = vm.variable.current.value;
if (_.isString(currentValues)) {
currentValues = [currentValues];
}
vm.options = _.map(vm.variable.options, function(option) {
if (_.indexOf(currentValues, option.value) >= 0) { option.selected = true; }
return option;
});
_.sortBy(vm.options, 'text');
vm.selectedValues = _.filter(vm.options, {selected: true});
vm.tags = _.map(vm.variable.tags, function(value) {
return { text: value, selected: false };
});
vm.search = {query: '', options: vm.options};
vm.dropdownVisible = true;
};
vm.updateLinkText = function() {
var current = vm.variable.current;
var currentValues = current.value;
if (_.isArray(currentValues) && current.tags.length) {
// filer out values that are in selected tags
currentValues = _.filter(currentValues, function(test) {
for (var i = 0; i < current.tags.length; i++) {
if (_.indexOf(current.tags[i].values, test) !== -1) {
return false;
}
}
return true;
});
// convert values to text
var currentTexts = _.map(currentValues, function(value) {
for (var i = 0; i < vm.variable.options.length; i++) {
var option = vm.variable.options[i];
if (option.value === value) {
return option.text;
}
}
return value;
});
// join texts
vm.linkText = currentTexts.join(' + ');
if (vm.linkText.length > 0) {
vm.linkText += ' + ';
}
} else {
vm.linkText = vm.variable.current.text;
}
};
vm.clearSelections = function() {
_.each(vm.options, function(option) {
option.selected = false;
});
vm.selectionsChanged(false);
};
vm.selectTag = function(tag) {
tag.selected = !tag.selected;
var tagValuesPromise;
if (!tag.values) {
tagValuesPromise = vm.getValuesForTag({tagKey: tag.text});
} else {
tagValuesPromise = $q.when(tag.values);
}
tagValuesPromise.then(function(values) {
tag.values = values;
tag.valuesText = values.join(' + ');
_.each(vm.options, function(option) {
if (_.indexOf(tag.values, option.value) !== -1) {
option.selected = tag.selected;
}
});
vm.selectionsChanged(false);
});
};
vm.keyDown = function (evt) {
if (evt.keyCode === 27) {
vm.hide();
}
if (evt.keyCode === 40) {
vm.moveHighlight(1);
}
if (evt.keyCode === 38) {
vm.moveHighlight(-1);
}
if (evt.keyCode === 13) {
vm.optionSelected(vm.search.options[vm.highlightIndex], {}, true, false);
}
if (evt.keyCode === 32) {
vm.optionSelected(vm.search.options[vm.highlightIndex], {}, false, false);
}
};
vm.moveHighlight = function(direction) {
vm.highlightIndex = (vm.highlightIndex + direction) % vm.search.options.length;
};
vm.selectValue = function(option, event, commitChange, excludeOthers) {
if (!option) { return; }
option.selected = !option.selected;
commitChange = commitChange || false;
excludeOthers = excludeOthers || false;
var setAllExceptCurrentTo = function(newValue) {
_.each(vm.options, function(other) {
if (option !== other) { other.selected = newValue; }
});
};
// commit action (enter key), should not deselect it
if (commitChange) {
option.selected = true;
}
if (option.text === 'All' || excludeOthers) {
setAllExceptCurrentTo(false);
commitChange = true;
}
else if (!vm.variable.multi) {
setAllExceptCurrentTo(false);
commitChange = true;
} else if (event.ctrlKey || event.metaKey || event.shiftKey) {
commitChange = true;
setAllExceptCurrentTo(false);
}
vm.selectionsChanged(commitChange);
};
vm.selectionsChanged = function(commitChange) {
vm.selectedValues = _.filter(vm.options, {selected: true});
if (vm.selectedValues.length > 1 && vm.selectedValues.length !== vm.options.length) {
if (vm.selectedValues[0].text === 'All') {
vm.selectedValues[0].selected = false;
vm.selectedValues = vm.selectedValues.slice(1, vm.selectedValues.length);
}
}
// validate selected tags
_.each(vm.tags, function(tag) {
if (tag.selected) {
_.each(tag.values, function(value) {
if (!_.findWhere(vm.selectedValues, {value: value})) {
tag.selected = false;
}
});
}
});
vm.selectedTags = _.filter(vm.tags, {selected: true});
vm.variable.current.value = _.pluck(vm.selectedValues, 'value');
vm.variable.current.text = _.pluck(vm.selectedValues, 'text').join(' + ');
vm.variable.current.tags = vm.selectedTags;
// only single value
if (vm.selectedValues.length === 1) {
vm.variable.current.value = vm.selectedValues[0].value;
}
if (commitChange) {
vm.commitChanges();
}
};
vm.commitChanges = function() {
// make sure one option is selected
if (vm.selectedValues.length === 0) {
vm.options[0].selected = true;
vm.selectionsChanged(false);
}
vm.dropdownVisible = false;
vm.updateLinkText();
if (vm.variable.current.text !== vm.oldVariableText) {
vm.onUpdated();
}
};
vm.queryChanged = function() {
vm.highlightIndex = -1;
vm.search.options = _.filter(vm.options, function(option) {
return option.text.toLowerCase().indexOf(vm.search.query.toLowerCase()) !== -1;
});
};
vm.init = function() {
vm.selectedTags = vm.variable.current.tags || [];
vm.updateLinkText();
};
});
angular
.module('grafana.directives')
.directive('variableValueSelect', function($compile, $window, $timeout) {
return {
scope: {
variable: "=",
onUpdated: "&"
},
scope: { variable: "=", onUpdated: "&", getValuesForTag: "&" },
templateUrl: 'app/features/dashboard/partials/variableValueSelect.html',
controller: 'SelectDropdownCtrl',
controllerAs: 'vm',
bindToController: true,
link: function(scope, elem) {
var bodyEl = angular.element($window.document.body);
var variable = scope.variable;
var linkEl = elem.find('.variable-value-link');
var inputEl = elem.find('input');
scope.show = function() {
if (scope.selectorOpen) {
return;
}
function openDropdown() {
inputEl.css('width', Math.max(linkEl.width(), 30) + 'px');
scope.selectorOpen = true;
scope.giveFocus = 1;
scope.oldCurrentText = variable.current.text;
scope.highlightIndex = -1;
inputEl.show();
linkEl.hide();
var currentValues = variable.current.value;
inputEl.focus();
$timeout(function() { bodyEl.on('click', bodyOnClick); }, 0, false);
}
if (_.isString(currentValues)) {
currentValues = [currentValues];
}
function switchToLink() {
inputEl.hide();
linkEl.show();
bodyEl.off('click', bodyOnClick);
}
scope.options = _.map(variable.options, function(option) {
if (_.indexOf(currentValues, option.value) >= 0) {
option.selected = true;
}
return option;
});
scope.search = {query: '', options: scope.options};
$timeout(function() {
bodyEl.on('click', scope.bodyOnClick);
}, 0, false);
};
scope.queryChanged = function() {
scope.highlightIndex = -1;
scope.search.options = _.filter(scope.options, function(option) {
return option.text.toLowerCase().indexOf(scope.search.query.toLowerCase()) !== -1;
});
};
scope.keyDown = function (evt) {
if (evt.keyCode === 27) {
scope.hide();
}
if (evt.keyCode === 40) {
scope.moveHighlight(1);
}
if (evt.keyCode === 38) {
scope.moveHighlight(-1);
}
if (evt.keyCode === 13) {
scope.optionSelected(scope.search.options[scope.highlightIndex], {});
}
};
scope.moveHighlight = function(direction) {
scope.highlightIndex = (scope.highlightIndex + direction) % scope.search.options.length;
};
scope.optionSelected = function(option, event) {
option.selected = !option.selected;
var hideAfter = true;
var setAllExceptCurrentTo = function(newValue) {
_.each(scope.options, function(other) {
if (option !== other) { other.selected = newValue; }
function bodyOnClick (e) {
if (elem.has(e.target).length === 0) {
scope.$apply(function() {
scope.vm.commitChanges();
});
};
if (option.text === 'All') {
setAllExceptCurrentTo(false);
}
else if (!variable.multi) {
setAllExceptCurrentTo(false);
}
scope.$watch('vm.dropdownVisible', function(newValue) {
if (newValue) {
openDropdown();
} else {
if (event.ctrlKey || event.metaKey || event.shiftKey) {
hideAfter = false;
}
else {
setAllExceptCurrentTo(false);
}
switchToLink();
}
var selected = _.filter(scope.options, {selected: true});
if (selected.length === 0) {
option.selected = true;
selected = [option];
}
if (selected.length > 1 && selected.length !== scope.options.length) {
if (selected[0].text === 'All') {
selected[0].selected = false;
selected = selected.slice(1, selected.length);
}
}
variable.current = {
text: _.pluck(selected, 'text').join(', '),
value: _.pluck(selected, 'value'),
};
// only single value
if (variable.current.value.length === 1) {
variable.current.value = selected[0].value;
}
scope.updateLinkText();
scope.onUpdated();
if (hideAfter) {
scope.hide();
}
};
scope.hide = function() {
scope.selectorOpen = false;
bodyEl.off('click', scope.bodyOnClick);
};
scope.bodyOnClick = function(e) {
var dropdown = elem.find('.variable-value-dropdown');
if (dropdown.has(e.target).length === 0) {
scope.$apply(scope.hide);
}
};
scope.updateLinkText = function() {
scope.labelText = variable.label || '$' + variable.name;
scope.linkText = variable.current.text;
};
scope.$watchGroup(['variable.hideLabel', 'variable.name', 'variable.label', 'variable.current.text'], function() {
scope.updateLinkText();
});
scope.vm.init();
},
};
});

View File

@ -1,23 +1,26 @@
define([
'angular',
'lodash',
],
function (angular) {
function (angular, _) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('AdminEditUserCtrl', function($scope, $routeParams, backendSrv, $location) {
$scope.user = {};
$scope.newOrg = { name: '', role: 'Editor' };
$scope.permissions = {};
$scope.init = function() {
if ($routeParams.id) {
$scope.getUser($routeParams.id);
$scope.getUserOrgs($routeParams.id);
}
};
$scope.getUser = function(id) {
backendSrv.get('/api/admin/users/' + id).then(function(user) {
backendSrv.get('/api/users/' + id).then(function(user) {
$scope.user = user;
$scope.user_id = id;
$scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin;
@ -49,14 +52,58 @@ function (angular) {
});
};
$scope.getUserOrgs = function(id) {
backendSrv.get('/api/users/' + id + '/orgs').then(function(orgs) {
$scope.orgs = orgs;
});
};
$scope.update = function() {
if (!$scope.userForm.$valid) { return; }
backendSrv.put('/api/admin/users/' + $scope.user_id + '/details', $scope.user).then(function() {
backendSrv.put('/api/users/' + $scope.user_id, $scope.user).then(function() {
$location.path('/admin/users');
});
};
$scope.updateOrgUser= function(orgUser) {
backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser).then(function() {
});
};
$scope.removeOrgUser = function(orgUser) {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(function() {
$scope.getUserOrgs($scope.user_id);
});
};
$scope.orgsSearchCache = [];
$scope.searchOrgs = function(queryStr, callback) {
if ($scope.orgsSearchCache.length > 0) {
callback(_.pluck($scope.orgsSearchCache, "name"));
return;
}
backendSrv.get('/api/orgs', {query: ''}).then(function(result) {
$scope.orgsSearchCache = result;
callback(_.pluck(result, "name"));
});
};
$scope.addOrgUser = function() {
if (!$scope.addOrgForm.$valid) { return; }
var orgInfo = _.findWhere($scope.orgsSearchCache, {name: $scope.newOrg.name});
if (!orgInfo) { return; }
$scope.newOrg.loginOrEmail = $scope.user.login;
backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(function() {
$scope.getUserOrgs($scope.user_id);
});
};
$scope.init();
});

View File

@ -13,7 +13,7 @@ function (angular) {
};
$scope.getUsers = function() {
backendSrv.get('/api/admin/users').then(function(users) {
backendSrv.get('/api/users').then(function(users) {
$scope.users = users;
});
};

View File

@ -25,7 +25,7 @@
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" style="margin-top: 5px">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
<strong>Email</strong>
@ -36,7 +36,7 @@
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" style="margin-top: 5px">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
<strong>Username</strong>
@ -80,19 +80,73 @@
Permissions
</h2>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item last">
Grafana Admin&nbsp;
<input class="cr1" id="permissions.isGrafanaAdmin" type="checkbox"
ng-model="permissions.isGrafanaAdmin" ng-checked="permissions.isGrafanaAdmin">
<label for="permissions.isGrafanaAdmin" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
<div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item last">
Grafana Admin&nbsp;
<input class="cr1" id="permissions.isGrafanaAdmin" type="checkbox"
ng-model="permissions.isGrafanaAdmin" ng-checked="permissions.isGrafanaAdmin">
<label for="permissions.isGrafanaAdmin" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
<br>
<button type="submit" class="pull-right btn btn-success" ng-click="updatePermissions()">Update</button>
<br>
</div>
<br>
<button type="submit" class="pull-right btn btn-success" ng-click="updatePermissions()">Update</button>
<h2>
Organizations
</h2>
<form name="addOrgForm">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 160px">
Add organization
</li>
<li>
<input type="text" ng-model="newOrg.name" bs-typeahead="searchOrgs"
required class="input-xlarge tight-form-input" placeholder="organization name">
</li>
<li class="tight-form-item">
Role
</li>
<li>
<select type="text" ng-model="newOrg.role" class="input-small tight-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']">
</select>
</li>
<li>
<button class="btn btn-success tight-form-btn" ng-click="addOrgUser()">Add</button>
</li>
<div class="clearfix"></div>
</ul>
</div>
</form>
<table class="grafana-options-table form-inline">
<tr>
<th>Name</th>
<th>Role</th>
<th></th>
</tr>
<tr ng-repeat="org in orgs">
<td>
{{org.name}} <span class="label label-info" ng-show="org.orgId === user.orgId">Current</span>
</td>
<td>
<select type="text" ng-model="org.role" class="input-small" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(org)">
</select>
</td>
<td style="width: 1%">
<a ng-click="removeOrgUser(org)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</table>
</div>
</div>

View File

@ -24,7 +24,7 @@
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" style="margin-top: 5px">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
<strong>Email</strong>
@ -35,7 +35,7 @@
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" style="margin-top: 5px">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
<strong>Username</strong>
@ -46,7 +46,7 @@
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" style="margin-top: 5px">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
<strong>Password</strong>

View File

@ -1,4 +1,4 @@
<topnav icon="fa fa-fw fa-user" title="Global users" subnav="true">
<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
<ul class="nav">
<li class="active"><a href="admin/users">Overview</a></li>
<li><a href="admin/users/create">Create user</a></li>

View File

@ -1,17 +1,15 @@
define([
'angular',
'lodash',
'moment',
'./editorCtrl'
], function (angular, _, moment) {
], function (angular, _) {
'use strict';
var module = angular.module('grafana.services');
module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope, $sanitize) {
module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope) {
var promiseCached;
var list = [];
var timezone;
var self = this;
this.init = function() {
@ -33,7 +31,7 @@ define([
return promiseCached;
}
timezone = dashboard.timezone;
self.dashboard = dashboard;
var annotations = _.where(dashboard.annotations.list, {enable: true});
var promises = _.map(annotations, function(annotation) {
@ -54,49 +52,29 @@ define([
this.receiveAnnotationResults = function(results) {
for (var i = 0; i < results.length; i++) {
addAnnotation(results[i]);
self.addAnnotation(results[i]);
}
};
this.addAnnotation = function(options) {
list.push({
annotation: options.annotation,
min: options.time,
max: options.time,
eventType: options.annotation.name,
title: options.title,
tags: options.tags,
text: options.text,
score: 1
});
};
function errorHandler(err) {
console.log('Annotation error: ', err);
var message = err.message || "Annotation query failed";
alertSrv.set('Annotations error', message,'error');
}
function addAnnotation(options) {
var title = $sanitize(options.title);
var tooltip = "<small><b>" + title + "</b><br/>";
if (options.tags) {
var tags = $sanitize(options.tags);
tooltip += '<span class="tag label label-tag">' + (tags || '') + '</span><br/>';
}
if (timezone === 'browser') {
tooltip += '<i>' + moment(options.time).format('YYYY-MM-DD HH:mm:ss') + '</i><br/>';
}
else {
tooltip += '<i>' + moment.utc(options.time).format('YYYY-MM-DD HH:mm:ss') + '</i><br/>';
}
if (options.text) {
var text = $sanitize(options.text);
tooltip += text.replace(/\n/g, '<br/>');
}
tooltip += "</small>";
list.push({
annotation: options.annotation,
min: options.time,
max: options.time,
eventType: options.annotation.name,
title: null,
description: tooltip,
score: 1
});
}
// Now init
this.init();
});

View File

@ -1,26 +1,38 @@
<span class="template-variable" ng-show="!variable.hideLabel" style="padding-right: 5px">
{{labelText}}:
</span>
<div style="position: relative; display: inline-block">
<a ng-click="show()" class="variable-value-link">
{{linkText}}
<div class="variable-link-wrapper">
<a ng-click="vm.show()" class="variable-value-link tight-form-item">
{{vm.linkText}}
<span ng-repeat="tag in vm.selectedTags" bs-tooltip='tag.valuesText' data-placement="bottom">
<span class="label-tag"tag-color-from-name="tag.text">
&nbsp;&nbsp;<i class="fa fa-tag"></i>&nbsp;
{{tag.text}}
</span>
</span>
<i class="fa fa-caret-down"></i>
</a>
<div ng-if="selectorOpen" class="variable-value-dropdown">
<div class="variable-search-wrapper">
<span style="position: relative;">
<input type="text" placeholder="Search values..." ng-keydown="keyDown($event)" give-focus="giveFocus" tabindex="1" ng-model="search.query" spellcheck='false' ng-change="queryChanged()" />
</span>
</div>
<input type="text" class="tight-form-clear-input input-small" style="display: none" ng-keydown="vm.keyDown($event)" ng-model="vm.search.query" ng-change="vm.queryChanged()" ></input>
<div class="variable-options-container" ng-if="!query.tagcloud">
<a class="variable-option pointer" bindonce ng-repeat="option in search.options"
ng-class="{'selected': option.selected, 'highlighted': $index === highlightIndex}" ng-click="optionSelected(option, $event)">
<span >{{option.text}}</label>
<span class="fa fa-fw variable-option-icon"></span>
</a>
<div class="variable-value-dropdown" ng-if="vm.dropdownVisible" ng-class="{'multi': vm.variable.multi, 'single': !vm.variable.multi}">
<div class="variable-options-wrapper">
<div class="variable-options-column">
<a class="variable-options-column-header" ng-if="vm.variable.multi" ng-class="{'many-selected': vm.selectedValuesCount > 1}" bs-tooltip="'Clear selections'" data-placement="top" ng-click="vm.clearSelections()">
<span class="variable-option-icon"></span>
Selected ({{vm.selectedValues.length}})
</a>
<a class="variable-option pointer" bindonce ng-repeat="option in vm.search.options" ng-class="{'selected': option.selected, 'highlighted': $index === vm.highlightIndex}" ng-click="vm.selectValue(option, $event)">
<span class="variable-option-icon"></span>
<span>{{option.text}}</span>
</a>
</div>
<div class="variable-options-column" ng-if="vm.tags.length">
<div class="variable-options-column-header text-center">
Tags
</div>
<a class="variable-option-tag pointer" ng-repeat="tag in vm.tags" ng-click="vm.selectTag(tag, $event)" ng-class="{'selected': tag.selected}">
<span class="fa fa-fw variable-option-icon"></span>
<span class="label-tag" tag-color-from-name="tag.text">{{tag.text}}&nbsp;&nbsp;<i class="fa fa-tag"></i>&nbsp;</span>
</a>
</div>
</div>
</div>
</div>

View File

@ -43,9 +43,9 @@ function (angular, _, require, config) {
var params = angular.copy($location.search());
var range = timeSrv.timeRangeForUrl();
params.from = range.from;
params.to = range.to;
var range = timeSrv.timeRange();
params.from = range.from.getTime();
params.to = range.to.getTime();
if ($scope.options.includeTemplateVars) {
templateSrv.fillVariableValuesForUrl(params);

View File

@ -17,8 +17,8 @@ function (angular, _) {
$scope.init = function() {
$scope.panel = $scope.pulldown;
$scope.row = $scope.pulldown;
$scope.variables = $scope.dashboard.templating.list;
$scope.annotations = $scope.dashboard.templating.list;
$scope.variables = $scope.dashboard.templating.list;
};
$scope.disableAnnotation = function (annotation) {
@ -26,6 +26,10 @@ function (angular, _) {
$rootScope.$broadcast('refresh');
};
$scope.getValuesForTag = function(variable, tagKey) {
return templateValuesSrv.getValuesForTag(variable, tagKey);
};
$scope.variableUpdated = function(variable) {
templateValuesSrv.variableUpdated(variable).then(function() {
dynamicDashboardSrv.update($scope.dashboard);

View File

@ -93,7 +93,7 @@ define([
_.extend(this.time, time);
// disable refresh if we have an absolute time
if (time.to !== 'now') {
if (_.isString(time.to) && time.to.indexOf('now') === -1) {
this.old_refresh = this.dashboard.refresh || this.old_refresh;
this.set_interval(false);
}

View File

@ -130,10 +130,11 @@ function (angular, _, $) {
var docHeight = $(window).height();
var editHeight = Math.floor(docHeight * 0.3);
var fullscreenHeight = Math.floor(docHeight * 0.7);
this.oldTimeRange = panelScope.range;
panelScope.height = this.state.edit ? editHeight : fullscreenHeight;
panelScope.editMode = this.state.edit;
panelScope.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit;
panelScope.height = panelScope.editMode ? editHeight : fullscreenHeight;
this.oldTimeRange = panelScope.range;
this.fullscreenPanel = panelScope;
$(window).scrollTop(0);

View File

@ -23,9 +23,10 @@
<select class="input-medium tight-form-input" style="width: 150px;" ng-model="link.type" ng-options="f for f in ['dashboards','link']" ng-change="updated()"></select>
</li>
<li class="tight-form-item" ng-show="link.type === 'dashboards'">With tag</li>
<li class="tight-form-item" ng-show="link.type === 'dashboards'">With tags</li>
<li ng-show="link.type === 'dashboards'">
<input type="text" ng-model="link.tag" class="input-small tight-form-input" style="width: 151px" ng-model-onblur ng-change="updated()">
<bootstrap-tagsinput ng-model="link.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
</li>
<li class="tight-form-item" ng-show="link.type === 'dashboards'">
<editor-checkbox text="As dropdown" model="link.asDropdown" change="updated()"></editor-checkbox>

View File

@ -89,7 +89,7 @@ function (angular, _) {
function buildLinks(linkDef) {
if (linkDef.type === 'dashboards') {
if (!linkDef.tag) {
if (!linkDef.tags) {
console.log('Dashboard link missing tag');
return $q.when([]);
}
@ -97,7 +97,7 @@ function (angular, _) {
if (linkDef.asDropdown) {
return $q.when([{
title: linkDef.title,
tag: linkDef.tag,
tags: linkDef.tags,
keepTime: linkDef.keepTime,
includeVars: linkDef.includeVars,
icon: "fa fa-bars",
@ -132,7 +132,7 @@ function (angular, _) {
}
$scope.searchDashboards = function(link) {
return backendSrv.search({tag: link.tag}).then(function(results) {
return backendSrv.search({tag: link.tags}).then(function(results) {
return _.reduce(results, function(memo, dash) {
// do not add current dashboard
if (dash.id !== currentDashId) {

View File

@ -25,7 +25,6 @@ function (angular, config) {
$scope.loadDatasourceTypes().then(function() {
if ($routeParams.id) {
$scope.isNew = false;
$scope.getDatasourceById($routeParams.id);
} else {
$scope.current = angular.copy(defaults);
@ -48,6 +47,7 @@ function (angular, config) {
$scope.getDatasourceById = function(id) {
backendSrv.get('/api/datasources/' + id).then(function(ds) {
$scope.isNew = false;
$scope.current = ds;
$scope.typeChanged();
});
@ -65,26 +65,55 @@ function (angular, config) {
});
};
$scope.update = function() {
if (!$scope.editForm.$valid) {
return;
}
$scope.testDatasource = function() {
$scope.testing = { done: false };
backendSrv.post('/api/datasources', $scope.current).then(function() {
$scope.updateFrontendSettings();
$location.path("datasources");
datasourceSrv.get($scope.current.name).then(function(datasource) {
if (!datasource.testDatasource) {
$scope.testing.message = 'Data source does not support test connection feature.';
$scope.testing.status = 'warning';
$scope.testing.title = 'Unknown';
return;
}
return datasource.testDatasource().then(function(result) {
$scope.testing.message = result.message;
$scope.testing.status = result.status;
$scope.testing.title = result.title;
}, function(err) {
if (err.statusText) {
$scope.testing.message = err.statusText;
$scope.testing.title = "HTTP Error";
} else {
$scope.testing.message = err.message;
$scope.testing.title = "Unknown error";
}
});
}).finally(function() {
$scope.testing.done = true;
});
};
$scope.add = function() {
$scope.saveChanges = function(test) {
if (!$scope.editForm.$valid) {
return;
}
backendSrv.put('/api/datasources', $scope.current).then(function() {
$scope.updateFrontendSettings();
$location.path("datasources");
});
if ($scope.current.id) {
return backendSrv.put('/api/datasources/' + $scope.current.id, $scope.current).then(function() {
$scope.updateFrontendSettings();
if (test) {
$scope.testDatasource();
} else {
$location.path('datasources');
}
});
} else {
return backendSrv.post('/api/datasources', $scope.current).then(function(result) {
$scope.updateFrontendSettings();
$location.path('datasources/edit/' + result.id);
});
}
};
$scope.init();

View File

@ -11,7 +11,7 @@ function (angular) {
$scope.newOrg = {name: ''};
$scope.createOrg = function() {
backendSrv.post('/api/org/', $scope.newOrg).then($scope.getUserOrgs);
backendSrv.post('/api/orgs/', $scope.newOrg).then($scope.getUserOrgs);
};
});

View File

@ -35,6 +35,8 @@ function (angular) {
src: './app/features/org/partials/apikeyModal.html',
scope: modalScope
});
$scope.getTokens();
});
};

View File

@ -43,11 +43,22 @@
</div>
<div ng-include="datasourceMeta.partials.config" ng-if="datasourceMeta.partials.config"></div>
<br>
<br>
<div class="pull-right">
<button type="submit" class="btn btn-success" ng-show="isNew" ng-click="add()">Add</button>
<button type="submit" class="btn btn-success" ng-show="!isNew" ng-click="update()">Update</button>
<div ng-if="testing" style="margin-top: 25px">
<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
<h5 ng-show="testing.done">Test results</h5>
<div class="alert-{{testing.status}} alert">
<div class="alert-title">{{testing.title}}</div>
<div ng-bind='testing.message'></div>
</div>
</div>
<div class="pull-right" style="margin-top: 35px">
<button type="submit" class="btn btn-success" ng-show="isNew" ng-click="saveChanges()">Add</button>
<button type="submit" class="btn btn-success" ng-show="!isNew" ng-click="saveChanges()">Save</button>
<button type="submit" class="btn btn-inverse" ng-show="!isNew" ng-click="saveChanges(true)">
Test Connection
</button>
<a class="btn btn-inverse" ng-show="!isNew" href="datasources">Cancel</a>
</div>
<br>

View File

@ -6,7 +6,7 @@
Url
</li>
<li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.url' placeholder="http://my.server.com:8080" required></input>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.url' placeholder="http://my.server.com:8080" ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
</li>
<li class="tight-form-item">
Access <tip>Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request</label>

View File

@ -7,12 +7,12 @@
<div class="page-container">
<div class="page">
<h2>Account users</h2>
<h2>Organization users</h2>
<form name="form">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 160px">
<li class="tight-form-item" style="width: 127px">
<strong>Username or Email</strong>
</li>
<li>
@ -22,7 +22,7 @@
role
</li>
<li>
<select type="text" ng-model="user.role" class="input-small tight-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Admin']">
<select type="text" ng-model="user.role" class="input-medium tight-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']">
</select>
</li>
<li>
@ -46,7 +46,7 @@
<td>{{user.login}}</td>
<td>{{user.email}}</td>
<td>
<select type="text" ng-model="user.role" class="input-small" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="updateOrgUser(user)">
<select type="text" ng-model="user.role" class="input-medium" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(user)">
</select>
</td>
<td style="width: 1%">

View File

@ -18,18 +18,26 @@ function (angular, $, _) {
function createMenuTemplate($scope) {
var template = '<div class="panel-menu small">';
template += '<div class="panel-menu-inner">';
template += '<div class="panel-menu-row">';
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
template += '<a class="panel-menu-icon pull-right" ng-click="removePanel(panel)"><i class="fa fa-remove"></i></a>';
template += '<div class="clearfix"></div>';
template += '</div>';
if ($scope.dashboardMeta.canEdit) {
template += '<div class="panel-menu-inner">';
template += '<div class="panel-menu-row">';
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
template += '<a class="panel-menu-icon pull-right" ng-click="removePanel(panel)"><i class="fa fa-remove"></i></a>';
template += '<div class="clearfix"></div>';
template += '</div>';
}
template += '<div class="panel-menu-row">';
template += '<a class="panel-menu-link" gf-dropdown="extendedMenu"><i class="fa fa-bars"></i></a>';
_.each($scope.panelMeta.menu, function(item) {
// skip edit actions if not editor
if (item.role === 'Editor' && !$scope.dashboardMeta.canEdit) {
return;
}
template += '<a class="panel-menu-link" ';
if (item.click) { template += ' ng-click="' + item.click + '"'; }
if (item.editorLink) { template += ' dash-editor-link="' + item.editorLink + '"'; }
@ -61,7 +69,6 @@ function (angular, $, _) {
link: function($scope, elem) {
var $link = $(linkTemplate);
var $panelContainer = elem.parents(".panel-container");
var menuWidth = $scope.panelMeta.menu.length === 4 ? 236 : 191;
var menuScope = null;
var timeout = null;
var $menu = null;
@ -111,21 +118,8 @@ function (angular, $, _) {
return;
}
var windowWidth = $(window).width();
var panelLeftPos = $(elem).offset().left;
var panelWidth = $(elem).width();
var menuLeftPos = (panelWidth / 2) - (menuWidth/2);
var stickingOut = panelLeftPos + menuLeftPos + menuWidth - windowWidth;
if (stickingOut > 0) {
menuLeftPos -= stickingOut + 10;
}
if (panelLeftPos + menuLeftPos < 0) {
menuLeftPos = 0;
}
var menuTemplate = createMenuTemplate($scope);
$menu = $(menuTemplate);
$menu.css('left', menuLeftPos);
$menu.mouseleave(function() {
dismiss(1000);
});
@ -136,15 +130,35 @@ function (angular, $, _) {
dismiss(null, true);
};
$('.panel-menu').remove();
elem.append($menu);
$scope.$apply(function() {
$compile($menu.contents())(menuScope);
});
$(".panel-container").removeClass('panel-highlight');
$panelContainer.toggleClass('panel-highlight');
$('.panel-menu').remove();
elem.append($menu);
$scope.$apply(function() {
$compile($menu.contents())(menuScope);
var menuWidth = $menu[0].offsetWidth;
var menuHeight = $menu[0].offsetHeight;
var windowWidth = $(window).width();
var panelLeftPos = $(elem).offset().left;
var panelWidth = $(elem).width();
var menuLeftPos = (panelWidth / 2) - (menuWidth/2);
var stickingOut = panelLeftPos + menuLeftPos + menuWidth - windowWidth;
if (stickingOut > 0) {
menuLeftPos -= stickingOut + 10;
}
if (panelLeftPos + menuLeftPos < 0) {
menuLeftPos = 0;
}
$menu.css({'left': menuLeftPos, top: -menuHeight});
});
dismiss(2200);
};

View File

@ -71,14 +71,6 @@ function (angular, _, config) {
};
$scope.toggleFullscreen = function(edit) {
if (edit && $scope.dashboardMeta.canEdit === false) {
$scope.appEvent('alert-warning', [
'Dashboard not editable',
'Use Save As.. feature to create an editable copy of this dashboard.'
]);
return;
}
$scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
};

View File

@ -71,10 +71,10 @@
<td style="width: 98%"><strong>Name: </strong> {{org.name}}</td>
<td><strong>Role: </strong> {{org.role}}</td>
<td class="nobg max-width-btns">
<span class="btn btn-primary btn-mini" ng-show="org.isUsing">
<span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId">
Current
</span>
<a ng-click="setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="!org.isUsing">
<a ng-click="setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
Select
</a>
</td>

View File

@ -65,7 +65,7 @@
Name
</li>
<li>
<input type="text" class="input-xlarge tight-form-input" placeholder="apps.servers.*" ng-model='current.name'></input>
<input type="text" class="input-large tight-form-input" placeholder="name" ng-model='current.name'></input>
</li>
<li class="tight-form-item">
Type
@ -139,7 +139,7 @@
Query
</li>
<li>
<input type="text" style="width: 646px" class="input-xxlarge tight-form-input last" placeholder="name" ng-model='current.query' placeholder="apps.servers.*" ng-model-onblur ng-change="runQuery()"></input>
<input type="text" style="width: 588px" class="input-xxlarge tight-form-input last" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()"></input>
</li>
</ul>
<div class="clearfix"></div>
@ -151,7 +151,7 @@
<tip>Optional, if you want to extract part of a series name or metric node segment</tip>
</li>
<li>
<input type="text" style="width: 646px" class="input tight-form-input last" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
<input type="text" style="width: 588px" class="input tight-form-input last" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
</li>
</ul>
<div class="clearfix"></div>
@ -163,7 +163,7 @@
<editor-checkbox text="All value" model="current.includeAll" change="runQuery()"></editor-checkbox>
</li>
<li ng-show="current.includeAll">
<input type="text" class="input-xlarge tight-form-input" style="width:422px" ng-model='current.options[0].value'></input>
<input type="text" class="input-xlarge tight-form-input" style="width:364px" ng-model='current.options[0].value'></input>
</li>
<li class="tight-form-item" ng-show="current.includeAll">
All format
@ -226,6 +226,42 @@
</div>
</div>
<div class="editor-row" ng-if="current.type === 'query'">
<div class="tight-form-section">
<h5>Value groups/tags (Experimental feature)</h5>
<div class="tight-form" ng-if="current.useTags">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 135px">
Tags query
</li>
<li>
<input type="text" style="width: 588px" class="input-xxlarge tight-form-input last" ng-model='current.tagsQuery' placeholder="metric name or tags query" ng-model-onblur></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-if="current.useTags">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 135px;">
Tag values query
</li>
<li>
<input type="text" style="width: 588px" class="input tight-form-input last" ng-model='current.tagValuesQuery' placeholder="apps.$__tag.*" ng-model-onblur></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item last">
<editor-checkbox text="Enable" model="current.useTags" change="runQuery()"></editor-checkbox>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="editor-row">
<div class="tight-form-section">
<h5>Preview of values (shows max 20)</h5>

View File

@ -78,7 +78,7 @@ function (angular, _, kbn) {
};
this.setVariableValue = function(variable, option) {
variable.current = option;
variable.current = angular.copy(option);
templateSrv.updateTemplateData();
return this.updateOptionsInChildVariables(variable);
};
@ -120,7 +120,7 @@ function (angular, _, kbn) {
}
return datasourceSrv.get(variable.datasource).then(function(datasource) {
return datasource.metricFindQuery(variable.query).then(function (results) {
var queryPromise = datasource.metricFindQuery(variable.query).then(function (results) {
variable.options = self.metricNamesToVariableValues(variable, results);
if (variable.includeAll) {
@ -130,6 +130,10 @@ function (angular, _, kbn) {
// if parameter has current value
// if it exists in options array keep value
if (variable.current) {
// if current value is an array do not do anything
if (_.isArray(variable.current.value)) {
return $q.when([]);
}
var currentOption = _.findWhere(variable.options, { text: variable.current.text });
if (currentOption) {
return self.setVariableValue(variable, currentOption);
@ -138,6 +142,31 @@ function (angular, _, kbn) {
return self.setVariableValue(variable, variable.options[0]);
});
if (variable.useTags) {
return queryPromise.then(function() {
datasource.metricFindQuery(variable.tagsQuery).then(function (results) {
variable.tags = [];
for (var i = 0; i < results.length; i++) {
variable.tags.push(results[i].text);
}
});
});
} else {
delete variable.tags;
return queryPromise;
}
});
};
this.getValuesForTag = function(variable, tagKey) {
return datasourceSrv.get(variable.datasource).then(function(datasource) {
var query = variable.tagValuesQuery.replace('$tag', tagKey);
return datasource.metricFindQuery(query).then(function (results) {
return _.map(results, function(value) {
return value.text;
});
});
});
};

View File

@ -23,15 +23,15 @@
Query
</li>
<li>
<input type="text" class="input-small tight-form-input" placeholder="title query"
<input type="text" class="input-medium tight-form-input" placeholder="title query"
ng-model="panel.query" ng-change="get_data()" ng-model-onblur>
</li>
<li class="tight-form-item">
Tag
Tags
</li>
<li>
<input type="text" class="input-small tight-form-input" placeholder="full tag name"
ng-model="panel.tag" ng-change="get_data()" ng-model-onblur>
<bootstrap-tagsinput ng-model="panel.tags" tagclass="label label-tag" placeholder="add tags" on-tags-updated="get_data()">
</bootstrap-tagsinput>
</li>
</ul>
<div class="clearfix"></div>

View File

@ -32,7 +32,7 @@ function (angular, app, _, config, PanelMeta) {
mode: 'starred',
query: '',
limit: 10,
tag: '',
tags: []
};
$scope.modes = ['starred', 'search'];
@ -43,6 +43,9 @@ function (angular, app, _, config, PanelMeta) {
$scope.init = function() {
panelSrv.init($scope);
if ($scope.panel.tag) {
$scope.panel.tags = [$scope.panel.tag];
}
if ($scope.isNewPanel()) {
$scope.panel.title = "Starred Dashboards";
@ -58,7 +61,7 @@ function (angular, app, _, config, PanelMeta) {
params.starred = "true";
} else {
params.query = $scope.panel.query;
params.tag = $scope.panel.tag;
params.tag = $scope.panel.tags;
}
return backendSrv.search(params).then(function(result) {

View File

@ -480,6 +480,9 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
case 'bps':
url += '&yUnitSystem=si';
break;
case 'pps':
url += '&yUnitSystem=si';
break;
case 'Bps':
url += '&yUnitSystem=si';
break;

View File

@ -190,7 +190,12 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) {
data.flotpairs = $scope.series[0].flotpairs;
}
// first check value to text mappings
var decimalInfo = $scope.getDecimalsForValue(data.value);
var formatFunc = kbn.valueFormats[$scope.panel.format];
data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
// check value to text mappings
for(var i = 0; i < $scope.panel.valueMaps.length; i++) {
var map = $scope.panel.valueMaps[i];
// special null case
@ -201,6 +206,7 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) {
}
continue;
}
// value/number to text mapping
var value = parseFloat(map.value);
if (value === data.value) {
@ -212,11 +218,6 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) {
if (data.value === null || data.value === void 0) {
data.valueFormated = "no value";
}
var decimalInfo = $scope.getDecimalsForValue(data.value);
var formatFunc = kbn.valueFormats[$scope.panel.format];
data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
};
$scope.removeValueMap = function(map) {

View File

@ -19,7 +19,7 @@
<form name="loginForm" class="login-form">
<div class="tight-form" ng-if="loginMode">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<li class="tight-form-item" style="width: 78px">
<strong>User</strong>
</li>
<li>
@ -30,7 +30,7 @@
</div>
<div class="tight-form" ng-if="loginMode">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
<li class="tight-form-item" style="width: 78px">
<strong>Password</strong>
</li>
<li>

View File

@ -15,11 +15,14 @@
<i class="fa fa-remove" ng-show="tagsMode"></i>
tags
</a>
<span ng-show="query.tag">
| <a ng-click="filterByTag('')" tag-color-from-name tag="query.tag" class="label label-tag" ng-if="query.tag">
<i class="fa fa-remove"></i>
{{query.tag}}
</a>
<span ng-if="query.tag.length">
|
<span ng-repeat="tagName in query.tag">
<a ng-click="removeTag(tagName, $event)" tag-color-from-name="tagName" class="label label-tag">
<i class="fa fa-remove"></i>
{{tagName}}
</a>
</span>
</span>
</div>
</div>
@ -30,7 +33,7 @@
<div ng-repeat="tag in results" class="pointer" style="width: 180px; float: left;"
ng-class="{'selected': $index === selectedIndex }"
ng-click="filterByTag(tag.term, $event)">
<a class="search-result-tag label label-tag" tag-color-from-name tag="tag.term">
<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
<i class="fa fa-tag"></i>
<span>{{tag.term}} &nbsp;({{tag.count}})</span>
</a>
@ -46,7 +49,7 @@
ng-class="{'selected': $index == selectedIndex}" ng-href="{{row.url}}">
<span class="search-result-tags">
<span ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name tag="tag" class="label label-tag">
<span ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>

View File

@ -3,7 +3,10 @@
<ul class="tight-form-list" ng-if="dashboard.templating.list.length > 0">
<li ng-repeat="variable in variables" class="submenu-item">
<variable-value-select variable="variable" on-updated="variableUpdated(variable)"></variable-value-select>
<span class="template-variable tight-form-item" ng-show="!variable.hideLabel" style="padding-right: 5px">
{{variable.label || variable.name}}:
</span>
<variable-value-select variable="variable" on-updated="variableUpdated(variable)" get-values-for-tag="getValuesForTag(variable, tagKey)"></variable-value-select>
</li>
</ul>

View File

@ -111,6 +111,7 @@ function (angular, _, $, config, kbn, moment) {
var list = [];
for (var i = 0; i < results.data.length; i++) {
var e = results.data[i];
list.push({
annotation: annotation,
time: e.when * 1000,
@ -195,6 +196,12 @@ function (angular, _, $, config, kbn, moment) {
});
};
GraphiteDatasource.prototype.testDatasource = function() {
return this.metricFindQuery('*').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
});
};
GraphiteDatasource.prototype.listDashboards = function(query) {
return this.doGraphiteRequest({ method: 'GET', url: '/dashboard/find/', params: {query: query || ''} })
.then(function(results) {

View File

@ -74,7 +74,9 @@
ng-show="showTextEditor" />
<ul class="tight-form-list" role="menu" ng-hide="showTextEditor">
<li ng-repeat="segment in segments" role="menuitem" graphite-segment></li>
<li ng-repeat="segment in segments" role="menuitem">
<metric-segment segment="segment" get-alt-segments="getAltSegments($index)" on-value-changed="segmentValueChanged(segment, $index)"></metric-segment>
</li>
<li ng-repeat="func in functions">
<span graphite-func-editor class="tight-form-item tight-form-func">
</span>

View File

@ -152,23 +152,18 @@ function (angular, _, config, gfunc, Parser) {
}
$scope.getAltSegments = function (index) {
$scope.altSegments = [];
var query = index === 0 ? '*' : getSegmentPathUpTo(index) + '.*';
return $scope.datasource.metricFindQuery(query)
.then(function(segments) {
$scope.altSegments = _.map(segments, function(segment) {
return $scope.datasource.metricFindQuery(query).then(function(segments) {
var altSegments = _.map(segments, function(segment) {
return new MetricSegment({ value: segment.text, expandable: segment.expandable });
});
if ($scope.altSegments.length === 0) {
return;
}
if (altSegments.length === 0) { return altSegments; }
// add template variables
_.each(templateSrv.variables, function(variable) {
$scope.altSegments.unshift(new MetricSegment({
altSegments.unshift(new MetricSegment({
type: 'template',
value: '$' + variable.name,
expandable: true,
@ -176,10 +171,12 @@ function (angular, _, config, gfunc, Parser) {
});
// add wildcard option
$scope.altSegments.unshift(new MetricSegment('*'));
altSegments.unshift(new MetricSegment('*'));
return altSegments;
})
.then(null, function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];
});
};

View File

@ -43,7 +43,6 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
// build query
var queryBuilder = new InfluxQueryBuilder(target);
var query = queryBuilder.build();
console.log('query builder result:' + query);
// replace grafana variables
query = query.replace('$timeFilter', timeFilter);
@ -69,12 +68,15 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
var query = annotation.query.replace('$timeFilter', timeFilter);
query = templateSrv.replace(query);
return this._seriesQuery(query).then(function(results) {
return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations();
return this._seriesQuery(query).then(function(data) {
if (!data || !data.results || !data.results[0]) {
throw { message: 'No results in response from InfluxDB' };
}
return new InfluxSeries({ series: data.results[0].series, annotation: annotation }).getAnnotations();
});
};
InfluxDatasource.prototype.metricFindQuery = function (query, queryType) {
InfluxDatasource.prototype.metricFindQuery = function (query) {
var interpolated;
try {
interpolated = templateSrv.replace(query);
@ -83,39 +85,33 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
return $q.reject(err);
}
console.log('metricFindQuery called with: ' + [query, queryType].join(', '));
return this._seriesQuery(interpolated, queryType).then(function (results) {
return this._seriesQuery(interpolated).then(function (results) {
if (!results || results.results.length === 0) { return []; }
var influxResults = results.results[0];
if (!influxResults.series) {
return [];
}
console.log('metric find query response', results);
var series = influxResults.series[0];
switch (queryType) {
case 'MEASUREMENTS':
if (query.indexOf('SHOW MEASUREMENTS') === 0) {
return _.map(series.values, function(value) { return { text: value[0], expandable: true }; });
case 'TAG_KEYS':
var tagKeys = _.flatten(series.values);
return _.map(tagKeys, function(tagKey) { return { text: tagKey, expandable: true }; });
case 'TAG_VALUES':
var tagValues = _.flatten(series.values);
return _.map(tagValues, function(tagValue) { return { text: tagValue, expandable: true }; });
default: // template values service does not pass in a a query type
var flattenedValues = _.flatten(series.values);
return _.map(flattenedValues, function(value) { return { text: value, expandable: true }; });
}
var flattenedValues = _.flatten(series.values);
return _.map(flattenedValues, function(value) { return { text: value, expandable: true }; });
});
};
function retry(deferred, callback, delay) {
return callback().then(undefined, function(reason) {
if (reason.status !== 0 || reason.status >= 300) {
reason.message = 'InfluxDB Error: <br/>' + reason.data;
if (reason.data && reason.data.error) {
reason.message = 'InfluxDB Error Response: ' + reason.data.error;
}
else {
reason.message = 'InfluxDB Error: ' + reason.message;
}
deferred.reject(reason);
}
else {
@ -130,6 +126,12 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
return this._influxRequest('GET', '/query', {q: query});
};
InfluxDatasource.prototype.testDatasource = function() {
return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
});
};
InfluxDatasource.prototype._influxRequest = function(method, url, data) {
var self = this;
var deferred = $q.defer();
@ -174,9 +176,11 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
return deferred.promise;
};
function handleInfluxQueryResponse(alias, seriesList) {
var influxSeries = new InfluxSeries({ seriesList: seriesList, alias: alias });
return influxSeries.getTimeSeries();
function handleInfluxQueryResponse(alias, data) {
if (!data || !data.results || !data.results[0].series) {
return [];
}
return new InfluxSeries({ series: data.results[0].series, alias: alias }).getTimeSeries();
}
function getTimeFilter(options) {

View File

@ -108,7 +108,7 @@ function (angular, _, $) {
function addElementsAndCompile() {
$funcLink.appendTo(elem);
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + $scope.target.column + '</a>');
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">value</a>');
var $input = $(paramTemplate);
$paramLink.appendTo(elem);

View File

@ -5,8 +5,7 @@ function (_) {
'use strict';
function InfluxSeries(options) {
this.seriesList = options.seriesList && options.seriesList.results && options.seriesList.results.length > 0
? options.seriesList.results[0].series || [] : [];
this.series = options.series;
this.alias = options.alias;
this.annotation = options.annotation;
}
@ -17,23 +16,25 @@ function (_) {
var output = [];
var self = this;
console.log(self.seriesList);
if (self.seriesList.length === 0) {
if (self.series.length === 0) {
return output;
}
_.each(self.seriesList, function(series) {
_.each(self.series, function(series) {
var datapoints = [];
for (var i = 0; i < series.values.length; i++) {
datapoints[i] = [series.values[i][1], new Date(series.values[i][0]).getTime()];
}
var seriesName = series.name;
var tags = _.map(series.tags, function(value, key) {
return key + ': ' + value;
});
if (tags.length > 0) {
if (self.alias) {
seriesName = self._getSeriesName(series);
} else if (series.tags) {
var tags = _.map(series.tags, function(value, key) {
return key + ': ' + value;
});
seriesName = seriesName + ' {' + tags.join(', ') + '}';
}
@ -43,11 +44,26 @@ function (_) {
return output;
};
p._getSeriesName = function(series) {
var regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g;
return this.alias.replace(regex, function(match, g1, g2) {
var group = g1 || g2;
if (group === 'm' || group === 'measurement') { return series.name; }
if (group.indexOf('tag_') !== 0) { return match; }
var tag = group.replace('tag_', '');
if (!series.tags) { return match; }
return series.tags[tag];
});
};
p.getAnnotations = function () {
var list = [];
var self = this;
_.each(this.seriesList, function (series) {
_.each(this.series, function (series) {
var titleCol = null;
var timeCol = null;
var tagsCol = null;

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