Merge branch 'develop' into develop-newgrid-rows

This commit is contained in:
Torkel Ödegaard 2017-08-17 15:56:38 +02:00
commit 637ff61f90
143 changed files with 1968 additions and 1587 deletions

3
.gitignore vendored
View File

@ -41,3 +41,6 @@ profile.cov
/examples/*/dist
/packaging/**/*.rpm
/packaging/**/*.deb
# Ignore OSX indexing
.DS_Store

View File

@ -88,7 +88,7 @@ You can switch to raw query mode by clicking hamburger icon and then `Switch edi
- $m = replaced with measurement name
- $measurement = replaced with measurement name
- $col = replaced with column name
- $tag_exampletag = replaced with the value of the `exampletag` tag. To use your tag as an alias in the ALIAS BY field then the tag must be used to group by in the query.
- $tag_exampletag = replaced with the value of the `exampletag` tag. The syntax is `$tag_yourTagName` (must start with `$tag_`). To use your tag as an alias in the ALIAS BY field then the tag must be used to group by in the query.
- You can also use [[tag_hostname]] pattern replacement syntax. For example, in the ALIAS BY field using this text `Host: [[tag_hostname]]` would substitute in the `hostname` tag value for each legend value and an example legend value would be: `Host: server1`.
### Table query / raw data

View File

@ -29,8 +29,7 @@ data from a MySQL compatible database.
The database user you specify when you add the data source should only be granted SELECT permissions on
the specified database & tables you want to query. Grafana does not validate that the query is safe. The query
could include any SQL statement. For example, statements like `USE otherdb;` and `DROP TABLE user;` would be
executed. To protect against this we **Highly** recommmend you create a specific mysql user with
restricted permissions.
executed. To protect against this we **Highly** recommmend you create a specific mysql user with restricted permissions.
Example:
@ -49,11 +48,9 @@ Macro example | Description
------------ | -------------
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > FROM_UNIXTIME(1494410783) AND dateColumn < FROM_UNIXTIME(1494497183)*
We plan to add many more macros. If you have suggestions for what macros you would like to see, please
[open an issue](https://github.com/grafana/grafana) in our GitHub repo.
We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
The query editor has a link named `Generated SQL` that show up after a query as been executed, while in panel edit mode. Click
on it and it will expand and show the raw interpolated SQL string that was executed.
The query editor has a link named `Generated SQL` that show up after a query as been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed.
## Table queries
@ -109,8 +106,71 @@ This is something we plan to add.
## Templating
You can use variables in your queries but there are currently no support for defining `Query` variables
that target a MySQL data source.
This feature is currently available in the nightly builds and will be included in the 5.0.0 release.
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns makes it easy to change the data being displayed in your dashboard.
Checkout the [Templating]({{< relref "reference/templating.md" >}}) documentation for an introduction to the templating feature and the different types of template variables.
### Query Variable
If you add a template variable of the type `Query`, you can write a MySQL query that can
return things like measurement names, key names or key values that are shown as a dropdown select box.
For example, you can have a variable that contains all values for the `hostname` column in a table if you specify a query like this in the templating variable *Query* setting.
```sql
SELECT hostname FROM my_host
```
A query can returns multiple columns and Grafana will automatically create a list from them. For example, the query below will return a list with values from `hostname` and `hostname2`.
```sql
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
```
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
```sql
SELECT hostname AS __text, id AS __value FROM my_host
```
You can also create nested variables. For example if you had another variable named `region`. Then you could have
the hosts variable only show hosts from the current selected region with a query like this (if `region` is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values):
```sql
SELECT hostname FROM my_host WHERE region IN($region)
```
### Using Variables in Queries
Template variables are quoted automatically so if it is a string value do not wrap them in quotes in where clauses. If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values.
There are two syntaxes:
`$<varname>` Example with a template variable named `hostname`:
```sql
SELECT
UNIX_TIMESTAMP(atimestamp) as time_sec,
aint as value,
avarchar as metric
FROM my_table
WHERE $__timeFilter(atimestamp) and hostname in($hostname)
ORDER BY atimestamp ASC
```
`[[varname]]` Example with a template variable named `hostname`:
```sql
SELECT
UNIX_TIMESTAMP(atimestamp) as time_sec,
aint as value,
avarchar as metric
FROM my_table
WHERE $__timeFilter(atimestamp) and hostname in([[hostname]])
ORDER BY atimestamp ASC
```
## Alerting

View File

@ -20,9 +20,9 @@ parent = "http_api"
GET /api/users HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
Authorization: Basic YWRtaW46YWRtaW4=
Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`.
Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. Requires basic authentication and that the authenticated user is a Grafana Admin.
**Example Response**:
@ -55,10 +55,12 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter
GET /api/users/search?perpage=10&page=1&query=mygraf HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
Authorization: Basic YWRtaW46YWRtaW4=
Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. The `totalCount` field in the response can be used for pagination of the user list E.g. if `totalCount` is equal to 100 users and the `perpage` parameter is set to 10 then there are 10 pages of users. The `query` parameter is optional and it will return results where the query value is contained in one of the `name`, `login` or `email` fields. Query values with spaces need to be url encoded e.g. `query=Jane%20Doe`.
Requires basic authentication and that the authenticated user is a Grafana Admin.
**Example Response**:
HTTP/1.1 200
@ -94,7 +96,9 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter
GET /api/users/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
Authorization: Basic YWRtaW46YWRtaW4=
Requires basic authentication and that the authenticated user is a Grafana Admin.
**Example Response**:
@ -126,7 +130,9 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter
GET /api/users/lookup?loginOrEmail=admin HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
Authorization: Basic YWRtaW46YWRtaW4=
Requires basic authentication and that the authenticated user is a Grafana Admin.
**Example Response**:
@ -152,7 +158,7 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter
PUT /api/users/2 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
Authorization: Basic YWRtaW46YWRtaW4=
{
"email":"user@mygraf.com",
@ -161,6 +167,8 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter
"theme":"light"
}
Requires basic authentication and that the authenticated user is a Grafana Admin.
**Example Response**:
HTTP/1.1 200
@ -178,7 +186,9 @@ Default value for the `perpage` parameter is `1000` and for the `page` parameter
GET /api/users/1/orgs HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
Authorization: Basic YWRtaW46YWRtaW4=
Requires basic authentication and that the authenticated user is a Grafana Admin.
**Example Response**:
@ -246,11 +256,29 @@ Changes the password for the user
{"message":"User password changed"}
## Switch user context
## Switch user context for a specified user
`POST /api/user/using/:organisationId`
`POST /api/users/:userId/using/:organizationId`
Switch user context to the given organisation.
Switch user context to the given organization. Requires basic authentication and that the authenticated user is a Grafana Admin.
**Example Request**:
POST /api/users/7/using/2 HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{"message":"Active organization changed"}
## Switch user context for signed in user
`POST /api/user/using/:organizationId`
Switch user context to the given organization.
**Example Request**:

View File

@ -66,6 +66,7 @@
"eventemitter3": "^2.0.2",
"gaze": "^1.1.2",
"gridstack": "https://github.com/grafana/gridstack.js#grafana",
"gemini-scrollbar": "https://github.com/grafana/gemini-scrollbar#grafana",
"grunt-jscs": "3.0.1",
"grunt-sass-lint": "^0.2.2",
"grunt-sync": "^0.6.2",

View File

@ -41,6 +41,7 @@ func (hs *HttpServer) registerRoutes() {
r.Get("/org/users/", reqSignedIn, Index)
r.Get("/org/apikeys/", reqSignedIn, Index)
r.Get("/dashboard/import/", reqSignedIn, Index)
r.Get("/configuration", reqGrafanaAdmin, Index)
r.Get("/admin", reqGrafanaAdmin, Index)
r.Get("/admin/settings", reqGrafanaAdmin, Index)
r.Get("/admin/users", reqGrafanaAdmin, Index)

View File

@ -65,7 +65,7 @@ func New(hash string) *Avatar {
return &Avatar{
hash: hash,
reqParams: url.Values{
"d": {"404"},
"d": {"retro"},
"size": {"200"},
"r": {"pg"}}.Encode(),
}
@ -146,7 +146,7 @@ func CacheServer() http.Handler {
}
func newNotFound() *Avatar {
avatar := &Avatar{}
avatar := &Avatar{notFound: true}
// load transparent png into buffer
path := filepath.Join(setting.StaticRootPath, "img", "transparent.png")

View File

@ -7,7 +7,7 @@ type IndexViewData struct {
AppSubUrl string
GoogleAnalyticsId string
GoogleTagManagerId string
MainNavLinks []*NavLink
NavTree []*NavLink
BuildVersion string
BuildCommit string
NewGrafanaVersionExists bool
@ -20,10 +20,14 @@ type PluginCss struct {
}
type NavLink struct {
Text string `json:"text,omitempty"`
Icon string `json:"icon,omitempty"`
Img string `json:"img,omitempty"`
Url string `json:"url,omitempty"`
Divider bool `json:"divider,omitempty"`
Children []*NavLink `json:"children,omitempty"`
Id string `json:"id,omitempty"`
Text string `json:"text,omitempty"`
Description string `json:"description,omitempty"`
Icon string `json:"icon,omitempty"`
Img string `json:"img,omitempty"`
Url string `json:"url,omitempty"`
Target string `json:"target,omitempty"`
Divider bool `json:"divider,omitempty"`
HideFromMenu bool `json:"hideFromMenu,omitempty"`
Children []*NavLink `json:"children,omitempty"`
}

View File

@ -27,6 +27,7 @@ type CurrentUser struct {
Email string `json:"email"`
Name string `json:"name"`
LightTheme bool `json:"lightTheme"`
OrgCount int `json:"orgCount"`
OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"`
OrgRole m.RoleType `json:"orgRole"`

View File

@ -141,7 +141,6 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
"alertingEnabled": setting.AlertingEnabled,
"googleAnalyticsId": setting.GoogleAnalyticsId,
"disableLoginForm": setting.DisableLoginForm,
"disableSignoutMenu": setting.DisableSignoutMenu,
"externalUserMngInfo": setting.ExternalUserMngInfo,
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
"externalUserMngLinkName": setting.ExternalUserMngLinkName,

View File

@ -50,6 +50,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Login: c.Login,
Email: c.Email,
Name: c.Name,
OrgCount: c.OrgCount,
OrgId: c.OrgId,
OrgName: c.OrgName,
OrgRole: c.OrgRole,
@ -85,10 +86,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
}
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "New",
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Create",
Icon: "fa fa-fw fa-plus",
Url: "",
Url: "#",
Children: []*dtos.NavLink{
{Text: "Dashboard", Icon: "fa fa-fw fa-plus", Url: setting.AppSubUrl + "/dashboard/new"},
{Text: "Folder", Icon: "fa fa-fw fa-plus", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"},
@ -99,53 +100,56 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
dashboardChildNavs := []*dtos.NavLink{
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home"},
{Text: "Playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"},
{Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-snapshot"},
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"},
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-snapshot"},
}
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Dashboards",
Id: "dashboards",
Icon: "icon-gf icon-gf-dashboard",
Url: setting.AppSubUrl + "/",
Children: dashboardChildNavs,
})
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
alertChildNavs := []*dtos.NavLink{
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list", Icon: "fa fa-fw fa-list-ul"},
{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "fa fa-fw fa-bell-o"},
if c.IsSignedIn {
profileNode := &dtos.NavLink{
Text: c.SignedInUser.Login,
Id: "profile",
Img: data.User.GravatarUrl,
Url: setting.AppSubUrl + "/profile",
HideFromMenu: true,
Children: []*dtos.NavLink{
{Text: "Your profile", Url: setting.AppSubUrl + "/profile", Icon: "fa fa-fw fa-sliders"},
{Text: "Change Password", Id: "change-password", Url: setting.AppSubUrl + "/profile/password", Icon: "fa fa-fw fa-lock", HideFromMenu: true},
},
}
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
if !setting.DisableSignoutMenu {
// add sign out first
profileNode.Children = append([]*dtos.NavLink{
{Text: "Sign out", Url: setting.AppSubUrl + "/logout", Icon: "fa fa-fw fa-sign-out", Target: "_self"},
}, profileNode.Children...)
}
data.NavTree = append(data.NavTree, profileNode)
}
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
alertChildNavs := []*dtos.NavLink{
{Text: "Alert List", Id: "alert-list", Url: setting.AppSubUrl + "/alerting/list", Icon: "fa fa-fw fa-list-ul"},
{Text: "Notification channels", Id: "channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "fa fa-fw fa-bell-o"},
}
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Alerting",
Id: "alerting",
Icon: "icon-gf icon-gf-alert",
Url: setting.AppSubUrl + "/alerting/list",
Children: alertChildNavs,
})
}
if c.OrgRole == m.ROLE_ADMIN {
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Data Sources",
Icon: "icon-gf icon-gf-datasources",
Url: setting.AppSubUrl + "/datasources",
Children: []*dtos.NavLink{
{Text: "List", Url: setting.AppSubUrl + "/datasources", Icon: "icon-gf icon-gf-datasources"},
{Text: "New", Url: setting.AppSubUrl + "/datasources", Icon: "fa fa-fw fa-plus"},
},
})
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Plugins",
Icon: "icon-gf icon-gf-apps",
Url: setting.AppSubUrl + "/plugins",
Children: []*dtos.NavLink{
{Text: "Panels", Url: setting.AppSubUrl + "/plugins?type=panel", Icon: "fa fa-fw fa-stop"},
{Text: "Data sources", Url: setting.AppSubUrl + "/plugins?type=datasource", Icon: "icon-gf icon-gf-datasources"},
{Text: "Apps", Url: setting.AppSubUrl + "/plugins?type=app", Icon: "icon-gf icon-gf-apps"},
},
})
}
enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId)
if err != nil {
return nil, err
@ -155,6 +159,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
if plugin.Pinned {
appLink := &dtos.NavLink{
Text: plugin.Name,
Id: "plugin-page-" + plugin.Id,
Url: plugin.DefaultNavUrl,
Img: plugin.Info.Logos.Small,
}
@ -187,25 +192,97 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
}
if len(appLink.Children) > 0 {
data.MainNavLinks = append(data.MainNavLinks, appLink)
data.NavTree = append(data.NavTree, appLink)
}
}
}
if c.IsGrafanaAdmin {
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Admin",
if c.OrgRole == m.ROLE_ADMIN {
cfgNode := &dtos.NavLink{
Id: "cfg",
Text: "Configuration",
Icon: "fa fa-fw fa-cogs",
Url: setting.AppSubUrl + "/admin",
Url: setting.AppSubUrl + "/configuration",
Children: []*dtos.NavLink{
{Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"},
{Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"},
{Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"},
{Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"},
{
Text: "Data Sources",
Icon: "icon-gf icon-gf-datasources",
Description: "Add and configure data sources",
Id: "datasources",
Url: setting.AppSubUrl + "/datasources",
Children: []*dtos.NavLink{
{Text: "List", Url: setting.AppSubUrl + "/datasources", Icon: "icon-gf icon-gf-datasources"},
{Text: "New", Url: setting.AppSubUrl + "/datasources", Icon: "fa fa-fw fa-plus"},
},
},
{
Text: "Preferences",
Id: "org",
Description: "Organization preferences",
Icon: "fa fa-fw fa-sliders",
Url: setting.AppSubUrl + "/org",
},
{
Text: "Plugins",
Id: "plugins",
Description: "View and configure plugins",
Icon: "icon-gf icon-gf-apps",
Url: setting.AppSubUrl + "/plugins",
Children: []*dtos.NavLink{
{Text: "Panels", Url: setting.AppSubUrl + "/plugins?type=panel", Icon: "fa fa-fw fa-stop"},
{Text: "Data sources", Url: setting.AppSubUrl + "/plugins?type=datasource", Icon: "icon-gf icon-gf-datasources"},
{Text: "Apps", Url: setting.AppSubUrl + "/plugins?type=app", Icon: "icon-gf icon-gf-apps"},
},
},
{
Text: "Users",
Id: "users",
Description: "Manage users & user groups",
Icon: "fa fa-fw fa-users",
Url: setting.AppSubUrl + "/org/users",
},
{
Text: "API Keys",
Id: "apikeys",
Description: "Create & manage API keys",
Icon: "fa fa-fw fa-key",
Url: setting.AppSubUrl + "/org/apikeys",
},
},
})
}
if c.IsGrafanaAdmin {
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
Text: "Server Admin",
Id: "admin",
Icon: "fa fa-fw fa-shield",
Url: setting.AppSubUrl + "/admin",
Children: []*dtos.NavLink{
{Text: "Global Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users"},
{Text: "Global Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs"},
{Text: "Server Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings"},
{Text: "Server Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats"},
{Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/admin/styleguide"},
},
})
}
data.NavTree = append(data.NavTree, cfgNode)
}
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Help",
Id: "help",
Url: "#",
Icon: "fa fa-fw fa-question",
HideFromMenu: true,
Children: []*dtos.NavLink{
{Text: "Keyboard shortcuts", Url: "/shortcuts", Icon: "fa fa-fw fa-keyboard-o", Target: "_self"},
{Text: "Community site", Url: "http://community.grafana.com", Icon: "fa fa-fw fa-comment", Target: "_blank"},
{Text: "Documentation", Url: "http://docs.grafana.org", Icon: "fa fa-fw fa-file", Target: "_blank"},
},
})
return &data, nil
}

View File

@ -160,6 +160,7 @@ type SignedInUser struct {
Name string
Email string
ApiKeyId int64
OrgCount int
IsGrafanaAdmin bool
HelpFlags1 HelpFlags1
LastSeenAt time.Time

View File

@ -350,6 +350,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
u.name as name,
u.help_flags1 as help_flags1,
u.last_seen_at as last_seen_at,
(SELECT COUNT(*) FROM org_user where org_user.user_id = u.id) as org_count,
org.name as org_name,
org_user.role as org_role,
org.id as org_id

View File

@ -183,7 +183,7 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows)
values := make([]interface{}, len(types))
for i, stype := range types {
e.log.Info("type", "type", stype)
e.log.Debug("type", "type", stype)
switch stype.DatabaseTypeName() {
case mysql.FieldTypeNameTiny:
values[i] = new(int8)

View File

@ -0,0 +1,42 @@
///<reference path="../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
const template = `
<div class="scroll-canvas">
<navbar model="model"></navbar>
<div class="page-container">
<div class="page-header">
<h1>
<i class="{{::model.node.icon}}" ng-if="::model.node.icon"></i>
<img ng-src="{{::model.node.img}}" ng-if="::model.node.img"></i>
{{::model.node.text}}
</h1>
<div class="page-header__actions" ng-transclude="header"></div>
</div>
<div class="page-body" ng-transclude="body">
</div>
</div>
</div>
`;
export function gfPageDirective() {
return {
restrict: 'E',
template: template,
scope: {
"model": "=",
},
transclude: {
'header': '?gfPageHeader',
'body': 'gfPageBody',
},
link: function(scope, elem, attrs) {
console.log(scope);
}
};
}
coreModule.directive('gfPage', gfPageDirective);

View File

@ -65,36 +65,24 @@ export class GrafanaCtrl {
}
/** @ngInject */
export function grafanaAppDirective(playlistSrv, contextSrv) {
export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope) {
return {
restrict: 'E',
controller: GrafanaCtrl,
link: (scope, elem) => {
var ignoreSideMenuHide;
var sidemenuOpen;
var body = $('body');
// see https://github.com/zenorocha/clipboard.js/issues/155
$.fn.modal.Constructor.prototype.enforceFocus = function() {};
// handle sidemenu open state
scope.$watch('contextSrv.sidemenu', newVal => {
if (newVal !== undefined) {
body.toggleClass('sidemenu-open', scope.contextSrv.sidemenu);
if (!newVal) {
contextSrv.setPinnedState(false);
}
}
if (contextSrv.sidemenu) {
ignoreSideMenuHide = true;
setTimeout(() => {
ignoreSideMenuHide = false;
}, 300);
}
});
sidemenuOpen = scope.contextSrv.sidemenu;
body.toggleClass('sidemenu-open', sidemenuOpen);
scope.$watch('contextSrv.pinned', newVal => {
if (newVal !== undefined) {
body.toggleClass('sidemenu-pinned', newVal);
scope.$watch('contextSrv.sidemenu', newVal => {
if (sidemenuOpen !== scope.contextSrv.sidemenu) {
sidemenuOpen = scope.contextSrv.sidemenu;
body.toggleClass('sidemenu-open', scope.contextSrv.sidemenu);
}
});
@ -130,6 +118,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
var lastActivity = new Date().getTime();
var activeUser = true;
var inActiveTimeLimit = 60 * 1000;
var sidemenuHidden = false;
function checkForInActiveUser() {
if (!activeUser) {
@ -143,6 +132,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
if ((new Date().getTime() - lastActivity) > inActiveTimeLimit) {
activeUser = false;
body.addClass('user-activity-low');
// hide sidemenu
if (sidemenuOpen) {
sidemenuHidden = true;
body.removeClass('sidemenu-open');
$timeout(function() {
$rootScope.$broadcast("render");
}, 100);
}
}
}
@ -151,6 +148,15 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
if (!activeUser) {
activeUser = true;
body.removeClass('user-activity-low');
// restore sidemenu
if (sidemenuHidden) {
sidemenuHidden = false;
body.addClass('sidemenu-open');
$timeout(function() {
$rootScope.$broadcast("render");
}, 100);
}
}
}
@ -199,15 +205,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
}
}
// hide sidemenu
if (!ignoreSideMenuHide && !contextSrv.pinned && body.find('.sidemenu').length > 0) {
if (target.parents('.sidemenu').length === 0) {
scope.$apply(function() {
scope.contextSrv.toggleSideMenu();
});
}
}
// hide popovers
var popover = elem.find('.popover');
if (popover.length > 0 && target.parents('.graph-legend').length === 0) {

View File

@ -4,15 +4,6 @@
<i class="fa fa-keyboard-o"></i>
<span class="p-l-1">Shortcuts</span>
</h2>
<!-- <ul class="gf&#45;tabs"> -->
<!-- <li class="gf&#45;tabs&#45;item" ng&#45;repeat="tab in ['Shortcuts']"> -->
<!-- <a class="gf&#45;tabs&#45;link" ng&#45;click="ctrl.tabindex = $index" ng&#45;class="{active: ctrl.tabIndex === $index}"> -->
<!-- {{::tab}} -->
<!-- </a> -->
<!-- </li> -->
<!-- </ul> -->
<a class="modal-header-close" ng-click="ctrl.dismiss();">
<i class="fa fa-remove"></i>
</a>

View File

@ -1,36 +1,26 @@
<div class="navbar-inner">
<a class="navbar-brand-btn pointer" ng-click="ctrl.contextSrv.toggleSideMenu()">
<span class="navbar-brand-btn-background">
<img src="public/img/grafana_icon.svg"></img>
</span>
</a>
<div class="page-nav">
<div class="container">
<div class="page-breadcrumb">
<div class="page-breadcrumb__item dropdown" ng-repeat="item in ctrl.model.breadcrumbs">
<!-- <a class="pointer" ng&#45;href="{{::item.url}}" data&#45;toggle="dropdown" ng&#45;if="::item.children"> -->
<!-- {{::item.text}} -->
<!-- <i class="page&#45;breadcrumb__caret fa fa&#45;caret&#45;down"></i> -->
<!-- </a> -->
<div ng-if="::!ctrl.hasMenu">
<a href="{{::ctrl.section.url}}" class="navbar-page-btn">
<i class="{{::ctrl.section.icon}}" ng-show="::ctrl.section.icon"></i>
<img ng-src="{{::ctrl.section.iconUrl}}" ng-show="::ctrl.section.iconUrl"></i>
{{::ctrl.section.title}}
</a>
</div>
<div class="dropdown navbar-page-btn-wrapper" ng-if="::ctrl.hasMenu">
<a href="{{::ctrl.section.url}}" class="navbar-page-btn" data-toggle="dropdown">
<i class="{{::ctrl.section.icon}}" ng-show="::ctrl.section.icon"></i>
<img ng-src="{{::ctrl.section.iconUrl}}" ng-show="::ctrl.section.iconUrl"></i>
{{::ctrl.section.title}}
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-menu--navbar">
<li ng-repeat="navItem in ::ctrl.model.menu">
<a class="pointer" ng-href="{{::navItem.url}}" ng-click="ctrl.navItemClicked(navItem, $event)">
<i class="{{::navItem.icon}}" ng-show="::navItem.icon"></i>
{{::navItem.title}}
<a class="pointer" ng-href="{{::item.url}}">
{{::item.text}}
</a>
</li>
</ul>
</div>
<div ng-transclude></div>
<ul class="dropdown-menu dropdown-menu--navbar">
<li ng-repeat="subItem in ::item.children">
<a class="pointer" ng-href="{{::subItem.url}}" ng-click="ctrl.navItemClicked(subItem, $event)">
{{::subItem.text}}
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<dashboard-search></dashboard-search>

View File

@ -8,13 +8,9 @@ import {NavModel, NavModelItem} from '../../nav_model_srv';
export class NavbarCtrl {
model: NavModel;
section: NavModelItem;
hasMenu: boolean;
/** @ngInject */
constructor(private $scope, private $rootScope, private contextSrv) {
this.section = this.model.section;
this.hasMenu = this.model.menu.length > 0;
}
showSearch() {
@ -35,15 +31,31 @@ export function navbarDirective() {
templateUrl: 'public/app/core/components/navbar/navbar.html',
controller: NavbarCtrl,
bindToController: true,
transclude: true,
controllerAs: 'ctrl',
scope: {
model: "=",
},
link: function(scope, elem) {
elem.addClass('navbar');
}
};
}
export function pageH1() {
return {
restrict: 'E',
template: `
<h1>
<i class="{{::model.node.icon}}" ng-if="::model.node.icon"></i>
<img ng-src="{{::model.node.img}}" ng-if="::model.node.img"></i>
{{::model.node.text}}
</h1>
`,
scope: {
model: "=",
}
};
}
coreModule.directive('pageH1', pageH1);
coreModule.directive('navbar', navbarDirective);

View File

@ -0,0 +1,82 @@
///<reference path="../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import config from 'app/core/config';
import {contextSrv} from 'app/core/services/context_srv';
const template = `
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<i class="fa fa-random"></i>
<span class="p-l-1">Switch Organization</span>
</h2>
<a class="modal-header-close" ng-click="ctrl.dismiss();">
<i class="fa fa-remove"></i>
</a>
</div>
<div class="modal-content">
<div class="gf-form-group">
<table class="filter-table form-inline">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="org in ctrl.orgs">
<td>{{org.name}}</td>
<td>{{org.role}}</td>
<td class="text-right">
<span class="btn btn-primary btn-mini" ng-show="org.orgId === ctrl.currentOrgId">
Current
</span>
<a ng-click="ctrl.setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== ctrl.currentOrgId">
Switch to
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>`;
export class OrgSwitchCtrl {
orgs: any[];
currentOrgId: any;
/** @ngInject */
constructor(private backendSrv) {
this.currentOrgId = contextSrv.user.orgId;
this.getUserOrgs();
}
getUserOrgs() {
this.backendSrv.get('/api/user/orgs').then(orgs => {
this.orgs = orgs;
});
}
setUsingOrg(org) {
this.backendSrv.post('/api/user/using/' + org.orgId).then(() => {
window.location.href = window.location.href;
});
}
}
export function orgSwitcher() {
return {
restrict: 'E',
template: template,
controller: OrgSwitchCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {dismiss: "&"},
};
}
coreModule.directive('orgSwitcher', orgSwitcher);

View File

@ -1,211 +1,23 @@
// ///<reference path="../../headers/common.d.ts" />
//
// import _ from 'lodash';
//
// var objectAssign = require('object-assign');
// var Emitter = require('tiny-emitter');
// var Lethargy = require('lethargy').Lethargy;
// var support = require('./support');
// var clone = require('./clone');
// var bindAll = require('bindall-standalone');
// var EVT_ID = 'virtualscroll';
//
// var keyCodes = {
// LEFT: 37,
// UP: 38,
// RIGHT: 39,
// DOWN: 40
// };
//
// function VirtualScroll(this: any, options) {
// _.bindAll(this, '_onWheel', '_onMouseWheel', '_onTouchStart', '_onTouchMove', '_onKeyDown');
//
// this.el = window;
// if (options && options.el) {
// this.el = options.el;
// delete options.el;
// }
//
// this.options = _.assign({
// mouseMultiplier: 1,
// touchMultiplier: 2,
// firefoxMultiplier: 15,
// keyStep: 120,
// preventTouch: false,
// unpreventTouchClass: 'vs-touchmove-allowed',
// limitInertia: false
// }, options);
//
// if (this.options.limitInertia) this._lethargy = new Lethargy();
//
// this._emitter = new Emitter();
// this._event = {
// y: 0,
// x: 0,
// deltaX: 0,
// deltaY: 0
// };
//
// this.touchStartX = null;
// this.touchStartY = null;
// this.bodyTouchAction = null;
// }
//
// VirtualScroll.prototype._notify = function(e) {
// var evt = this._event;
// evt.x += evt.deltaX;
// evt.y += evt.deltaY;
//
// this._emitter.emit(EVT_ID, {
// x: evt.x,
// y: evt.y,
// deltaX: evt.deltaX,
// deltaY: evt.deltaY,
// originalEvent: e
// });
// };
//
// VirtualScroll.prototype._onWheel = function(e) {
// var options = this.options;
// if (this._lethargy && this._lethargy.check(e) === false) return;
//
// var evt = this._event;
//
// // In Chrome and in Firefox (at least the new one)
// evt.deltaX = e.wheelDeltaX || e.deltaX * -1;
// evt.deltaY = e.wheelDeltaY || e.deltaY * -1;
//
// // for our purpose deltamode = 1 means user is on a wheel mouse, not touch pad
// // real meaning: https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent#Delta_modes
// if(support.isFirefox && e.deltaMode == 1) {
// evt.deltaX *= options.firefoxMultiplier;
// evt.deltaY *= options.firefoxMultiplier;
// }
//
// evt.deltaX *= options.mouseMultiplier;
// evt.deltaY *= options.mouseMultiplier;
//
// this._notify(e);
// };
//
// VirtualScroll.prototype._onMouseWheel = function(e) {
// if (this.options.limitInertia && this._lethargy.check(e) === false) return;
//
// var evt = this._event;
//
// // In Safari, IE and in Chrome if 'wheel' isn't defined
// evt.deltaX = (e.wheelDeltaX) ? e.wheelDeltaX : 0;
// evt.deltaY = (e.wheelDeltaY) ? e.wheelDeltaY : e.wheelDelta;
//
// this._notify(e);
// };
//
// VirtualScroll.prototype._onTouchStart = function(e) {
// var t = (e.targetTouches) ? e.targetTouches[0] : e;
// this.touchStartX = t.pageX;
// this.touchStartY = t.pageY;
// };
//
// VirtualScroll.prototype._onTouchMove = function(e) {
// var options = this.options;
// if(options.preventTouch
// && !e.target.classList.contains(options.unpreventTouchClass)) {
// e.preventDefault();
// }
//
// var evt = this._event;
//
// var t = (e.targetTouches) ? e.targetTouches[0] : e;
//
// evt.deltaX = (t.pageX - this.touchStartX) * options.touchMultiplier;
// evt.deltaY = (t.pageY - this.touchStartY) * options.touchMultiplier;
//
// this.touchStartX = t.pageX;
// this.touchStartY = t.pageY;
//
// this._notify(e);
// };
//
// VirtualScroll.prototype._onKeyDown = function(e) {
// var evt = this._event;
// evt.deltaX = evt.deltaY = 0;
//
// switch(e.keyCode) {
// case keyCodes.LEFT:
// case keyCodes.UP:
// evt.deltaY = this.options.keyStep;
// break;
//
// case keyCodes.RIGHT:
// case keyCodes.DOWN:
// evt.deltaY = - this.options.keyStep;
// break;
//
// default:
// return;
// }
//
// this._notify(e);
// };
//
// VirtualScroll.prototype._bind = function() {
// if(support.hasWheelEvent) this.el.addEventListener('wheel', this._onWheel);
// if(support.hasMouseWheelEvent) this.el.addEventListener('mousewheel', this._onMouseWheel);
//
// if(support.hasTouch) {
// this.el.addEventListener('touchstart', this._onTouchStart);
// this.el.addEventListener('touchmove', this._onTouchMove);
// }
//
// if(support.hasPointer && support.hasTouchWin) {
// this.bodyTouchAction = document.body.style.msTouchAction;
// document.body.style.msTouchAction = 'none';
// this.el.addEventListener('MSPointerDown', this._onTouchStart, true);
// this.el.addEventListener('MSPointerMove', this._onTouchMove, true);
// }
//
// if(support.hasKeyDown) document.addEventListener('keydown', this._onKeyDown);
// };
//
// VirtualScroll.prototype._unbind = function() {
// if(support.hasWheelEvent) this.el.removeEventListener('wheel', this._onWheel);
// if(support.hasMouseWheelEvent) this.el.removeEventListener('mousewheel', this._onMouseWheel);
//
// if(support.hasTouch) {
// this.el.removeEventListener('touchstart', this._onTouchStart);
// this.el.removeEventListener('touchmove', this._onTouchMove);
// }
//
// if(support.hasPointer && support.hasTouchWin) {
// document.body.style.msTouchAction = this.bodyTouchAction;
// this.el.removeEventListener('MSPointerDown', this._onTouchStart, true);
// this.el.removeEventListener('MSPointerMove', this._onTouchMove, true);
// }
//
// if(support.hasKeyDown) document.removeEventListener('keydown', this._onKeyDown);
// };
//
// VirtualScroll.prototype.on = function(cb, ctx) {
// this._emitter.on(EVT_ID, cb, ctx);
//
// var events = this._emitter.e;
// if (events && events[EVT_ID] && events[EVT_ID].length === 1) this._bind();
// };
//
// VirtualScroll.prototype.off = function(cb, ctx) {
// this._emitter.off(EVT_ID, cb, ctx);
//
// var events = this._emitter.e;
// if (!events[EVT_ID] || events[EVT_ID].length <= 0) this._unbind();
// };
//
// VirtualScroll.prototype.reset = function() {
// var evt = this._event;
// evt.x = 0;
// evt.y = 0;
// };
//
// VirtualScroll.prototype.destroy = function() {
// this._emitter.off();
// this._unbind();
// };
///<reference path="../../../headers/common.d.ts" />
import GeminiScrollbar from 'gemini-scrollbar';
import coreModule from 'app/core/core_module';
import _ from 'lodash';
export function geminiScrollbar() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
var myScrollbar = new GeminiScrollbar({
autoshow: false,
element: elem[0]
}).create();
scope.$on('$destroy', () => {
myScrollbar.destroy();
});
}
};
}
coreModule.directive('geminiScrollbar', geminiScrollbar);

View File

@ -4,6 +4,7 @@
<div class="search-container" ng-if="ctrl.isOpen">
<div class="search-field-wrapper">
<div class="search-field-icon pointer" ng-click="ctrl.closeSearch()"><i class="fa fa-search"></i></div>
<input type="text" placeholder="Find dashboards by name" give-focus="ctrl.giveSearchFocus" tabindex="1"
ng-keydown="ctrl.keyDown($event)"
@ -39,35 +40,38 @@
</div>
<div class="search-dropdown" ng-class="{'search-dropdown--fade-in': ctrl.openCompleted}">
<div class="search-results-container" ng-if="ctrl.tagsMode">
<div ng-repeat="tag in ctrl.results" class="pointer" style="width: 180px; float: left;"
ng-class="{'selected': $index === ctrl.selectedIndex }"
ng-click="ctrl.filterByTag(tag.term, $event)">
<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
<i class="fa fa-tag"></i>
<span>{{tag.term}} &nbsp;({{tag.count}})</span>
</a>
<div gemini-scrollbar>
<div class="search-results-container" ng-if="ctrl.tagsMode">
<div ng-repeat="tag in ctrl.results" class="pointer" style="width: 180px; float: left;"
ng-class="{'selected': $index === ctrl.selectedIndex }"
ng-click="ctrl.filterByTag(tag.term, $event)">
<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
<i class="fa fa-tag"></i>
<span>{{tag.term}} &nbsp;({{tag.count}})</span>
</a>
</div>
</div>
<div class="search-results-container" ng-if="!ctrl.tagsMode">
<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
<div ng-repeat="row in ctrl.results">
<a class="search-item search-item--{{::row.type}}" ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
<span class="search-result-tags">
<span ng-click="ctrl.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>
</span>
<span class="search-result-link">
<i class="fa search-result-icon"></i>
{{::row.title}}
</span>
</a>
</div>
</div>
</div>
<div class="search-results-container" ng-if="!ctrl.tagsMode">
<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
<div ng-repeat="row in ctrl.results">
<a class="search-item search-item--{{::row.type}}" ng-class="{'selected': $index == ctrl.selectedIndex}" ng-href="{{row.url}}">
<span class="search-result-tags">
<span ng-click="ctrl.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>
</span>
<span class="search-result-link">
<i class="fa search-result-icon"></i>
{{::row.title}}
</span>
</a>
</div>
</div>
</div>

View File

@ -1,13 +1,12 @@
<ul class="sidemenu">
<a class="sidemenu__logo" ng-click="ctrl.toggleSideMenu()">
<span class="navbar-brand-btn-background">
<img src="public/img/grafana_icon.svg"></img>
</span>
</a>
<li>
<a class="sidemenu-item" ng-click="ctrl.search()">
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-search"></i></span>
</a>
</li>
<li ng-repeat="item in ::ctrl.mainLinks" class="dropdown">
<a href="{{::item.url}}" class="sidemenu-item" target="{{::item.target}}">
<div class="sidemenu__top">
<div ng-repeat="item in ::ctrl.mainLinks" class="sidemenu-item dropdown">
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
<span class="icon-circle sidemenu-icon">
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
<img ng-src="{{::item.img}}" ng-show="::item.img">
@ -24,48 +23,48 @@
</a>
</li>
</ul>
</li>
</div>
</div>
<li ng-show="::!ctrl.isSignedIn">
<a href="{{ctrl.loginUrl}}" class="sidemenu-item" target="_self">
<div class="sidemenu__bottom">
<div ng-show="::!ctrl.isSignedIn" class="sidemenu-item">
<a href="{{ctrl.loginUrl}}" class="sidemenu-link" target="_self">
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-sign-in"></i></span>
<span class="sidemenu-item-text">Sign in</span>
</a>
</li>
<li class="sidemenu-org-section" ng-if="::ctrl.isSignedIn" class="dropdown">
<a class="sidemenu-item" href="profile">
<span class="icon-circle sidemenu-icon sidemenu-org-avatar">
<img ng-src="{{::ctrl.user.gravatarUrl}}">
<span class="sidemenu-org-avatar--missing">
<i class="fa fa-fw fa-user"></i>
</span>
</div>
</a>
<ul class="dropdown-menu dropdown-menu--sidemenu dropup" role="menu">
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
<li class="side-menu-header">
<span class="sidemenu-org-user sidemenu-item-text">{{::ctrl.user.name}}</span>
<span class="sidemenu-org-name sidemenu-item-text">{{::ctrl.user.orgName}}</span>
</li>
<li ng-repeat="menuItem in ctrl.orgMenu" ng-class="::menuItem.cssClass">
<span ng-show="::menuItem.section">{{::menuItem.section}}</span>
<a href="{{::menuItem.url}}" ng-show="::menuItem.url" target="{{::menuItem.target}}">
<i class="{{::menuItem.icon}}" ng-show="::menuItem.icon"></i>
{{::menuItem.text}}
</a>
</li>
<li ng-show="ctrl.orgs.length > ctrl.maxShownOrgs" style="margin-left: 10px;width: 90%">
<span class="sidemenu-item-text">Max shown : {{::ctrl.maxShownOrgs}}</span>
<input ng-model="::ctrl.orgFilter" style="padding-left: 5px" type="text" ng-change="::ctrl.loadOrgsItems();" class="gf-input-small width-12" placeholder="Filter">
</li>
<li ng-repeat="orgItem in ctrl.orgItems" ng-class="::orgItem.cssClass">
<a href="{{::orgItem.url}}" ng-show="::orgItem.url" target="{{::orgItem.target}}">
<i class="{{::orgItem.icon}}" ng-show="::orgItem.icon"></i>
{{::orgItem.text}}
</a>
<span class="sidemenu-item-text">Sign In</span>
</li>
</ul>
</li>
</div>
</ul>
<div ng-repeat="item in ::ctrl.bottomNav" class="sidemenu-item dropdown dropup">
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
<span class="icon-circle sidemenu-icon">
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
<img ng-src="{{::item.img}}" ng-show="::item.img">
</span>
</a>
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
<li ng-if="item.showOrgSwitcher" class="sidemenu-org-switcher">
<a ng-click="ctrl.switchOrg()">
<div>
<div class="sidemenu-org-switcher__org-name">{{ctrl.contextSrv.user.orgName}}</div>
<div class="sidemenu-org-switcher__org-current">Current Org:</div>
</div>
<div class="sidemenu-org-switcher__switch"><i class="fa fa-fw fa-random"></i>Switch</div>
</a>
</li>
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}" ng-hide="::child.hideFromMenu">
<a href="{{::child.url}}" target="{{::child.target}}">
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
{{::child.text}}
</a>
</li>
<li class="side-menu-header">
<span class="sidemenu-item-text">{{::item.text}}</span>
</li>
</ul>
</div>
</div>

View File

@ -6,109 +6,44 @@ import $ from 'jquery';
import coreModule from '../../core_module';
export class SideMenuCtrl {
isSignedIn: boolean;
showSignout: boolean;
user: any;
mainLinks: any;
orgMenu: any;
appSubUrl: string;
bottomNav: any;
loginUrl: string;
orgFilter: string;
orgItems: any;
orgs: any;
maxShownOrgs: number;
isSignedIn: boolean;
/** @ngInject */
constructor(private $scope, private $rootScope, private $location, private contextSrv, private backendSrv, private $element) {
constructor(private $scope, private $rootScope, private $location, private contextSrv, private $timeout) {
this.isSignedIn = contextSrv.isSignedIn;
this.user = contextSrv.user;
this.appSubUrl = config.appSubUrl;
this.showSignout = this.contextSrv.isSignedIn && !config['disableSignoutMenu'];
this.maxShownOrgs = 10;
this.mainLinks = config.bootData.mainNavLinks;
this.openUserDropdown();
this.mainLinks = _.filter(config.bootData.navTree, item => !item.hideFromMenu);
this.bottomNav = _.filter(config.bootData.navTree, item => item.hideFromMenu);
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
this.$scope.$on('$routeChangeSuccess', () => {
if (!this.contextSrv.pinned) {
this.contextSrv.sidemenu = false;
if (contextSrv.user.orgCount > 1) {
let profileNode = _.find(this.bottomNav, {id: 'profile'});
if (profileNode) {
profileNode.showOrgSwitcher = true;
}
}
this.$scope.$on('$routeChangeSuccess', () => {
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
});
this.orgFilter = '';
}
getUrl(url) {
return config.appSubUrl + url;
}
toggleSideMenu() {
this.contextSrv.toggleSideMenu();
this.$timeout(() => {
this.$rootScope.$broadcast('render');
});
}
search() {
this.$rootScope.appEvent('show-dash-search');
}
openUserDropdown() {
this.orgMenu = [
{section: 'You', cssClass: 'dropdown-menu-title'},
{text: 'Profile', url: this.getUrl('/profile')},
];
if (this.showSignout) {
this.orgMenu.push({text: "Sign out", url: this.getUrl("/logout"), target: "_self"});
}
if (this.contextSrv.hasRole('Admin')) {
this.orgMenu.push({section: this.user.orgName, cssClass: 'dropdown-menu-title'});
this.orgMenu.push({
text: "Preferences",
url: this.getUrl("/org")
});
this.orgMenu.push({
text: "Users",
url: this.getUrl("/org/users")
});
this.orgMenu.push({
text: "User Groups",
url: this.getUrl("/org/user-groups")
});
this.orgMenu.push({
text: "API Keys",
url: this.getUrl("/org/apikeys")
});
}
this.orgMenu.push({cssClass: "divider"});
this.backendSrv.get('/api/user/orgs').then(orgs => {
this.orgs = orgs;
this.loadOrgsItems();
});
}
loadOrgsItems(){
this.orgItems = [];
this.orgs.forEach(org => {
if (org.orgId === this.contextSrv.user.orgId) {
return;
}
if (this.orgItems.length === this.maxShownOrgs) {
return;
}
if (this.orgFilter === '' || (org.name.toLowerCase().indexOf(this.orgFilter.toLowerCase()) !== -1)) {
this.orgItems.push({
text: "Switch to " + org.name,
icon: "fa fa-fw fa-random",
url: this.getUrl('/profile/switch-org/' + org.orgId),
target: '_self'
});
}
});
if (config.allowOrgCreate) {
this.orgItems.push({text: "New organization", icon: "fa fa-fw fa-plus", url: this.getUrl('/org/new')});
}
}
switchOrg() {
this.$rootScope.appEvent('show-modal', {
templateHtml: '<org-switcher dismiss="dismiss()"></org-switcher>',
});
}
}
export function sideMenuDirective() {

View File

@ -51,6 +51,9 @@ import {JsonExplorer} from './components/json_explorer/json_explorer';
import {NavModelSrv, NavModel} from './nav_model_srv';
import {userPicker} from './components/user_picker';
import {userGroupPicker} from './components/user_group_picker';
import {geminiScrollbar} from './components/scroll/scroll';
import {gfPageDirective} from './components/gf_page';
import {orgSwitcher} from './components/org_switcher';
export {
arrayJoin,
@ -81,4 +84,7 @@ export {
NavModel,
userPicker,
userGroupPicker,
geminiScrollbar,
gfPageDirective,
orgSwitcher,
};

View File

@ -122,7 +122,7 @@ function (angular, _, coreModule) {
vm.selectValue = function(option, event, commitChange, excludeOthers) {
if (!option) { return; }
option.selected = !option.selected;
option.selected = vm.variable.multi ? !option.selected: true;
commitChange = commitChange || false;
excludeOthers = excludeOthers || false;

View File

@ -1,6 +1,8 @@
///<reference path="../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import config from 'app/core/config';
import _ from 'lodash';
export interface NavModelItem {
title: string;
@ -15,117 +17,41 @@ export interface NavModel {
}
export class NavModelSrv {
navItems: any;
/** @ngInject */
constructor(private contextSrv) {
this.navItems = config.bootData.navTree;
}
getAlertingNav(subPage) {
return {
section: {
title: 'Alerting',
url: 'plugins',
icon: 'icon-gf icon-gf-alert'
},
menu: [
{title: 'Alert List', active: subPage === 0, url: 'alerting/list', icon: 'fa fa-list-ul'},
{title: 'Notification channels', active: subPage === 1, url: 'alerting/notifications', icon: 'fa fa-bell-o'},
]
};
getCfgNode() {
return _.find(this.navItems, {id: 'cfg'});
}
getDatasourceNav(subPage) {
return {
section: {
title: 'Data Sources',
url: 'datasources',
icon: 'icon-gf icon-gf-datasources'
},
menu: [
{title: 'List view', active: subPage === 0, url: 'datasources', icon: 'fa fa-list-ul'},
{title: 'Add data source', active: subPage === 1, url: 'datasources/new', icon: 'fa fa-plus'},
]
};
}
getNav(...args) {
var children = this.navItems;
var nav = {breadcrumbs: [], node: null};
getPlaylistsNav(subPage) {
return {
section: {
title: 'Playlists',
url: 'playlists',
icon: 'fa fa-fw fa-film'
},
menu: [
{title: 'List view', active: subPage === 0, url: 'playlists', icon: 'fa fa-list-ul'},
{title: 'Add Playlist', active: subPage === 1, url: 'playlists/create', icon: 'fa fa-plus'},
]
};
}
for (let id of args) {
let node = _.find(children, {id: id});
nav.breadcrumbs.push(node);
nav.node = node;
children = node.children;
}
getProfileNav() {
return {
section: {
title: 'User Profile',
url: 'profile',
icon: 'fa fa-fw fa-user'
},
menu: []
};
return nav;
}
getNotFoundNav() {
return {
section: {
title: 'Page',
url: '',
icon: 'fa fa-fw fa-warning'
},
menu: []
var node = {
text: "Page not found ",
icon: "fa fa-fw fa-warning",
};
}
getOrgNav(subPage) {
return {
section: {
title: 'Organization',
url: 'org',
icon: 'icon-gf icon-gf-users'
},
menu: [
{title: 'Preferences', active: subPage === 0, url: 'org', icon: 'fa fa-fw fa-cog'},
{title: 'Org Users', active: subPage === 1, url: 'org/users', icon: 'fa fa-fw fa-users'},
{title: 'API Keys', active: subPage === 2, url: 'org/apikeys', icon: 'fa fa-fw fa-key'},
{title: 'Org User Groups', active: subPage === 3, url: 'org/user-groups', icon: 'fa fa-fw fa-users'},
]
};
}
getAdminNav(subPage) {
return {
section: {
title: 'Admin',
url: 'admin',
icon: 'fa fa-fw fa-cogs'
},
menu: [
{title: 'Users', active: subPage === 0, url: 'admin/users', icon: 'fa fa-fw fa-user'},
{title: 'Orgs', active: subPage === 1, url: 'admin/orgs', icon: 'fa fa-fw fa-users'},
{title: 'Server Settings', active: subPage === 2, url: 'admin/settings', icon: 'fa fa-fw fa-cogs'},
{title: 'Server Stats', active: subPage === 2, url: 'admin/stats', icon: 'fa fa-fw fa-line-chart'},
{title: 'Style Guide', active: subPage === 2, url: 'styleguide', icon: 'fa fa-fw fa-key'},
]
};
}
getPluginsNav() {
return {
section: {
title: 'Plugins',
url: 'plugins',
icon: 'icon-gf icon-gf-apps'
},
menu: []
breadcrumbs: [node],
node: node
};
}
@ -199,12 +125,6 @@ export class NavModelSrv {
});
}
menu.push({
title: 'Shortcuts',
icon: 'fa fa-fw fa-keyboard-o',
clickHandler: () => dashNavCtrl.showHelpModal()
});
if (this.contextSrv.isEditor && !dashboard.meta.isFolder) {
menu.push({
title: 'Save As...',

View File

@ -44,6 +44,12 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
templateUrl: 'public/app/features/dashboard/partials/dash_list.html',
controller : 'DashListCtrl',
})
.when('/configuration', {
templateUrl: 'public/app/features/admin/partials/configuration_home.html',
controller : 'ConfigurationHomeCtrl',
controllerAs: 'ctrl',
resolve: loadAdminBundle,
})
.when('/datasources', {
templateUrl: 'public/app/features/plugins/partials/ds_list.html',
controller : 'DataSourcesCtrl',

View File

@ -9,6 +9,7 @@ export class User {
isGrafanaAdmin: any;
isSignedIn: any;
orgRole: any;
orgId: number;
timezone: string;
helpFlags1: number;
lightTheme: boolean;
@ -30,10 +31,7 @@ export class ContextSrv {
sidemenu: any;
constructor() {
this.pinned = store.getBool('grafana.sidemenu.pinned', false);
if (this.pinned) {
this.sidemenu = true;
}
this.sidemenu = store.getBool('grafana.sidemenu', false);
if (!config.buildInfo) {
config.buildInfo = {};
@ -53,18 +51,13 @@ export class ContextSrv {
return this.user.orgRole === role;
}
setPinnedState(val) {
this.pinned = val;
store.set('grafana.sidemenu.pinned', val);
}
isGrafanaVisible() {
return !!(document.visibilityState === undefined || document.visibilityState === 'visible');
}
toggleSideMenu() {
this.sidemenu = !this.sidemenu;
this.setPinnedState(this.sidemenu);
store.set('grafana.sidemenu',this.sidemenu);
}
}

View File

@ -10,7 +10,7 @@ class AdminSettingsCtrl {
/** @ngInject **/
constructor($scope, backendSrv, navModelSrv) {
this.navModel = navModelSrv.getAdminNav();
this.navModel = navModelSrv.getNav('cfg', 'admin', 'server-settings');
backendSrv.get('/api/admin/settings').then(function(settings) {
$scope.settings = settings;
@ -24,7 +24,7 @@ class AdminHomeCtrl {
/** @ngInject **/
constructor(navModelSrv) {
this.navModel = navModelSrv.getAdminNav();
this.navModel = navModelSrv.getNav('cfg', 'admin');
}
}
@ -34,7 +34,7 @@ export class AdminStatsCtrl {
/** @ngInject */
constructor(backendSrv: any, navModelSrv) {
this.navModel = navModelSrv.getAdminNav();
this.navModel = navModelSrv.getNav('cfg', 'admin', 'server-stats');
backendSrv.get('/api/admin/stats').then(stats => {
this.stats = stats;
@ -42,6 +42,15 @@ export class AdminStatsCtrl {
}
}
export class ConfigurationHomeCtrl { navModel: any;
/** @ngInject */
constructor(private $scope, private backendSrv, private navModelSrv) {
this.navModel = navModelSrv.getNav('cfg');
}
}
coreModule.controller('ConfigurationHomeCtrl', ConfigurationHomeCtrl);
coreModule.controller('AdminSettingsCtrl', AdminSettingsCtrl);
coreModule.controller('AdminHomeCtrl', AdminHomeCtrl);
coreModule.controller('AdminStatsCtrl', AdminStatsCtrl);

View File

@ -9,7 +9,7 @@ function (angular) {
module.controller('AdminEditOrgCtrl', function($scope, $routeParams, backendSrv, $location, navModelSrv) {
$scope.init = function() {
$scope.navModel = navModelSrv.getAdminNav();
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs');
if ($routeParams.id) {
$scope.getOrg($routeParams.id);

View File

@ -11,7 +11,7 @@ function (angular, _) {
$scope.user = {};
$scope.newOrg = { name: '', role: 'Editor' };
$scope.permissions = {};
$scope.navModel = navModelSrv.getAdminNav();
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-users');
$scope.init = function() {
if ($routeParams.id) {

View File

@ -9,7 +9,7 @@ function (angular) {
module.controller('AdminListOrgsCtrl', function($scope, backendSrv, navModelSrv) {
$scope.init = function() {
$scope.navModel = navModelSrv.getAdminNav();
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs');
$scope.getOrgs();
};

View File

@ -12,7 +12,7 @@ export default class AdminListUsersCtrl {
/** @ngInject */
constructor(private $scope, private backendSrv, private navModelSrv) {
this.navModel = navModelSrv.getAdminNav();
this.navModel = navModelSrv.getNav('cfg', 'admin', 'global-users');
this.query = '';
this.getUsers();
}

View File

@ -2,30 +2,34 @@
<div class="page-container">
<div class="page-header">
<h1>
Server Administration
</h1>
<page-h1 model="ctrl.navModel"></page-h1>
</div>
<a class="btn btn-inverse" href="admin/users">
Manage Users
</a>
<a class="btn btn-inverse" href="admin/orgs">
Manage Organizations
</a>
<a class="btn btn-inverse" href="admin/settings">
View Server Settings
</a>
<a class="btn btn-inverse" href="admin/stats">
View Server Stats
</a>
<a class="btn btn-inverse" href="styleguide">
Style guide
</a>
<section class="card-section card-list-layout-grid">
<ol class="card-list" >
<li class="card-item-wrapper" ng-repeat="navItem in ctrl.navModel.node.children">
<a class="card-item" ng-href="{{::navItem.url}}">
<div class="card-item-header">
<div class="card-item-type">
</div>
</div>
<div class="card-item-body">
<figure class="card-item-figure">
<i class="{{navItem.icon}}"></i>
</figure>
<div class="card-item-details">
<div class="card-item-name">
{{navItem.text}}
</div>
<div class="card-item-sub-name">
{{navItem.description}}
</div>
</div>
</div>
</a>
</li>
</ol>
</section>
</div>

View File

@ -0,0 +1,34 @@
<div class="scroll-canvas">
<navbar model="ctrl.navModel"></navbar>
<div class="page-container">
<div class="page-header">
<page-h1 model="ctrl.navModel"></page-h1>
</div>
<section class="card-section card-list-layout-grid">
<ol class="card-list" >
<li class="card-item-wrapper" ng-repeat="navItem in ctrl.navModel.node.children">
<a class="card-item" ng-href="{{::navItem.url}}">
<div class="card-item-header">
<div class="card-item-type">
</div>
</div>
<div class="card-item-body">
<figure class="card-item-figure">
<i class="{{navItem.icon}}"></i>
</figure>
<div class="card-item-details">
<div class="card-item-name">
{{navItem.text}}
</div>
<div class="card-item-sub-name">
{{navItem.description}}
</div>
</div>
</div>
</a>
</li>
</ol>
</section>
</div>
</div>

View File

@ -23,7 +23,7 @@ export class AlertListCtrl {
/** @ngInject */
constructor(private backendSrv, private $location, private $scope, navModelSrv) {
this.navModel = navModelSrv.getAlertingNav(0);
this.navModel = navModelSrv.getNav('alerting');
var params = $location.search();
this.filters.state = params.state || null;

View File

@ -25,7 +25,7 @@ export class AlertNotificationEditCtrl {
/** @ngInject */
constructor(private $routeParams, private backendSrv, private $location, private $templateCache, navModelSrv) {
this.navModel = navModelSrv.getAlertingNav();
this.navModel = navModelSrv.getNav('alerting', 'channels');
this.backendSrv.get(`/api/alert-notifiers`).then(notifiers => {
this.notifiers = notifiers;
@ -36,10 +36,14 @@ export class AlertNotificationEditCtrl {
}
if (!this.$routeParams.id) {
this.navModel.breadcrumbs.push({text: 'New channel'});
this.navModel.node = {text: 'New channel'};
return _.defaults(this.model, this.defaults);
}
return this.backendSrv.get(`/api/alert-notifications/${this.$routeParams.id}`).then(result => {
this.navModel.breadcrumbs.push({text: result.name});
this.navModel.node = {text: result.name};
return result;
});
}).then(model => {

View File

@ -14,7 +14,7 @@ export class AlertNotificationsListCtrl {
/** @ngInject */
constructor(private backendSrv, private $scope, navModelSrv) {
this.loadNotifications();
this.navModel = navModelSrv.getAlertingNav(1);
this.navModel = navModelSrv.getNav('alerting', 'channels');
}
loadNotifications() {

View File

@ -2,11 +2,16 @@
<div class="page-container" >
<div class="page-header">
<h1>Alert List</h1>
<page-h1 model="ctrl.navModel"></page-h1>
<a class="btn btn-inverse" ng-click="ctrl.openHowTo()">
<i class="fa fa-info-circle"></i>
How to add an alert
</a>
<a class="btn btn-inverse" href="alerting/notifications" >
<i class="fa fa-bell"></i>
Notification channels
</a>
</div>
<div class="gf-form-group">

View File

@ -2,8 +2,7 @@
<div class="page-container">
<div class="page-header">
<h1 ng-show="ctrl.model.id">Edit Channel</h1>
<h1 ng-show="!ctrl.model.id">New Channel</h1>
<page-h1 model="ctrl.navModel"></page-h1>
</div>
<form name="ctrl.theForm" ng-if="ctrl.notifiers">

View File

@ -2,7 +2,8 @@
<div class="page-container" >
<div class="page-header">
<h1>Notification channels</h1>
<page-h1 model="ctrl.navModel"></page-h1>
<a href="alerting/notification/new" class="btn btn-success pull-right">
<i class="fa fa-plus"></i>
New Channel

View File

@ -1,64 +1,87 @@
<navbar model="ctrl.navModel">
<div class="navbar">
<div class="navbar-inner">
<ul class="nav dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying">
<li>
<a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
</li>
<li>
<a ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
</li>
<li>
<a ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
</li>
</ul>
<div class="navbar-section-wrapper">
<a class="navbar-page-btn" ng-click="ctrl.showSearch()">
<i class="icon-gf icon-gf-dashboard"></i>
{{ctrl.dashboard.title}}
<i class="fa fa-caret-down"></i>
</a>
</div>
<ul class="nav pull-left dashnav-action-icons">
<li ng-show="::ctrl.dashboard.meta.canStar">
<a class="pointer" ng-click="ctrl.starDashboard()">
<i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}" style="color: orange;"></i>
</a>
</li>
<li ng-show="::ctrl.dashboard.meta.canShare" class="dropdown">
<a class="pointer" ng-click="ctrl.hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
<ul class="dropdown-menu">
<ul class="nav dash-playlist-actions" ng-if="ctrl.playlistSrv.isPlaying">
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(0)">
<i class="fa fa-link"></i> Link to Dashboard
<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
</a>
<a ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(1)">
<i class="icon-gf icon-gf-snapshot"></i>Snapshot
<div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
</a>
<a ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(2)">
<i class="fa fa-cloud-upload"></i>Export
<div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.com</div>
</a>
<a ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
</li>
</ul>
</li>
<li ng-show="::ctrl.dashboard.meta.canSave">
<a ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
</li>
<li ng-if="::ctrl.dashboard.snapshot.originalUrl">
<a ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
</li>
</ul>
<ul class="nav pull-right">
<li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard">
<a ng-click="ctrl.exitFullscreen()">
Back to dashboard
</a>
</li>
<li>
<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker>
</li>
</ul>
<ul class="nav pull-left dashnav-action-icons">
<li ng-show="::ctrl.dashboard.meta.canStar">
<a class="pointer" ng-click="ctrl.starDashboard()">
<i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}" style="color: orange;"></i>
</a>
</li>
<li ng-show="::ctrl.dashboard.meta.canShare" class="dropdown">
<a class="pointer" ng-click="ctrl.hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
<ul class="dropdown-menu">
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(0)">
<i class="fa fa-link"></i> Link to Dashboard
<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
</a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(1)">
<i class="icon-gf icon-gf-snapshot"></i>Snapshot
<div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
</a>
</li>
<li>
<a class="pointer" ng-click="ctrl.shareDashboard(2)">
<i class="fa fa-cloud-upload"></i>Export
<div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.com</div>
</a>
</li>
</ul>
</li>
<li ng-show="::ctrl.dashboard.meta.canSave">
<a ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom"><i class="fa fa-save"></i></a>
</li>
<li ng-if="::ctrl.dashboard.snapshot.originalUrl">
<a ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom"><i class="fa fa-link"></i></a>
</li>
<li class="dropdown">
<a class="pointer" data-toggle="dropdown">
<i class="fa fa-cog"></i>
</a>
<ul class="dropdown-menu dropdown-menu--navbar">
<li ng-repeat="navItem in ::ctrl.navModel.menu" ng-class="{active: navItem.active}">
<a class="pointer" ng-href="{{::navItem.url}}" ng-click="ctrl.navItemClicked(navItem, $event)">
<i class="{{::navItem.icon}}" ng-show="::navItem.icon"></i>
{{::navItem.title}}
</a>
</li>
</ul>
</li>
</ul>
</navbar>
<ul class="nav pull-right">
<li ng-show="ctrl.dashboard.meta.fullscreen" class="dashnav-back-to-dashboard">
<a ng-click="ctrl.exitFullscreen()">
Back to dashboard
</a>
</li>
<li>
<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker>
</li>
</ul>
</div>
</div>
<dashboard-search></dashboard-search>

View File

@ -144,6 +144,17 @@ export class DashNavCtrl {
onFolderChange(folderId) {
this.dashboard.folderId = folderId;
}
showSearch() {
this.$rootScope.appEvent('show-dash-search');
}
navItemClicked(navItem, evt) {
if (navItem.clickHandler) {
navItem.clickHandler();
evt.preventDefault();
}
}
}
export function dashNavDirective() {

View File

@ -83,33 +83,6 @@
</div>
</div>
</div>
</div>
<div ng-if="editor.index == 1">
<h5 class="section-heading">Rows settings</h5>
<div class="gf-form-group">
<div class="gf-form-inline" ng-repeat="row in dashboard.rows">
<div class="gf-form">
<span class="gf-form-label">Title</span>
<input type="text" class="gf-form-input max-width-14" ng-model='row.title'></input>
</div>
<gf-form-switch class="gf-form" label="Show title" checked="row.showTitle" switch-class="max-width-6"></gf-form-switch>
<div class="gf-form">
<button class="btn btn-inverse gf-form-btn" ng-click="_.move(dashboard.rows,$index,$index-1)">
<i ng-class="{'invisible': $first}" class="fa fa-arrow-up"></i>
</button>
<button class="btn btn-inverse gf-from-btn" ng-click="_.move(dashboard.rows,$index,$index+1)">
<i ng-class="{'invisible': $last}" class="fa fa-arrow-down"></i>
</button>
<button class="btn btn-inverse gf-form-btn" ng-click="dashboard.rows = _.without(dashboard.rows,row)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<div ng-if="editor.index == 2">

View File

@ -29,10 +29,6 @@ export class SubmenuCtrl {
var search = _.extend(this.$location.search(), {editview: editview});
this.$location.search(search);
}
exitBuildMode() {
this.dashboard.toggleEditMode();
}
}
export function submenuDirective() {

View File

@ -12,7 +12,7 @@ function (angular, config) {
$scope.command = {};
$scope.authProxyEnabled = config.authProxyEnabled;
$scope.ldapEnabled = config.ldapEnabled;
$scope.navModel = navModelSrv.getProfileNav();
$scope.navModel = navModelSrv.getNav('profile', 'change-password');
$scope.changePassword = function() {
if (!$scope.userForm.$valid) { return; }

View File

@ -8,7 +8,7 @@ function (angular) {
module.controller('OrgApiKeysCtrl', function($scope, $http, backendSrv, navModelSrv) {
$scope.navModel = navModelSrv.getOrgNav(0);
$scope.navModel = navModelSrv.getNav('cfg', 'apikeys');
$scope.roleTypes = ['Viewer', 'Editor', 'Admin'];
$scope.token = { role: 'Viewer' };

View File

@ -10,7 +10,7 @@ function (angular) {
$scope.init = function() {
$scope.getOrgInfo();
$scope.navModel = navModelSrv.getOrgNav(0);
$scope.navModel = navModelSrv.getNav('cfg', 'org');
};
$scope.getOrgInfo = function() {

View File

@ -23,7 +23,8 @@ export class OrgUsersCtrl {
loginOrEmail: '',
role: 'Viewer',
};
this.navModel = navModelSrv.getOrgNav(0);
this.navModel = navModelSrv.getNav('cfg', 'users');
this.get();
this.editor = { index: 0 };
@ -46,7 +47,7 @@ export class OrgUsersCtrl {
} else if (config.disableLoginForm) {
return "Add Users";
} else {
return "Add or Invite";
return "Add";
}
}

View File

@ -2,7 +2,7 @@
<div class="page-container">
<div class="page-header">
<h1>Change password</h1>
<page-h1 model="navModel"></page-h1>
</div>
<div ng-if="ldapEnabled || authProxyEnabled">

View File

@ -2,7 +2,7 @@
<div class="page-container">
<div class="page-header">
<h1>API Keys</h1>
<page-h1 model="navModel"></page-h1>
</div>
<h3 class="page-heading">Add new</h3>

View File

@ -2,14 +2,14 @@
<div class="page-container">
<div class="page-header">
<h1>Org Preferences</h1>
<page-h1 model="navModel"></page-h1>
</div>
<h3 class="page-heading">General</h3>
<form name="orgForm" class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form max-width-28">
<span class="gf-form-label width-6">Name</span>
<span class="gf-form-label">Organization name</span>
<input class="gf-form-input" type="text" required ng-model="org.name">
</div>
<div class="gf-form">
@ -61,16 +61,6 @@
</div>
</form>
<h3 class="page-heading">Admin Pages</h3>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form">
<a href="org/users" class="btn gf-form-btn btn-inverse">Users &amp; Roles</a>
</div>
<div class="gf-form">
<a href="org/apikeys" class="btn gf-form-btn btn-inverse">API Keys</a>
</div>
</div>
</div>

View File

@ -2,7 +2,10 @@
<div class="page-container">
<div class="page-header">
<h1>Organization users</h1>
<h1>
<i class="{{ctrl.navModel.node.icon}}"></i>
{{ctrl.navModel.node.text}}
</h1>
<div class="page-header-tabs">
@ -22,8 +25,13 @@
Users ({{ctrl.users.length}})
</a>
</li>
<li class="gf-tabs-item" ng-show="ctrl.showInviteUI">
<li class="gf-tabs-item">
<a class="gf-tabs-link" ng-click="ctrl.editor.index = 1" ng-class="{active: ctrl.editor.index === 1}">
Groups (0)
</a>
</li>
<li class="gf-tabs-item" ng-show="ctrl.pendingInvites.length">
<a class="gf-tabs-link" ng-click="ctrl.editor.index = 2" ng-class="{active: ctrl.editor.index === 2}">
Pending Invitations ({{ctrl.pendingInvites.length}})
</a>
</li>
@ -54,8 +62,10 @@
<td><span class="ellipsis">{{user.email}}</span></td>
<td>{{user.lastSeenAtAge}}</td>
<td>
<select type="text" ng-model="user.role" class="input-medium" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="ctrl.updateOrgUser(user)">
</select>
<div class="gf-form-select-wrapper width-9">
<select type="text" ng-model="user.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="ctrl.updateOrgUser(user)">
</select>
</div>
</td>
<td>
<a ng-click="ctrl.removeUser(user)" class="btn btn-danger btn-mini">
@ -66,7 +76,7 @@
</table>
</div>
<div ng-if="ctrl.editor.index === 1 && ctrl.showInviteUI">
<div ng-if="ctrl.editor.index === 2">
<table class="filter-table form-inline">
<thead>
<tr>

View File

@ -2,7 +2,7 @@
<div class="page-container">
<div class="page-header">
<h1>User Profile</h1>
<page-h1 model="ctrl.navModel"></page-h1>
</div>
<form name="ctrl.userForm" class="gf-form-group">

View File

@ -17,7 +17,7 @@ export class ProfileCtrl {
constructor(private backendSrv, private contextSrv, private $location, navModelSrv) {
this.getUser();
this.getUserOrgs();
this.navModel = navModelSrv.getProfileNav();
this.navModel = navModelSrv.getNav('profile');
}
getUser() {

View File

@ -9,7 +9,7 @@ export default class UserGroupDetailsCtrl {
navModel: any;
constructor(private $scope, private $http, private backendSrv, private $routeParams, navModelSrv) {
this.navModel = navModelSrv.getOrgNav(3);
this.navModel = navModelSrv.getNav('cfg', 'users');
this.get();
}

View File

@ -15,7 +15,7 @@ export class UserGroupsCtrl {
/** @ngInject */
constructor(private $scope, private $http, private backendSrv, private $location, navModelSrv) {
this.navModel = navModelSrv.getOrgNav(3);
this.navModel = navModelSrv.getNav('cfg', 'users');
this.get();
}

View File

@ -2,8 +2,7 @@
<div class="page-container" ng-form="playlistEditForm">
<div class="page-header">
<h1 ng-show="ctrl.isNew()">New Playlist</h1>
<h1 ng-show="!ctrl.isNew()">Edit Playlist</h1>
<page-h1 model="ctrl.navModel"></page-h1>
</div>
<p class="playlist-description">A playlist rotates through a pre-selected list of Dashboards. A Playlist can be a great way to build situational awareness, or just show off your metrics to your team or visitors.</p>

View File

@ -2,7 +2,7 @@
<div class="page-container">
<div class="page-header">
<h1>Saved playlists</h1>
<page-h1 model="ctrl.navModel"></page-h1>
<a class="btn btn-success pull-right" href="playlists/create">
<i class="fa fa-plus"></i>
New Playlist
@ -13,7 +13,7 @@
<thead>
<th><strong>Name</strong></th>
<th><strong>Start url</strong></th>
<th style="width: 68px"></th>
<th style="width: 78px"></th>
<th style="width: 78px"></th>
<th style="width: 25px"></th>
</thead>

View File

@ -27,19 +27,23 @@ export class PlaylistEditCtrl {
private $route,
private navModelSrv
) {
this.navModel = navModelSrv.getPlaylistsNav(0);
this.navModel = navModelSrv.getNav('dashboards', 'playlists');
if ($route.current.params.id) {
var playlistId = $route.current.params.id;
backendSrv.get('/api/playlists/' + playlistId).then(result => {
this.playlist = result;
this.navModel.node = {text: result.name, icon: this.navModel.node.icon};
this.navModel.breadcrumbs.push(this.navModel.node);
});
backendSrv.get('/api/playlists/' + playlistId + '/items').then(result => {
this.playlistItems = result;
});
} else {
this.navModel.node = {text: "New playlist", icon: this.navModel.node.icon};
this.navModel.breadcrumbs.push(this.navModel.node);
}
}

View File

@ -10,7 +10,7 @@ export class PlaylistsCtrl {
/** @ngInject */
constructor(private $scope, private $location, private backendSrv, private navModelSrv) {
this.navModel = navModelSrv.getPlaylistsNav(0);
this.navModel = navModelSrv.getNav('dashboards', 'playlists');
backendSrv.get('/api/playlists').then(result => {
this.playlists = result;

View File

@ -43,8 +43,7 @@ export class DataSourceEditCtrl {
private navModelSrv,
) {
this.navModel = navModelSrv.getDatasourceNav(0);
this.isNew = true;
this.navModel = navModelSrv.getNav('cfg', 'datasources');
this.datasources = [];
this.tabIndex = 0;
@ -58,8 +57,13 @@ export class DataSourceEditCtrl {
}
initNewDatasourceModel() {
this.isNew = true;
this.current = angular.copy(defaults);
// add to nav & breadcrumbs
this.navModel.node = {text: 'New data source', icon: 'icon-gf icon-gf-fw icon-gf-datasources'};
this.navModel.breadcrumbs.push(this.navModel.node);
// We are coming from getting started
if (this.$location.search().gettingstarted) {
this.gettingStarted = true;
@ -85,10 +89,14 @@ export class DataSourceEditCtrl {
this.backendSrv.get('/api/datasources/' + id).then(ds => {
this.isNew = false;
this.current = ds;
this.navModel.node = {text: ds.name, icon: 'icon-gf icon-gf-fw icon-gf-datasources'};
this.navModel.breadcrumbs.push(this.navModel.node);
if (datasourceCreated) {
datasourceCreated = false;
this.testDatasource();
}
return this.typeChanged();
});
}

View File

@ -15,10 +15,9 @@ export class DataSourcesCtrl {
private $http,
private backendSrv,
private datasourceSrv,
private navModelSrv
) {
private navModelSrv) {
this.navModel = this.navModelSrv.getDatasourceNav(0);
this.navModel = this.navModelSrv.getNav('cfg', 'datasources');
backendSrv.get('/api/datasources').then(result => {
this.datasources = result;

View File

@ -1,85 +1,87 @@
<navbar model="ctrl.navModel"></navbar>
<div class="scroll-canvas">
<div gemini-scrollbar>
<navbar model="ctrl.navModel"></navbar>
<div class="page-container">
<div class="page-header">
<page-h1 model="ctrl.navModel"></page-h1>
<div class="page-container">
<div class="page-header-tabs" ng-show="ctrl.hasDashboards">
<ul class="gf-tabs">
<li class="gf-tabs-item">
<a class="gf-tabs-link" ng-click="ctrl.tabIndex = 0" ng-class="{active: ctrl.tabIndex === 0}">
Config
</a>
</li>
<li class="gf-tabs-item">
<a class="gf-tabs-link" ng-click="ctrl.tabIndex = 1" ng-class="{active: ctrl.tabIndex === 1}">
Dashboards
</a>
</li>
</ul>
</div>
</div>
<div class="page-header">
<h1 ng-show="ctrl.isNew">Add data source</h1>
<h1 ng-hide="ctrl.isNew">Edit data source</h1>
<div class="page-header-tabs" ng-show="ctrl.hasDashboards">
<ul class="gf-tabs">
<li class="gf-tabs-item">
<a class="gf-tabs-link" ng-click="ctrl.tabIndex = 0" ng-class="{active: ctrl.tabIndex === 0}">
Config
</a>
</li>
<li class="gf-tabs-item">
<a class="gf-tabs-link" ng-click="ctrl.tabIndex = 1" ng-class="{active: ctrl.tabIndex === 1}">
Dashboards
</a>
</li>
</ul>
<div ng-if="ctrl.tabIndex === 0" class="tab-content">
<form name="ctrl.editForm" ng-if="ctrl.current">
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-7">Name</span>
<input class="gf-form-input max-width-23" type="text" ng-model="ctrl.current.name" placeholder="name" required>
<info-popover offset="0px -135px" mode="right-absolute">
The name is used when you select the data source in panels.
The <em>Default</em> data source is preselected in new
panels.
</info-popover>
</div>
<gf-form-switch class="gf-form" label="Default" checked="ctrl.current.isDefault" switch-class="max-width-6"></gf-form-switch>
</div>
<div class="gf-form">
<span class="gf-form-label width-7">Type</span>
<div class="gf-form-select-wrapper max-width-23">
<select class="gf-form-input" ng-model="ctrl.current.type" ng-options="v.id as v.name for v in ctrl.types" ng-change="ctrl.typeChanged()"></select>
</div>
</div>
</div>
<div class="alert alert-info gf-form-group" ng-if="ctrl.datasourceMeta.state === 'alpha'">
This plugin is marked as being in alpha state, which means it is in early development phase and
updates will include breaking changes.
</div>
<rebuild-on-change property="ctrl.datasourceMeta.id">
<plugin-component type="datasource-config-ctrl">
</plugin-component>
</rebuild-on-change>
<div ng-if="ctrl.testing" class="gf-form-group">
<h5 ng-show="!ctrl.testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
<div class="alert-{{ctrl.testing.status}} alert">
<div class="alert-title">{{ctrl.testing.title}}</div>
<div ng-bind='ctrl.testing.message'></div>
</div>
</div>
<div class="gf-form-button-row">
<button type="submit" class="btn btn-success" ng-show="ctrl.isNew" ng-click="ctrl.saveChanges()">Add</button>
<button type="submit" class="btn btn-success" ng-show="!ctrl.isNew" ng-click="ctrl.saveChanges()">Save &amp; Test</button>
<button type="submit" class="btn btn-danger" ng-show="!ctrl.isNew" ng-click="ctrl.delete()">
Delete
</button>
<a class="btn btn-link" href="datasources">Cancel</a>
</div>
</form>
</div>
<div ng-if="ctrl.tabIndex === 1" class="tab-content">
<dashboard-import-list plugin="ctrl.datasourceMeta" datasource="ctrl.current"></dashboard-import-list>
</div>
</div>
</div>
<div ng-if="ctrl.tabIndex === 0" class="tab-content">
<form name="ctrl.editForm" ng-if="ctrl.current">
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-7">Name</span>
<input class="gf-form-input max-width-23" type="text" ng-model="ctrl.current.name" placeholder="name" required>
<info-popover offset="0px -135px" mode="right-absolute">
The name is used when you select the data source in panels.
The <em>Default</em> data source is preselected in new
panels.
</info-popover>
</div>
<gf-form-switch class="gf-form" label="Default" checked="ctrl.current.isDefault" switch-class="max-width-6"></gf-form-switch>
</div>
<div class="gf-form">
<span class="gf-form-label width-7">Type</span>
<div class="gf-form-select-wrapper max-width-23">
<select class="gf-form-input" ng-model="ctrl.current.type" ng-options="v.id as v.name for v in ctrl.types" ng-change="ctrl.typeChanged()"></select>
</div>
</div>
</div>
<div class="alert alert-info gf-form-group" ng-if="ctrl.datasourceMeta.state === 'alpha'">
This plugin is marked as being in alpha state, which means it is in early development phase and
updates will include breaking changes.
</div>
<rebuild-on-change property="ctrl.datasourceMeta.id">
<plugin-component type="datasource-config-ctrl">
</plugin-component>
</rebuild-on-change>
<div ng-if="ctrl.testing" class="gf-form-group">
<h5 ng-show="!ctrl.testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
<div class="alert-{{ctrl.testing.status}} alert">
<div class="alert-title">{{ctrl.testing.title}}</div>
<div ng-bind='ctrl.testing.message'></div>
</div>
</div>
<div class="gf-form-button-row">
<button type="submit" class="btn btn-success" ng-show="ctrl.isNew" ng-click="ctrl.saveChanges()">Add</button>
<button type="submit" class="btn btn-success" ng-show="!ctrl.isNew" ng-click="ctrl.saveChanges()">Save &amp; Test</button>
<button type="submit" class="btn btn-danger" ng-show="!ctrl.isNew" ng-click="ctrl.delete()">
Delete
</button>
<a class="btn btn-link" href="datasources">Cancel</a>
</div>
</form>
</div>
<div ng-if="ctrl.tabIndex === 1" class="tab-content">
<dashboard-import-list plugin="ctrl.datasourceMeta" datasource="ctrl.current"></dashboard-import-list>
</div>
</div>

View File

@ -1,48 +1,52 @@
<navbar model="ctrl.navModel"></navbar>
<div class="scroll-canvas">
<div gemini-scrollbar>
<navbar model="ctrl.navModel"></navbar>
<div class="page-container">
<div class="page-header">
<page-h1 model="ctrl.navModel"></page-h1>
<div class="page-container">
<div class="page-header">
<h1>Data Sources</h1>
<a class="btn btn-success" href="datasources/new">
<i class="fa fa-plus"></i>
Add data source
</a>
</div>
<a class="page-header__cta btn btn-success" href="datasources/new">
<i class="fa fa-plus"></i>
Add data source
</a>
</div>
<section class="card-section" layout-mode>
<layout-selector></layout-selector>
<section class="card-section" layout-mode>
<layout-selector></layout-selector>
<ol class="card-list" >
<li class="card-item-wrapper" ng-repeat="ds in ctrl.datasources">
<a class="card-item" href="datasources/edit/{{ds.id}}/">
<div class="card-item-header">
<div class="card-item-type">
{{ds.type}}
</div>
</div>
<div class="card-item-body">
<figure class="card-item-figure">
<img ng-src="{{ds.typeLogoUrl}}">
</figure>
<div class="card-item-details">
<div class="card-item-name">
{{ds.name}}
<span ng-if="ds.isDefault">
<span class="btn btn-secondary btn-mini">default</span>
</span>
</div>
<div class="card-item-sub-name">
{{ds.url}}
</div>
</div>
</div>
</a>
</li>
</ol>
</section>
<div ng-if="ctrl.datasources.length === 0">
<em>No data sources defined</em>
</div>
<ol class="card-list" >
<li class="card-item-wrapper" ng-repeat="ds in ctrl.datasources">
<a class="card-item" href="datasources/edit/{{ds.id}}/">
<div class="card-item-header">
<div class="card-item-type">
{{ds.type}}
</div>
</div>
<div class="card-item-body">
<figure class="card-item-figure">
<img ng-src="{{ds.typeLogoUrl}}">
</figure>
<div class="card-item-details">
<div class="card-item-name">
{{ds.name}}
<span ng-if="ds.isDefault">
<span class="btn btn-secondary btn-mini">default</span>
</span>
</div>
<div class="card-item-sub-name">
{{ds.url}}
</div>
</div>
</div>
</a>
</li>
</ol>
</section>
<div ng-if="ctrl.datasources.length === 0">
<em>No data sources defined</em>
</div>
</div>
</div>
</div>

View File

@ -3,6 +3,7 @@
<div class="page-container">
<div class="page-header">
<h1>
<i class="icon-gf icon-gf-apps"></i>
Plugins <span class="muted small">(currently installed)</span>
</h1>

View File

@ -28,7 +28,7 @@ export class PluginEditCtrl {
private $http,
private navModelSrv,
) {
this.navModel = navModelSrv.getPluginsNav();
this.navModel = navModelSrv.getNav('cfg', 'plugins');
this.model = {};
this.pluginId = $routeParams.pluginId;
this.tabIndex = 0;
@ -42,6 +42,7 @@ export class PluginEditCtrl {
return this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(result => {
this.model = result;
this.pluginIcon = this.getPluginIcon(this.model.type);
this.navModel.breadcrumbs.push({text: this.model.name});
this.model.dependencies.plugins.forEach(plug => {
plug.icon = this.getPluginIcon(plug.type);

View File

@ -10,7 +10,7 @@ export class PluginListCtrl {
/** @ngInject */
constructor(private backendSrv: any, $location, navModelSrv) {
this.tabIndex = 0;
this.navModel = navModelSrv.getPluginsNav();
this.navModel = navModelSrv.getNav('cfg', 'plugins');
var pluginType = $location.search().type || 'panel';
switch (pluginType) {

View File

@ -2,7 +2,6 @@
import angular from 'angular';
import _ from 'lodash';
import {NavModel} from 'app/core/core';
var pluginInfoCache = {};
@ -10,10 +9,10 @@ export class AppPageCtrl {
page: any;
pluginId: any;
appModel: any;
navModel: NavModel;
navModel: any;
/** @ngInject */
constructor(private backendSrv, private $routeParams: any, private $rootScope) {
constructor(private backendSrv, private $routeParams: any, private $rootScope, private navModelSrv) {
this.pluginId = $routeParams.pluginId;
if (pluginInfoCache[this.pluginId]) {
@ -32,47 +31,12 @@ export class AppPageCtrl {
if (!this.page) {
this.$rootScope.appEvent('alert-error', ['App Page Not Found', '']);
this.navModel = {
section: {
title: "Page not found",
url: app.defaultNavUrl,
icon: 'icon-gf icon-gf-sadface',
},
menu: [],
};
this.navModel = this.navModelSrv.getNotFoundNav();
return;
}
let menu = [];
for (let item of app.includes) {
if (item.addToNav) {
if (item.type === 'dashboard') {
menu.push({
title: item.name,
url: 'dashboard/db/' + item.slug,
icon: 'fa fa-fw fa-dot-circle-o',
});
}
if (item.type === 'page') {
menu.push({
title: item.name,
url: `plugins/${app.id}/page/${item.slug}`,
icon: 'fa fa-fw fa-dot-circle-o',
});
}
}
}
this.navModel = {
section: {
title: app.name,
url: app.defaultNavUrl,
iconUrl: app.info.logos.small,
},
menu: menu,
};
this.navModel = this.navModelSrv.getNav('plugin-page-' + app.id);
this.navModel.breadcrumbs.push({text: this.page.name});
}
loadPluginInfo() {

View File

@ -2,20 +2,17 @@
<div class="page-container">
<div class="page-header">
<h1>Available snapshots</h1>
<page-h1 model="ctrl.navModel"></page-h1>
</div>
<table class="filter-table" style="margin-top: 20px">
<thead>
<th><strong>Name</strong></th>
<th><strong>Snapshot url</strong></th>
<th style="width: 70px"></th>
<th style="width: 25px"></th>
</thead>
<tr ng-repeat="snapshot in ctrl.snapshots">
</thead>
<tr ng-repeat="snapshot in ctrl.snapshots">
<td>
<a href="dashboard/snapshot/{{snapshot.key}}">{{snapshot.name}}</a>
</td>

View File

@ -8,16 +8,8 @@ export class SnapshotsCtrl {
snapshots: any;
/** @ngInject */
constructor(private $rootScope, private backendSrv) {
this.navModel = {
section: {
title: 'Snapshots',
icon: 'icon-gf icon-gf-snapshot',
url: 'dashboard/snapshots',
},
menu: [],
};
constructor(private $rootScope, private backendSrv, navModelSrv) {
this.navModel = navModelSrv.getNav('dashboards', 'snapshots');
this.backendSrv.get('/api/dashboard/snapshots').then(result => {
this.snapshots = result;
});

View File

@ -2,7 +2,7 @@
<div class="page-container">
<div class="page-header">
<h1>Style Guide</h1>
<page-h1 model="ctrl.navModel"></page-h1>
<a class="btn btn-success" ng-click="ctrl.switchTheme()">
<i class="fa fa-random"></i>

View File

@ -16,7 +16,7 @@ class StyleGuideCtrl {
/** @ngInject **/
constructor(private $http, private $routeParams, private $location, private backendSrv, navModelSrv) {
this.navModel = navModelSrv.getAdminNav();
this.navModel = navModelSrv.getNav('cfg', 'admin', 'styleguide');
this.theme = config.bootData.user.lightTheme ? 'light': 'dark';
this.page = {};

View File

@ -106,7 +106,7 @@ export class VariableEditorCtrl {
var clone = _.cloneDeep(variable.getSaveModel());
$scope.current = variableSrv.createVariableFromModel(clone);
$scope.current.name = 'copy_of_'+variable.name;
$scope.variableSrv.addVariable($scope.current);
variableSrv.addVariable($scope.current);
};
$scope.update = function() {

View File

@ -128,7 +128,7 @@ export class QueryVariable implements Variable {
}
metricFindQuery(datasource, query) {
var options = {range: undefined};
var options = {range: undefined, variable: this};
if (this.refresh === 2) {
options.range = this.timeSrv.timeRange();

View File

@ -51,6 +51,31 @@ describe('templateSrv', function() {
});
});
describe('getAdhocFilters', function() {
beforeEach(function() {
initTemplateSrv([
{type: 'datasource', name: 'ds', current: {value: 'logstash', text: 'logstash'}},
{type: 'adhoc', name: 'test', datasource: 'oogle', filters: [1]},
{type: 'adhoc', name: 'test2', datasource: '$ds', filters: [2]},
]);
});
it('should return filters if datasourceName match', function() {
var filters = _templateSrv.getAdhocFilters('oogle');
expect(filters).to.eql([1]);
});
it('should return empty array if datasourceName does not match', function() {
var filters = _templateSrv.getAdhocFilters('oogleasdasd');
expect(filters).to.eql([]);
});
it('should return filters when datasourceName match via data source variable', function() {
var filters = _templateSrv.getAdhocFilters('logstash');
expect(filters).to.eql([2]);
});
});
describe('replace can pass multi / all format', function() {
beforeEach(function() {
initTemplateSrv([{type: 'query', name: 'test', current: {value: ['value1', 'value2'] }}]);

View File

@ -15,7 +15,6 @@ function (angular, _, kbn) {
this._index = {};
this._texts = {};
this._grafanaVariables = {};
this._adhocVariables = {};
// default built ins
this._builtIns = {};
@ -30,24 +29,16 @@ function (angular, _, kbn) {
this.updateTemplateData = function() {
this._index = {};
this._filters = {};
this._adhocVariables = {};
for (var i = 0; i < this.variables.length; i++) {
var variable = this.variables[i];
// add adhoc filters to it's own index
if (variable.type === 'adhoc') {
this._adhocVariables[variable.datasource] = variable;
continue;
}
if (!variable.current || !variable.current.isNone && !variable.current.value) {
continue;
}
this._index[variable.name] = variable;
}
};
this.variableInitialized = function(variable) {
@ -55,11 +46,26 @@ function (angular, _, kbn) {
};
this.getAdhocFilters = function(datasourceName) {
var variable = this._adhocVariables[datasourceName];
if (variable) {
return variable.filters || [];
var filters = [];
for (var i = 0; i < this.variables.length; i++) {
var variable = this.variables[i];
if (variable.type !== 'adhoc') {
continue;
}
if (variable.datasource === datasourceName) {
filters = filters.concat(variable.filters);
}
if (variable.datasource.indexOf('$') === 0) {
if (this.replace(variable.datasource) === datasourceName) {
filters = filters.concat(variable.filters);
}
}
}
return [];
return filters;
};
function luceneEscape(value) {

View File

@ -247,8 +247,6 @@ export class VariableSrv {
}
filter.operator = options.operator;
variable.setFilters(filters);
this.variableUpdated(variable, true);
}

View File

@ -77,3 +77,8 @@ declare module 'gridstack' {
var gridstack: any;
export default gridstack;
}
declare module 'gemini-scrollbar' {
var d3: any;
export default d3;
}

View File

@ -1,23 +1,27 @@
<div dash-class ng-if="dashboard">
<dashnav dashboard="dashboard"></dashnav>
<div class="dashboard-container">
<div dash-editor-view class="dash-edit-view"></div>
<div class="clearfix"></div>
<div class="scroll-canvas scroll-canvas--dashboard">
<div gemini-scrollbar>
<div class="dashboard-container">
<div dash-editor-view class="dash-edit-view"></div>
<div class="clearfix"></div>
<dashboard-submenu ng-if="dashboard.meta.submenuEnabled" dashboard="dashboard"></dashboard-submenu>
<dashboard-submenu ng-if="dashboard.meta.submenuEnabled" dashboard="dashboard"></dashboard-submenu>
<div class="clearfix"></div>
<div class="clearfix"></div>
<dash-row class="dash-row" ng-repeat="row in dashboard.rows" row="row" dashboard="dashboard">
</dash-row>
<dash-row class="dash-row" ng-repeat="row in dashboard.rows" row="row" dashboard="dashboard">
</dash-row>
<div ng-show='dashboard.meta.canEdit && !dashboard.meta.fullscreen' class="add-row-panel-hint">
<div class="span12" style="text-align:left;">
<span style="margin-left: 12px;" ng-click="addRowDefault()" class="pointer btn btn-inverse btn-small">
<span><i class="fa fa-plus"></i> ADD ROW</span>
</span>
</div>
</div>
</div>
<div ng-show='dashboard.meta.canEdit && !dashboard.meta.fullscreen' class="add-row-panel-hint">
<div class="span12" style="text-align:left;">
<span style="margin-left: 12px;" ng-click="addRowDefault()" class="pointer btn btn-inverse btn-small">
<span><i class="fa fa-plus"></i> ADD ROW</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div class="container">
<div class="login-container container">
<div class="signup-page-background">
</div>

View File

@ -1,24 +1,27 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import ResponseParser from './response_parser';
export class MysqlDatasource {
id: any;
name: any;
responseParser: ResponseParser;
/** @ngInject **/
constructor(instanceSettings, private backendSrv, private $q, private templateSrv) {
this.name = instanceSettings.name;
this.id = instanceSettings.id;
this.responseParser = new ResponseParser(this.$q);
}
interpolateVariable(value) {
if (typeof value === 'string') {
return '\"' + value + '\"';
return '\'' + value + '\'';
}
var quotedValues = _.map(value, function(val) {
return '\"' + val + '\"';
return '\'' + val + '\'';
});
return quotedValues.join(',');
}
@ -49,7 +52,7 @@ export class MysqlDatasource {
to: options.range.to.valueOf().toString(),
queries: queries,
}
}).then(this.processQueryResult.bind(this));
}).then(this.responseParser.processQueryResult);
}
annotationQuery(options) {
@ -72,46 +75,30 @@ export class MysqlDatasource {
to: options.range.to.valueOf().toString(),
queries: [query],
}
}).then(this.transformAnnotationResponse.bind(this, options));
}).then(data => this.responseParser.transformAnnotationResponse(options, data));
}
transformAnnotationResponse(options, data) {
const table = data.data.results[options.annotation.name].tables[0];
metricFindQuery(query, optionalOptions) {
let refId = 'tempvar';
if (optionalOptions && optionalOptions.variable && optionalOptions.variable.name) {
refId = optionalOptions.variable.name;
}
let timeColumnIndex = -1;
let titleColumnIndex = -1;
let textColumnIndex = -1;
let tagsColumnIndex = -1;
const interpolatedQuery = {
refId: refId,
datasourceId: this.id,
rawSql: this.templateSrv.replace(query, {}, this.interpolateVariable),
format: 'table',
};
for (let i = 0; i < table.columns.length; i++) {
if (table.columns[i].text === 'time_sec') {
timeColumnIndex = i;
} else if (table.columns[i].text === 'title') {
titleColumnIndex = i;
} else if (table.columns[i].text === 'text') {
textColumnIndex = i;
} else if (table.columns[i].text === 'tags') {
tagsColumnIndex = i;
return this.backendSrv.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
queries: [interpolatedQuery],
}
}
if (timeColumnIndex === -1) {
return this.$q.reject({message: 'Missing mandatory time column (with time_sec column alias) in annotation query.'});
}
const list = [];
for (let i = 0; i < table.rows.length; i++) {
const row = table.rows[i];
list.push({
annotation: options.annotation,
time: Math.floor(row[timeColumnIndex]) * 1000,
title: row[titleColumnIndex],
text: row[textColumnIndex],
tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : []
});
}
return list;
})
.then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
}
testDatasource() {
@ -141,39 +128,5 @@ export class MysqlDatasource {
}
});
}
processQueryResult(res) {
var data = [];
if (!res.data.results) {
return {data: data};
}
for (let key in res.data.results) {
let queryRes = res.data.results[key];
if (queryRes.series) {
for (let series of queryRes.series) {
data.push({
target: series.name,
datapoints: series.points,
refId: queryRes.refId,
meta: queryRes.meta,
});
}
}
if (queryRes.tables) {
for (let table of queryRes.tables) {
table.type = 'table';
table.refId = queryRes.refId;
table.meta = queryRes.meta;
data.push(table);
}
}
}
return {data: data};
}
}

View File

@ -14,7 +14,6 @@
}
},
"state": "alpha",
"alerting": true,
"annotations": true,
"metrics": true

View File

@ -0,0 +1,143 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
export default class ResponseParser {
constructor(private $q){}
processQueryResult(res) {
var data = [];
if (!res.data.results) {
return {data: data};
}
for (let key in res.data.results) {
let queryRes = res.data.results[key];
if (queryRes.series) {
for (let series of queryRes.series) {
data.push({
target: series.name,
datapoints: series.points,
refId: queryRes.refId,
meta: queryRes.meta,
});
}
}
if (queryRes.tables) {
for (let table of queryRes.tables) {
table.type = 'table';
table.refId = queryRes.refId;
table.meta = queryRes.meta;
data.push(table);
}
}
}
return {data: data};
}
parseMetricFindQueryResult(refId, results) {
if (!results || results.data.length === 0 || results.data.results[refId].meta.rowCount === 0) { return []; }
const columns = results.data.results[refId].tables[0].columns;
const rows = results.data.results[refId].tables[0].rows;
const textColIndex = this.findColIndex(columns, '__text');
const valueColIndex = this.findColIndex(columns, '__value');
if (columns.length === 2 && textColIndex !== -1 && valueColIndex !== -1){
return this.transformToKeyValueList(rows, textColIndex, valueColIndex);
}
return this.transformToSimpleList(rows);
}
transformToKeyValueList(rows, textColIndex, valueColIndex) {
const res = [];
for (let i = 0; i < rows.length; i++) {
if (!this.containsKey(res, rows[i][textColIndex])) {
res.push({text: rows[i][textColIndex], value: rows[i][valueColIndex]});
}
}
return res;
}
transformToSimpleList(rows) {
const res = [];
for (let i = 0; i < rows.length; i++) {
for (let j = 0; j < rows[i].length; j++) {
const value = rows[i][j];
if ( res.indexOf( value ) === -1 ) {
res.push(value);
}
}
}
return _.map(res, value => {
return { text: value};
});
}
findColIndex(columns, colName) {
for (let i = 0; i < columns.length; i++) {
if (columns[i].text === colName) {
return i;
}
}
return -1;
}
containsKey(res, key) {
for (let i = 0; i < res.length; i++) {
if (res[i].text === key) {
return true;
}
}
return false;
}
transformAnnotationResponse(options, data) {
const table = data.data.results[options.annotation.name].tables[0];
let timeColumnIndex = -1;
let titleColumnIndex = -1;
let textColumnIndex = -1;
let tagsColumnIndex = -1;
for (let i = 0; i < table.columns.length; i++) {
if (table.columns[i].text === 'time_sec') {
timeColumnIndex = i;
} else if (table.columns[i].text === 'title') {
titleColumnIndex = i;
} else if (table.columns[i].text === 'text') {
textColumnIndex = i;
} else if (table.columns[i].text === 'tags') {
tagsColumnIndex = i;
}
}
if (timeColumnIndex === -1) {
return this.$q.reject({message: 'Missing mandatory time column (with time_sec column alias) in annotation query.'});
}
const list = [];
for (let i = 0; i < table.rows.length; i++) {
const row = table.rows[i];
list.push({
annotation: options.annotation,
time: Math.floor(row[timeColumnIndex]) * 1000,
title: row[titleColumnIndex],
text: row[textColumnIndex],
tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : []
});
}
return list;
}
}

View File

@ -76,4 +76,122 @@ describe('MySQLDatasource', function() {
});
});
describe('When performing metricFindQuery', function() {
let results;
const query = 'select * from atable';
const response = {
results: {
tempvar: {
meta: {
rowCount: 3
},
refId: 'tempvar',
tables: [
{
columns: [{text: 'title'}, {text: 'text'}],
rows: [
['aTitle', 'some text'],
['aTitle2', 'some text2'],
['aTitle3', 'some text3']
]
}
]
}
}
};
beforeEach(function() {
ctx.backendSrv.datasourceRequest = function(options) {
return ctx.$q.when({data: response, status: 200});
};
ctx.ds.metricFindQuery(query).then(function(data) { results = data; });
ctx.$rootScope.$apply();
});
it('should return list of all column values', function() {
expect(results.length).to.be(6);
expect(results[0].text).to.be('aTitle');
expect(results[5].text).to.be('some text3');
});
});
describe('When performing metricFindQuery with key, value columns', function() {
let results;
const query = 'select * from atable';
const response = {
results: {
tempvar: {
meta: {
rowCount: 3
},
refId: 'tempvar',
tables: [
{
columns: [{text: '__value'}, {text: '__text'}],
rows: [
['value1', 'aTitle'],
['value2', 'aTitle2'],
['value3', 'aTitle3']
]
}
]
}
}
};
beforeEach(function() {
ctx.backendSrv.datasourceRequest = function(options) {
return ctx.$q.when({data: response, status: 200});
};
ctx.ds.metricFindQuery(query).then(function(data) { results = data; });
ctx.$rootScope.$apply();
});
it('should return list of as text, value', function() {
expect(results.length).to.be(3);
expect(results[0].text).to.be('aTitle');
expect(results[0].value).to.be('value1');
expect(results[2].text).to.be('aTitle3');
expect(results[2].value).to.be('value3');
});
});
describe('When performing metricFindQuery with key, value columns and with duplicate keys', function() {
let results;
const query = 'select * from atable';
const response = {
results: {
tempvar: {
meta: {
rowCount: 3
},
refId: 'tempvar',
tables: [
{
columns: [{text: '__text'}, {text: '__value'}],
rows: [
['aTitle', 'same'],
['aTitle', 'same'],
['aTitle', 'diff']
]
}
]
}
}
};
beforeEach(function() {
ctx.backendSrv.datasourceRequest = function(options) {
return ctx.$q.when({data: response, status: 200});
};
ctx.ds.metricFindQuery(query).then(function(data) { results = data; });
ctx.$rootScope.$apply();
});
it('should return list of unique keys', function() {
expect(results.length).to.be(1);
expect(results[0].text).to.be('aTitle');
expect(results[0].value).to.be('same');
});
});
});

View File

@ -1,4 +1,4 @@
<query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="true">
<query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="false">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<textarea rows="3" class="gf-form-input" ng-model="ctrl.target.expr" spellcheck="false" placeholder="query expression" data-min-length=0 data-items=100 give-focus="ctrl.target.refId == 'A'" ng-model-onblur ng-change="ctrl.refreshMetricData()"></textarea>

View File

@ -498,7 +498,7 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
logBase: panel.yaxes[0].logBase || 1,
min: panel.yaxes[0].min ? _.toNumber(panel.yaxes[0].min) : null,
max: panel.yaxes[0].max ? _.toNumber(panel.yaxes[0].max) : null,
tickDecimals: panel.yaxes[0].decimals !== null ? _.toNumber(panel.yaxes[0].decimals): null
tickDecimals: panel.yaxes[0].decimals
};
options.yaxes.push(defaults);

View File

@ -157,9 +157,9 @@ export class TableRenderer {
// because of the fixed table headers css only solution
// there is an issue if header cell is wider the cell
// this hack adds header content to cell (not visible)
var widthHack = '';
var columnHtml = '';
if (addWidthHack) {
widthHack = '<div class="table-panel-width-hack">' + this.table.columns[columnIndex].title + '</div>';
columnHtml = '<div class="table-panel-width-hack">' + this.table.columns[columnIndex].title + '</div>';
}
if (value === undefined) {
@ -173,8 +173,6 @@ export class TableRenderer {
cellClasses.push("table-panel-cell-pre");
}
var columnHtml = widthHack + value;
if (column.style && column.style.link) {
// Render cell as link
var scopedVars = this.renderRowVariables(rowIndex);
@ -185,11 +183,13 @@ export class TableRenderer {
var cellTarget = column.style.linkTargetBlank ? '_blank' : '';
cellClasses.push("table-panel-cell-link");
columnHtml = `
columnHtml += `
<a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right">
${columnHtml}
${value}
</a>
`;
} else {
columnHtml += value;
}
if (column.filterable) {

View File

@ -2,7 +2,7 @@ System.config({
defaultJSExtenions: true,
baseURL: 'public',
paths: {
'virtual-scroll': 'vendor/npm/virtual-scroll/src/index.js',
'gemini-scrollbar': 'vendor/npm/gemini-scrollbar/index.js',
'mousetrap': 'vendor/npm/mousetrap/mousetrap.js',
'remarkable': 'vendor/npm/remarkable/dist/remarkable.js',
'tether': 'vendor/npm/tether/dist/js/tether.js',
@ -71,10 +71,6 @@ System.config({
format: 'global',
deps: ['gridstack'],
},
'vendor/npm/virtual-scroll/src/indx.js': {
format: 'cjs',
exports: 'VirtualScroll',
},
'vendor/angular/angular.js': {
format: 'global',
deps: ['jquery'],

Binary file not shown.

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