mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into ldap
This commit is contained in:
commit
2c7d33cdfa
@ -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)
|
||||
|
||||
|
@ -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 .
|
||||
```
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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]
|
||||
|
@ -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**']
|
||||
|
@ -27,7 +27,7 @@ Open a graph in edit mode by click the title.
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||

|
||||
|
||||
Grafana can query any Elasticsearch index for annotation events. The index name can be the name of an alias or an index wildcard pattern.
|
||||
|
@ -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`.
|
||||
|
@ -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`
|
||||
|
@ -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
|
||||
|
||||
|
@ -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).
|
||||
|
||||

|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2"
|
||||
}
|
||||
|
@ -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 = "************"
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
122
pkg/api/common.go
Normal 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),
|
||||
}
|
||||
}
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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"`
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
}
|
||||
|
127
pkg/api/user.go
127
pkg/api/user.go
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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"`
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
61
pkg/search/handlers_test.go
Normal file
61
pkg/search/handlers_test.go
Normal 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")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
@ -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{
|
||||
|
@ -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)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
// ________ .__
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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'},
|
||||
]
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@ -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 () {});
|
||||
|
49
public/app/directives/annotationTooltip.js
Normal file
49
public/app/directives/annotationTooltip.js
Normal 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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
134
public/app/directives/bootstrap-tagsinput.js
vendored
134
public/app/directives/bootstrap-tagsinput.js
vendored
@ -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');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
@ -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
|
@ -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');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
137
public/app/directives/tags.js
Normal file
137
public/app/directives/tags.js
Normal 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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
@ -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();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -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();
|
||||
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
@ -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
|
||||
<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
|
||||
<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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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">
|
||||
<i class="fa fa-tag"></i>
|
||||
{{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}} <i class="fa fa-tag"></i> </span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
});
|
||||
|
@ -35,6 +35,8 @@ function (angular) {
|
||||
src: './app/features/org/partials/apikeyModal.html',
|
||||
scope: modalScope
|
||||
});
|
||||
|
||||
$scope.getTokens();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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%">
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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 });
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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}} ({{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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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 [];
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user