From cf926134ef03d4a1ccbb44178660181ff40b8fd5 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Sat, 5 Mar 2016 13:15:49 -0800 Subject: [PATCH 001/142] Added savePreferencesAPI --- pkg/api/api.go | 1 + pkg/api/user.go | 21 ++++++++++++++++++++- pkg/models/preferences.go | 28 ++++++++++++++++++++++++++++ pkg/services/sqlstore/user.go | 25 +++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 pkg/models/preferences.go diff --git a/pkg/api/api.go b/pkg/api/api.go index ed029a5171a..3f805cd9c98 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -96,6 +96,7 @@ func Register(r *macaron.Macaron) { r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard)) r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword)) r.Get("/quotas", wrap(GetUserQuotas)) + r.Put("/prefs", bind(m.SavePreferenceCommand{}), wrap(SaveUserPreferences)) }) // users (admin permission required) diff --git a/pkg/api/user.go b/pkg/api/user.go index 5af243eeb22..0f972431426 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/log" ) // GET /api/user (current authenticated user) @@ -110,7 +111,10 @@ func UserSetUsingOrg(c *middleware.Context) Response { } func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response { - userQuery := m.GetUserByIdQuery{Id: c.UserId} + + log.Info("%v", cmd) + + userQuery := m.GetUserByIdQuery{Id: c.UserId} if err := bus.Dispatch(&userQuery); err != nil { return ApiError(500, "Could not read user from database", err) @@ -144,3 +148,18 @@ func SearchUsers(c *middleware.Context) Response { return Json(200, query.Result) } + +func SaveUserPreferences(c *middleware.Context, cmd m.SavePreferenceCommand) Response { + + log.Info("%v", cmd.PrefData) + + cmd.PrefId = c.UserId + cmd.PrefType = `user` + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to saved user preferences", err) + } + + return ApiSuccess("User preferences saved") + +} diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go new file mode 100644 index 00000000000..cdaab0f6af8 --- /dev/null +++ b/pkg/models/preferences.go @@ -0,0 +1,28 @@ +package models + +import ( + "errors" +) + +// Typed errors +var ( + ErrPreferenceNotFound = errors.New("Preference not found") +) + +type Preference struct { + Id int64 + PrefId int64 + PrefType string + PrefData map[string]interface{} +} + +// --------------------- +// COMMANDS + +type SavePreferenceCommand struct { + + PrefData map[string]interface{} `json:"prefData"` + PrefId int64 `json:"-"` + PrefType string `json:"-"` + +} diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 96b8c24b8fc..a5c992d10b6 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -11,6 +11,7 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/log" ) func init() { @@ -27,6 +28,7 @@ func init() { bus.AddHandler("sql", DeleteUser) bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", UpdateUserPermissions) + bus.AddHandler("sql", SaveUserPreferences) } func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *session) (int64, error) { @@ -346,3 +348,26 @@ func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error { return err }) } + +func SaveUserPreferences(cmd *m.SavePreferenceCommand) error { + return inTransaction2(func(sess *session) error { + + log.Info("%v", cmd) + + pref := m.Preference{ + PrefId: cmd.PrefId, + PrefType: cmd.PrefType, + PrefData: cmd.PrefData, + } + + sess.Table("preferences").Where("pref_id", pref.PrefId).And("pref_type", pref.PrefType) + + if _, err := sess.Update(&pref); err != nil { + return err + } + + log.Info("%v", pref) + + return nil + }) +} From 1ef332e82ceacbfa0ab7ff1d038016f6e282353e Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Sat, 5 Mar 2016 23:44:56 -0800 Subject: [PATCH 002/142] preferences UI poc --- pkg/api/user.go | 3 -- .../app/core/components/sidemenu/sidemenu.ts | 1 + public/app/core/routes/routes.ts | 4 +++ public/app/features/all.js | 1 + .../profile/partials/preferences.html | 32 +++++++++++++++++++ .../features/profile/partials/profile.html | 2 +- .../app/features/profile/preferencesCtrl.js | 25 +++++++++++++++ 7 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 public/app/features/profile/partials/preferences.html create mode 100644 public/app/features/profile/preferencesCtrl.js diff --git a/pkg/api/user.go b/pkg/api/user.go index 0f972431426..d4545a7c709 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -111,9 +111,6 @@ func UserSetUsingOrg(c *middleware.Context) Response { } func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response { - - log.Info("%v", cmd) - userQuery := m.GetUserByIdQuery{Id: c.UserId} if err := bus.Dispatch(&userQuery); err != nil { diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index 4c0e100f85c..f68edf8fca5 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -39,6 +39,7 @@ export class SideMenuCtrl { this.orgMenu = [ {section: 'You', cssClass: 'dropdown-menu-title'}, {text: 'Profile', url: this.getUrl('/profile')}, + {text: 'Preferences', url: this.getUrl('/preferences')}, ]; if (this.isSignedIn) { diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 6e3af683c82..334800c22f5 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -89,6 +89,10 @@ function setupAngularRoutes($routeProvider, $locationProvider) { templateUrl: 'public/app/features/profile/partials/profile.html', controller : 'ProfileCtrl', }) + .when('/preferences', { + templateUrl: 'public/app/features/profile/partials/preferences.html', + controller : 'PreferencesCtrl', + }) .when('/profile/password', { templateUrl: 'public/app/features/profile/partials/password.html', controller : 'ChangePasswordCtrl', diff --git a/public/app/features/all.js b/public/app/features/all.js index c110bcff7cd..94da3809ae7 100644 --- a/public/app/features/all.js +++ b/public/app/features/all.js @@ -10,5 +10,6 @@ define([ './profile/profileCtrl', './profile/changePasswordCtrl', './profile/selectOrgCtrl', + './profile/preferencesCtrl', './styleguide/styleguide', ], function () {}); diff --git a/public/app/features/profile/partials/preferences.html b/public/app/features/profile/partials/preferences.html new file mode 100644 index 00000000000..093163dde32 --- /dev/null +++ b/public/app/features/profile/partials/preferences.html @@ -0,0 +1,32 @@ + + + +
+ + +
+
+ Home Dashboard + +
+ +
+ Time Range + +
+ +
+ Theme + +
+ +
+ + Cancel +
+
+ +
+ diff --git a/public/app/features/profile/partials/profile.html b/public/app/features/profile/partials/profile.html index 042344eeac6..dd5afebeaac 100644 --- a/public/app/features/profile/partials/profile.html +++ b/public/app/features/profile/partials/profile.html @@ -7,7 +7,7 @@
-

Preferences

+

Information

Name diff --git a/public/app/features/profile/preferencesCtrl.js b/public/app/features/profile/preferencesCtrl.js new file mode 100644 index 00000000000..00e7d59182c --- /dev/null +++ b/public/app/features/profile/preferencesCtrl.js @@ -0,0 +1,25 @@ +define([ + 'angular', + 'app/core/config', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('PreferencesCtrl', function($scope, backendSrv, $location) { + + $scope.command = {}; + + $scope.setUserPreferences = function() { + if (!$scope.userForm.$valid) { return; } + + console.log($scope.command); + + backendSrv.put('/api/user/prefs', $scope.command).then(function() { + $location.path("profile"); + }); + }; + + }); +}); From 660d3fa1e9a75e79468eb66c2dd14ff3f282e318 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Sun, 6 Mar 2016 03:47:39 -0800 Subject: [PATCH 003/142] Implemented savePreferences API --- pkg/api/api.go | 2 +- pkg/api/preferences.go | 21 +++++++++++ pkg/api/user.go | 18 +-------- pkg/models/preferences.go | 18 ++++----- pkg/services/sqlstore/preferences.go | 37 +++++++++++++++++++ pkg/services/sqlstore/user.go | 25 ------------- .../profile/partials/preferences.html | 6 +-- .../app/features/profile/preferencesCtrl.js | 4 +- 8 files changed, 73 insertions(+), 58 deletions(-) create mode 100644 pkg/api/preferences.go create mode 100644 pkg/services/sqlstore/preferences.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 3f805cd9c98..a3d206c4956 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -96,7 +96,7 @@ func Register(r *macaron.Macaron) { r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard)) r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword)) r.Get("/quotas", wrap(GetUserQuotas)) - r.Put("/prefs", bind(m.SavePreferenceCommand{}), wrap(SaveUserPreferences)) + r.Put("/prefs", bind(m.SavePreferencesCommand{}), wrap(SaveUserPreferences)) }) // users (admin permission required) diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go new file mode 100644 index 00000000000..849ed4aa8aa --- /dev/null +++ b/pkg/api/preferences.go @@ -0,0 +1,21 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" +) + +// PUT /api/user/prefs +func SaveUserPreferences(c *middleware.Context, cmd m.SavePreferencesCommand) Response { + + cmd.PrefId = c.UserId + cmd.PrefType = `user` + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to saved user preferences", err) + } + + return ApiSuccess("User preferences saved") + +} diff --git a/pkg/api/user.go b/pkg/api/user.go index d4545a7c709..5af243eeb22 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" - "github.com/grafana/grafana/pkg/log" ) // GET /api/user (current authenticated user) @@ -111,7 +110,7 @@ func UserSetUsingOrg(c *middleware.Context) Response { } func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response { - userQuery := m.GetUserByIdQuery{Id: c.UserId} + userQuery := m.GetUserByIdQuery{Id: c.UserId} if err := bus.Dispatch(&userQuery); err != nil { return ApiError(500, "Could not read user from database", err) @@ -145,18 +144,3 @@ func SearchUsers(c *middleware.Context) Response { return Json(200, query.Result) } - -func SaveUserPreferences(c *middleware.Context, cmd m.SavePreferenceCommand) Response { - - log.Info("%v", cmd.PrefData) - - cmd.PrefId = c.UserId - cmd.PrefType = `user` - - if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to saved user preferences", err) - } - - return ApiSuccess("User preferences saved") - -} diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go index cdaab0f6af8..8476ec49429 100644 --- a/pkg/models/preferences.go +++ b/pkg/models/preferences.go @@ -9,20 +9,18 @@ var ( ErrPreferenceNotFound = errors.New("Preference not found") ) -type Preference struct { - Id int64 - PrefId int64 - PrefType string - PrefData map[string]interface{} +type Preferences struct { + Id int64 + PrefId int64 + PrefType string + PrefData map[string]interface{} } // --------------------- // COMMANDS -type SavePreferenceCommand struct { - - PrefData map[string]interface{} `json:"prefData"` - PrefId int64 `json:"-"` +type SavePreferencesCommand struct { + PrefData map[string]interface{} `json:"prefData" binding:"Required"` + PrefId int64 `json:"-"` PrefType string `json:"-"` - } diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go new file mode 100644 index 00000000000..756048360ec --- /dev/null +++ b/pkg/services/sqlstore/preferences.go @@ -0,0 +1,37 @@ +package sqlstore + +import ( + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", SavePreferences) +} + +func SavePreferences(cmd *m.SavePreferencesCommand) error { + return inTransaction2(func(sess *session) error { + + sql := `SELECT * FROM preferences WHERE pref_id = ? ` + + `AND pref_type = ?` + + var prefResults = make([]m.Preferences, 0) + + resultsErr := sess.Sql(sql, cmd.PrefId, cmd.PrefType).Find(&prefResults) + + if resultsErr != nil { + return resultsErr + } + + var matchedPref m.Preferences + matchedPref = prefResults[0] + matchedPref.PrefData = cmd.PrefData + affectedRows, updateErr := sess.Id(matchedPref.Id).Update(&matchedPref) + + if affectedRows == 0 { + return m.ErrPreferenceNotFound + } + + return updateErr + }) +} diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index a5c992d10b6..96b8c24b8fc 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -11,7 +11,6 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - "github.com/grafana/grafana/pkg/log" ) func init() { @@ -28,7 +27,6 @@ func init() { bus.AddHandler("sql", DeleteUser) bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", UpdateUserPermissions) - bus.AddHandler("sql", SaveUserPreferences) } func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *session) (int64, error) { @@ -348,26 +346,3 @@ func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error { return err }) } - -func SaveUserPreferences(cmd *m.SavePreferenceCommand) error { - return inTransaction2(func(sess *session) error { - - log.Info("%v", cmd) - - pref := m.Preference{ - PrefId: cmd.PrefId, - PrefType: cmd.PrefType, - PrefData: cmd.PrefData, - } - - sess.Table("preferences").Where("pref_id", pref.PrefId).And("pref_type", pref.PrefType) - - if _, err := sess.Update(&pref); err != nil { - return err - } - - log.Info("%v", pref) - - return nil - }) -} diff --git a/public/app/features/profile/partials/preferences.html b/public/app/features/profile/partials/preferences.html index 093163dde32..39cf64957cb 100644 --- a/public/app/features/profile/partials/preferences.html +++ b/public/app/features/profile/partials/preferences.html @@ -9,17 +9,17 @@
Home Dashboard - +
Time Range - +
Theme - +
diff --git a/public/app/features/profile/preferencesCtrl.js b/public/app/features/profile/preferencesCtrl.js index 00e7d59182c..f9d1e4da4f0 100644 --- a/public/app/features/profile/preferencesCtrl.js +++ b/public/app/features/profile/preferencesCtrl.js @@ -9,14 +9,14 @@ function (angular) { module.controller('PreferencesCtrl', function($scope, backendSrv, $location) { - $scope.command = {}; + $scope.prefData = {}; $scope.setUserPreferences = function() { if (!$scope.userForm.$valid) { return; } console.log($scope.command); - backendSrv.put('/api/user/prefs', $scope.command).then(function() { + backendSrv.put('/api/user/prefs', { prefData : $scope.prefData }).then(function() { $location.path("profile"); }); }; From 8f42bec270428bd1714258dd7cd0d8e60d7d87af Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Sun, 6 Mar 2016 11:42:15 -0800 Subject: [PATCH 004/142] Implemented GetUserPreferences API --- pkg/api/api.go | 2 +- pkg/api/preferences.go | 11 +++++++++++ pkg/models/preferences.go | 19 +++++++++++++++++++ pkg/services/sqlstore/preferences.go | 22 ++++++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index a3d206c4956..c02629ddd8f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -96,7 +96,7 @@ func Register(r *macaron.Macaron) { r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard)) r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword)) r.Get("/quotas", wrap(GetUserQuotas)) - r.Put("/prefs", bind(m.SavePreferencesCommand{}), wrap(SaveUserPreferences)) + r.Combo("/prefs").Get(GetUserPreferences).Put(bind(m.SavePreferencesCommand{}), wrap(SaveUserPreferences)) }) // users (admin permission required) diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index 849ed4aa8aa..3a72467acb0 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -19,3 +19,14 @@ func SaveUserPreferences(c *middleware.Context, cmd m.SavePreferencesCommand) Re return ApiSuccess("User preferences saved") } + +func GetUserPreferences(c *middleware.Context) Response { + + query := m.GetPreferencesQuery{PrefId: c.UserId, PrefType: `user`} + + if err := bus.Dispatch(&query); err != nil { + return ApiError(500, "Failed to get user", err) + } + + return Json(200, query.Result) +} diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go index 8476ec49429..d54bbcda9bc 100644 --- a/pkg/models/preferences.go +++ b/pkg/models/preferences.go @@ -16,6 +16,16 @@ type Preferences struct { PrefData map[string]interface{} } +// --------------------- +// QUERIES + +type GetPreferencesQuery struct { + PrefId int64 + PrefType string + + Result PreferencesDTO +} + // --------------------- // COMMANDS @@ -24,3 +34,12 @@ type SavePreferencesCommand struct { PrefId int64 `json:"-"` PrefType string `json:"-"` } + +// ---------------------- +// DTO & Projections + +type PreferencesDTO struct { + PrefId int64 `json:"prefId"` + PrefType string `json:"prefType"` + PrefData map[string]interface{} `json:"prefData"` +} diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index 756048360ec..7d210749a7d 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -6,9 +6,31 @@ import ( ) func init() { + bus.AddHandler("sql", GetPreferences) bus.AddHandler("sql", SavePreferences) } +func GetPreferences(query *m.GetPreferencesQuery) error { + + sql := `SELECT * FROM preferences WHERE pref_id = ? ` + + `AND pref_type = ?` + + var prefResults = make([]m.Preferences, 0) + + resultsErr := x.Sql(sql, query.PrefId, query.PrefType).Find(&prefResults) + + if resultsErr != nil { + return resultsErr + } + query.Result = m.PreferencesDTO{ + PrefId: prefResults[0].PrefId, + PrefType: prefResults[0].PrefType, + PrefData: prefResults[0].PrefData, + } + + return nil +} + func SavePreferences(cmd *m.SavePreferencesCommand) error { return inTransaction2(func(sess *session) error { From 9c8d508247e1c3b048edd6b68248ab5859d5b653 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Sun, 6 Mar 2016 12:32:22 -0800 Subject: [PATCH 005/142] Made API handling better, removed unused components --- pkg/api/preferences.go | 13 ++++++-- pkg/models/preferences.go | 4 +-- pkg/services/sqlstore/preferences.go | 33 ++++++++++++------- .../app/core/components/sidemenu/sidemenu.ts | 1 - public/app/core/routes/routes.ts | 4 --- public/app/features/all.js | 1 - .../profile/partials/preferences.html | 32 ------------------ .../features/profile/partials/profile.html | 2 +- .../app/features/profile/preferencesCtrl.js | 25 -------------- 9 files changed, 35 insertions(+), 80 deletions(-) delete mode 100644 public/app/features/profile/partials/preferences.html delete mode 100644 public/app/features/profile/preferencesCtrl.js diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index 3a72467acb0..b39d0446a9b 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -20,13 +20,20 @@ func SaveUserPreferences(c *middleware.Context, cmd m.SavePreferencesCommand) Re } -func GetUserPreferences(c *middleware.Context) Response { +// GET /api/user/prefs +func GetUserPreferences(c *middleware.Context) { query := m.GetPreferencesQuery{PrefId: c.UserId, PrefType: `user`} if err := bus.Dispatch(&query); err != nil { - return ApiError(500, "Failed to get user", err) + c.JsonApiErr(500, "Failed to get preferences for user", err) } - return Json(200, query.Result) + dto := m.PreferencesDTO{ + PrefId: query.Result.PrefId, + PrefType: query.Result.PrefType, + PrefData: query.Result.PrefData, + } + + c.JSON(200, dto) } diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go index d54bbcda9bc..237e577dbc9 100644 --- a/pkg/models/preferences.go +++ b/pkg/models/preferences.go @@ -6,7 +6,7 @@ import ( // Typed errors var ( - ErrPreferenceNotFound = errors.New("Preference not found") + ErrPreferencesNotFound = errors.New("Preferences not found") ) type Preferences struct { @@ -23,7 +23,7 @@ type GetPreferencesQuery struct { PrefId int64 PrefType string - Result PreferencesDTO + Result *Preferences } // --------------------- diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index 7d210749a7d..b03463f8089 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -22,10 +22,11 @@ func GetPreferences(query *m.GetPreferencesQuery) error { if resultsErr != nil { return resultsErr } - query.Result = m.PreferencesDTO{ - PrefId: prefResults[0].PrefId, - PrefType: prefResults[0].PrefType, - PrefData: prefResults[0].PrefData, + + if len(prefResults) > 0 { + query.Result = &prefResults[0] + } else { + query.Result = new(m.Preferences) } return nil @@ -45,15 +46,25 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { return resultsErr } - var matchedPref m.Preferences - matchedPref = prefResults[0] - matchedPref.PrefData = cmd.PrefData - affectedRows, updateErr := sess.Id(matchedPref.Id).Update(&matchedPref) + var savePref m.Preferences + var affectedRows int64 + var saveErr error - if affectedRows == 0 { - return m.ErrPreferenceNotFound + if len(prefResults) == 0 { + savePref.PrefId = cmd.PrefId + savePref.PrefType = cmd.PrefType + savePref.PrefData = cmd.PrefData + affectedRows, saveErr = sess.Insert(&savePref) + } else { + savePref = prefResults[0] + savePref.PrefData = cmd.PrefData + affectedRows, saveErr = sess.Id(savePref.Id).Update(&savePref) } - return updateErr + if affectedRows == 0 { + return m.ErrPreferencesNotFound + } + + return saveErr }) } diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index f68edf8fca5..4c0e100f85c 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -39,7 +39,6 @@ export class SideMenuCtrl { this.orgMenu = [ {section: 'You', cssClass: 'dropdown-menu-title'}, {text: 'Profile', url: this.getUrl('/profile')}, - {text: 'Preferences', url: this.getUrl('/preferences')}, ]; if (this.isSignedIn) { diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 334800c22f5..6e3af683c82 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -89,10 +89,6 @@ function setupAngularRoutes($routeProvider, $locationProvider) { templateUrl: 'public/app/features/profile/partials/profile.html', controller : 'ProfileCtrl', }) - .when('/preferences', { - templateUrl: 'public/app/features/profile/partials/preferences.html', - controller : 'PreferencesCtrl', - }) .when('/profile/password', { templateUrl: 'public/app/features/profile/partials/password.html', controller : 'ChangePasswordCtrl', diff --git a/public/app/features/all.js b/public/app/features/all.js index 94da3809ae7..c110bcff7cd 100644 --- a/public/app/features/all.js +++ b/public/app/features/all.js @@ -10,6 +10,5 @@ define([ './profile/profileCtrl', './profile/changePasswordCtrl', './profile/selectOrgCtrl', - './profile/preferencesCtrl', './styleguide/styleguide', ], function () {}); diff --git a/public/app/features/profile/partials/preferences.html b/public/app/features/profile/partials/preferences.html deleted file mode 100644 index 39cf64957cb..00000000000 --- a/public/app/features/profile/partials/preferences.html +++ /dev/null @@ -1,32 +0,0 @@ - - - -
- - - -
- Home Dashboard - -
- -
- Time Range - -
- -
- Theme - -
- -
- - Cancel -
- - -
- diff --git a/public/app/features/profile/partials/profile.html b/public/app/features/profile/partials/profile.html index dd5afebeaac..042344eeac6 100644 --- a/public/app/features/profile/partials/profile.html +++ b/public/app/features/profile/partials/profile.html @@ -7,7 +7,7 @@
-

Information

+

Preferences

Name diff --git a/public/app/features/profile/preferencesCtrl.js b/public/app/features/profile/preferencesCtrl.js deleted file mode 100644 index f9d1e4da4f0..00000000000 --- a/public/app/features/profile/preferencesCtrl.js +++ /dev/null @@ -1,25 +0,0 @@ -define([ - 'angular', - 'app/core/config', -], -function (angular) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('PreferencesCtrl', function($scope, backendSrv, $location) { - - $scope.prefData = {}; - - $scope.setUserPreferences = function() { - if (!$scope.userForm.$valid) { return; } - - console.log($scope.command); - - backendSrv.put('/api/user/prefs', { prefData : $scope.prefData }).then(function() { - $location.path("profile"); - }); - }; - - }); -}); From 43b474143cf1c55264ef033299aa253a7c98e618 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Fri, 11 Mar 2016 06:30:05 -0800 Subject: [PATCH 006/142] Preferences model updated --- pkg/api/api.go | 3 +- pkg/api/preferences.go | 23 +++++++------- pkg/models/preferences.go | 30 +++++++++++-------- .../sqlstore/migrations/preferences_mig.go | 9 ++++-- pkg/services/sqlstore/preferences.go | 20 ++++++------- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index c02629ddd8f..bbf0fc522c9 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -96,7 +96,6 @@ func Register(r *macaron.Macaron) { r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard)) r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword)) r.Get("/quotas", wrap(GetUserQuotas)) - r.Combo("/prefs").Get(GetUserPreferences).Put(bind(m.SavePreferencesCommand{}), wrap(SaveUserPreferences)) }) // users (admin permission required) @@ -165,6 +164,8 @@ func Register(r *macaron.Macaron) { r.Delete("/:id", wrap(DeleteApiKey)) }, reqOrgAdmin) + r.Combo("/preferences").Get(GetPreferences).Put(bind(m.SavePreferencesCommand{}), wrap(SavePreferences)) + // Data sources r.Group("/datasources", func() { r.Get("/", GetDataSources) diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index b39d0446a9b..c95bcd46aa2 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -7,32 +7,33 @@ import ( ) // PUT /api/user/prefs -func SaveUserPreferences(c *middleware.Context, cmd m.SavePreferencesCommand) Response { +func SavePreferences(c *middleware.Context, cmd m.SavePreferencesCommand) Response { - cmd.PrefId = c.UserId - cmd.PrefType = `user` + cmd.UserId = c.UserId + cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to saved user preferences", err) + return ApiError(500, "Failed to saved preferences", err) } - return ApiSuccess("User preferences saved") + return ApiSuccess("Preferences saved") } // GET /api/user/prefs -func GetUserPreferences(c *middleware.Context) { +func GetPreferences(c *middleware.Context) { - query := m.GetPreferencesQuery{PrefId: c.UserId, PrefType: `user`} + query := m.GetPreferencesQuery{UserId: c.UserId, OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to get preferences for user", err) + c.JsonApiErr(500, "Failed to get preferences", err) } dto := m.PreferencesDTO{ - PrefId: query.Result.PrefId, - PrefType: query.Result.PrefType, - PrefData: query.Result.PrefData, + Id: query.Result.Id, + UserId: query.Result.UserId, + OrgId: query.Result.OrgId, + Preference: query.Result.Preference, } c.JSON(200, dto) diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go index 237e577dbc9..f2a26da4ba9 100644 --- a/pkg/models/preferences.go +++ b/pkg/models/preferences.go @@ -2,6 +2,7 @@ package models import ( "errors" + "time" ) // Typed errors @@ -10,18 +11,22 @@ var ( ) type Preferences struct { - Id int64 - PrefId int64 - PrefType string - PrefData map[string]interface{} + Id int64 + OrgId int64 + UserId int64 + Version int + Preference map[string]interface{} + Created time.Time + Updated time.Time } // --------------------- // QUERIES type GetPreferencesQuery struct { - PrefId int64 - PrefType string + Id int64 + OrgId int64 + UserId int64 Result *Preferences } @@ -30,16 +35,17 @@ type GetPreferencesQuery struct { // COMMANDS type SavePreferencesCommand struct { - PrefData map[string]interface{} `json:"prefData" binding:"Required"` - PrefId int64 `json:"-"` - PrefType string `json:"-"` + Preference map[string]interface{} `json:"Preference" binding:"Required"` + UserId int64 `json:"-"` + OrgId int64 `json:"-"` } // ---------------------- // DTO & Projections type PreferencesDTO struct { - PrefId int64 `json:"prefId"` - PrefType string `json:"prefType"` - PrefData map[string]interface{} `json:"prefData"` + Id int64 `json:"Id"` + UserId int64 `json:"UserId"` + OrgId int64 `json:"OrgId"` + Preference map[string]interface{} `json:"Preference"` } diff --git a/pkg/services/sqlstore/migrations/preferences_mig.go b/pkg/services/sqlstore/migrations/preferences_mig.go index 0ce01857b75..c14e47a8733 100644 --- a/pkg/services/sqlstore/migrations/preferences_mig.go +++ b/pkg/services/sqlstore/migrations/preferences_mig.go @@ -8,9 +8,12 @@ func addPreferencesMigrations(mg *Migrator) { Name: "preferences", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "pref_id", Type: DB_Int, Nullable: false}, - {Name: "pref_type", Type: DB_NVarchar, Length: 255, Nullable: false}, - {Name: "pref_data", Type: DB_Text, Nullable: false}, + {Name: "org_id", Type: DB_Int, Nullable: false}, + {Name: "user_id", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "version", Type: DB_Int, Nullable: false}, + {Name: "preference", Type: DB_Text, Nullable: false}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + {Name: "updated", Type: DB_DateTime, Nullable: false}, }, } diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index b03463f8089..9d395d95304 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -12,12 +12,12 @@ func init() { func GetPreferences(query *m.GetPreferencesQuery) error { - sql := `SELECT * FROM preferences WHERE pref_id = ? ` + - `AND pref_type = ?` + sql := `SELECT * FROM preferences WHERE user_id = ? ` + + `AND org_id = ?` var prefResults = make([]m.Preferences, 0) - resultsErr := x.Sql(sql, query.PrefId, query.PrefType).Find(&prefResults) + resultsErr := x.Sql(sql, query.UserId, query.OrgId).Find(&prefResults) if resultsErr != nil { return resultsErr @@ -35,12 +35,12 @@ func GetPreferences(query *m.GetPreferencesQuery) error { func SavePreferences(cmd *m.SavePreferencesCommand) error { return inTransaction2(func(sess *session) error { - sql := `SELECT * FROM preferences WHERE pref_id = ? ` + - `AND pref_type = ?` + sql := `SELECT * FROM preferences WHERE user_id = ? ` + + `AND org_id = ?` var prefResults = make([]m.Preferences, 0) - resultsErr := sess.Sql(sql, cmd.PrefId, cmd.PrefType).Find(&prefResults) + resultsErr := sess.Sql(sql, cmd.UserId, cmd.OrgId).Find(&prefResults) if resultsErr != nil { return resultsErr @@ -51,13 +51,13 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { var saveErr error if len(prefResults) == 0 { - savePref.PrefId = cmd.PrefId - savePref.PrefType = cmd.PrefType - savePref.PrefData = cmd.PrefData + savePref.UserId = cmd.UserId + savePref.OrgId = cmd.OrgId + savePref.Preference = cmd.Preference affectedRows, saveErr = sess.Insert(&savePref) } else { savePref = prefResults[0] - savePref.PrefData = cmd.PrefData + savePref.Preference = cmd.Preference affectedRows, saveErr = sess.Id(savePref.Id).Update(&savePref) } From 0bf721a74ce3cf102142889976fe7f6ff9bc9bda Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Sun, 13 Mar 2016 23:57:38 -0700 Subject: [PATCH 007/142] Able to save home dashboard --- .../features/dashboard/dashnav/dashnav.html | 1 + .../app/features/dashboard/dashnav/dashnav.ts | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index 19112f77a43..e8e1ddc9011 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -48,6 +48,7 @@
  • View JSON
  • Make Editable
  • Save As...
  • +
  • Save As Home
  • Delete dashboard
  • diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index dcc2f40d0dc..44ab8710144 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -7,7 +7,7 @@ import angular from 'angular'; export class DashNavCtrl { /** @ngInject */ - constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout) { + constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, contextSrv, $timeout) { $scope.init = function() { $scope.onAppEvent('save-dashboard', $scope.saveDashboard); @@ -103,6 +103,26 @@ export class DashNavCtrl { }, $scope.handleSaveDashError); }; + $scope.saveDashboardAsHome = function() { + var orgId = 'org-' + contextSrv.user.orgId; + backendSrv.get('/api/preferences').then(function(prefs) { + + // Checking if the preferences already exists or not + if (prefs.userId === 0 && prefs.orgId === 0 && prefs.preference === null) { + prefs.preference = {}; + } + if (prefs.preference == null) { + prefs.preference = { + home_dashboard_id: $scope.dashboard.id + }; + } else { + var orgPrefs = prefs.preference; + orgPrefs.home_dashboard = $scope.dashboard.id; + } + backendSrv.put('api/preferences', prefs); + }); + }; + $scope.handleSaveDashError = function(err) { if (err.data && err.data.status === "version-mismatch") { err.isHandled = true; From e371e03696acde9151f8594a740e9682e6fa90d5 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Mon, 14 Mar 2016 03:12:52 -0700 Subject: [PATCH 008/142] Able to save preference version, created, updated fields --- pkg/api/api.go | 2 +- pkg/api/preferences.go | 6 +++--- pkg/models/preferences.go | 12 ++++++------ .../sqlstore/migrations/preferences_mig.go | 6 +++--- pkg/services/sqlstore/preferences.go | 16 ++++++++++++++++ 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index bbf0fc522c9..d58f54b1783 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -164,7 +164,7 @@ func Register(r *macaron.Macaron) { r.Delete("/:id", wrap(DeleteApiKey)) }, reqOrgAdmin) - r.Combo("/preferences").Get(GetPreferences).Put(bind(m.SavePreferencesCommand{}), wrap(SavePreferences)) + r.Combo("/preferences").Get(GetPreferences).Put(bind(m.SavePreferencesCommand{}), wrap(SavePreferences)) // Data sources r.Group("/datasources", func() { diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index c95bcd46aa2..f7a8028fd6d 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -10,10 +10,10 @@ import ( func SavePreferences(c *middleware.Context, cmd m.SavePreferencesCommand) Response { cmd.UserId = c.UserId - cmd.OrgId = c.OrgId + cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to saved preferences", err) + return ApiError(500, "Failed to save preferences", err) } return ApiSuccess("Preferences saved") @@ -32,7 +32,7 @@ func GetPreferences(c *middleware.Context) { dto := m.PreferencesDTO{ Id: query.Result.Id, UserId: query.Result.UserId, - OrgId: query.Result.OrgId, + OrgId: query.Result.OrgId, Preference: query.Result.Preference, } diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go index f2a26da4ba9..3d6ad44a515 100644 --- a/pkg/models/preferences.go +++ b/pkg/models/preferences.go @@ -2,7 +2,7 @@ package models import ( "errors" - "time" + "time" ) // Typed errors @@ -14,10 +14,10 @@ type Preferences struct { Id int64 OrgId int64 UserId int64 - Version int + Version int Preference map[string]interface{} - Created time.Time - Updated time.Time + Created time.Time + Updated time.Time } // --------------------- @@ -26,7 +26,7 @@ type Preferences struct { type GetPreferencesQuery struct { Id int64 OrgId int64 - UserId int64 + UserId int64 Result *Preferences } @@ -46,6 +46,6 @@ type SavePreferencesCommand struct { type PreferencesDTO struct { Id int64 `json:"Id"` UserId int64 `json:"UserId"` - OrgId int64 `json:"OrgId"` + OrgId int64 `json:"OrgId"` Preference map[string]interface{} `json:"Preference"` } diff --git a/pkg/services/sqlstore/migrations/preferences_mig.go b/pkg/services/sqlstore/migrations/preferences_mig.go index c14e47a8733..46a91307a5d 100644 --- a/pkg/services/sqlstore/migrations/preferences_mig.go +++ b/pkg/services/sqlstore/migrations/preferences_mig.go @@ -10,10 +10,10 @@ func addPreferencesMigrations(mg *Migrator) { {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "org_id", Type: DB_Int, Nullable: false}, {Name: "user_id", Type: DB_NVarchar, Length: 255, Nullable: false}, - {Name: "version", Type: DB_Int, Nullable: false}, + {Name: "version", Type: DB_Int, Nullable: false}, {Name: "preference", Type: DB_Text, Nullable: false}, - {Name: "created", Type: DB_DateTime, Nullable: false}, - {Name: "updated", Type: DB_DateTime, Nullable: false}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + {Name: "updated", Type: DB_DateTime, Nullable: false}, }, } diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index 9d395d95304..ece113cf523 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -3,6 +3,7 @@ package sqlstore import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "time" ) func init() { @@ -54,10 +55,12 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { savePref.UserId = cmd.UserId savePref.OrgId = cmd.OrgId savePref.Preference = cmd.Preference + savePref = SetPreferencesModel(savePref, false) affectedRows, saveErr = sess.Insert(&savePref) } else { savePref = prefResults[0] savePref.Preference = cmd.Preference + savePref = SetPreferencesModel(savePref, true) affectedRows, saveErr = sess.Id(savePref.Id).Update(&savePref) } @@ -68,3 +71,16 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { return saveErr }) } + +func SetPreferencesModel(pref m.Preferences, updating bool) m.Preferences { + + if updating { + pref.Version = pref.Version + 1 + } else { + pref.Version = 0 + pref.Created = time.Now() + } + pref.Updated = time.Now() + + return pref +} From a88176e06086e1e1e670f0f7b52ae3c0ced758f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 15 Mar 2016 22:49:52 +0100 Subject: [PATCH 009/142] feat(preferences): lots of refactoring and changes to #3214 --- pkg/api/dtos/prefs.go | 7 ++ pkg/api/preferences.go | 10 +-- pkg/models/preferences.go | 34 ++++---- .../sqlstore/migrations/preferences_mig.go | 19 +++-- pkg/services/sqlstore/preferences.go | 77 ++++++------------- .../features/dashboard/dashnav/dashnav.html | 2 +- .../app/features/dashboard/dashnav/dashnav.ts | 19 +---- 7 files changed, 66 insertions(+), 102 deletions(-) create mode 100644 pkg/api/dtos/prefs.go diff --git a/pkg/api/dtos/prefs.go b/pkg/api/dtos/prefs.go new file mode 100644 index 00000000000..f66068edccb --- /dev/null +++ b/pkg/api/dtos/prefs.go @@ -0,0 +1,7 @@ +package dtos + +type Preferences struct { + Theme string `json:"theme"` + HomeDashboardId int64 `json:"homeDashboardId"` + Timezone string `json:"timezone"` +} diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index f7a8028fd6d..7738d1f987a 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -1,6 +1,7 @@ package api import ( + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -29,11 +30,10 @@ func GetPreferences(c *middleware.Context) { c.JsonApiErr(500, "Failed to get preferences", err) } - dto := m.PreferencesDTO{ - Id: query.Result.Id, - UserId: query.Result.UserId, - OrgId: query.Result.OrgId, - Preference: query.Result.Preference, + dto := dtos.Preferences{ + HomeDashboardId: query.Result.HomeDashboardId, + Timezone: query.Result.Timezone, + Theme: query.Result.Theme, } c.JSON(200, dto) diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go index 3d6ad44a515..5163835daf0 100644 --- a/pkg/models/preferences.go +++ b/pkg/models/preferences.go @@ -11,13 +11,15 @@ var ( ) type Preferences struct { - Id int64 - OrgId int64 - UserId int64 - Version int - Preference map[string]interface{} - Created time.Time - Updated time.Time + Id int64 + OrgId int64 + UserId int64 + Version int + HomeDashboardId int64 + Timezone string + Theme string + Created time.Time + Updated time.Time } // --------------------- @@ -33,19 +35,11 @@ type GetPreferencesQuery struct { // --------------------- // COMMANDS - type SavePreferencesCommand struct { - Preference map[string]interface{} `json:"Preference" binding:"Required"` - UserId int64 `json:"-"` - OrgId int64 `json:"-"` -} + UserId int64 + OrgId int64 -// ---------------------- -// DTO & Projections - -type PreferencesDTO struct { - Id int64 `json:"Id"` - UserId int64 `json:"UserId"` - OrgId int64 `json:"OrgId"` - Preference map[string]interface{} `json:"Preference"` + HomeDashboardId int64 + Timezone string + Theme string } diff --git a/pkg/services/sqlstore/migrations/preferences_mig.go b/pkg/services/sqlstore/migrations/preferences_mig.go index 46a91307a5d..6425c3735bb 100644 --- a/pkg/services/sqlstore/migrations/preferences_mig.go +++ b/pkg/services/sqlstore/migrations/preferences_mig.go @@ -4,20 +4,27 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" func addPreferencesMigrations(mg *Migrator) { - preferencesV1 := Table{ + mg.AddMigration("drop preferences table v2", NewDropTableMigration("preferences")) + + preferencesV2 := Table{ Name: "preferences", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "org_id", Type: DB_Int, Nullable: false}, - {Name: "user_id", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "org_id", Type: DB_Int, Nullable: true}, + {Name: "user_id", Type: DB_NVarchar, Length: 255, Nullable: true}, {Name: "version", Type: DB_Int, Nullable: false}, - {Name: "preference", Type: DB_Text, Nullable: false}, + {Name: "home_dashboard_id", Type: DB_BigInt, Nullable: true}, + {Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: true}, + {Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: true}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, }, + Indices: []*Index{ + {Cols: []string{"org_id"}}, + {Cols: []string{"user_id"}}, + }, } // create table - mg.AddMigration("create preferences table v1", NewAddTableMigration(preferencesV1)) - + mg.AddMigration("create preferences table v2", NewAddTableMigration(preferencesV2)) } diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index ece113cf523..dfe960d1f39 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -3,7 +3,6 @@ package sqlstore import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "time" ) func init() { @@ -13,19 +12,15 @@ func init() { func GetPreferences(query *m.GetPreferencesQuery) error { - sql := `SELECT * FROM preferences WHERE user_id = ? ` + - `AND org_id = ?` + var prefs m.Preferences + exists, err := x.Where("org_id=? AND user_id=?", query.OrgId, query.UserId).Get(&prefs) - var prefResults = make([]m.Preferences, 0) - - resultsErr := x.Sql(sql, query.UserId, query.OrgId).Find(&prefResults) - - if resultsErr != nil { - return resultsErr + if err != nil { + return err } - if len(prefResults) > 0 { - query.Result = &prefResults[0] + if exists { + query.Result = &prefs } else { query.Result = new(m.Preferences) } @@ -36,51 +31,25 @@ func GetPreferences(query *m.GetPreferencesQuery) error { func SavePreferences(cmd *m.SavePreferencesCommand) error { return inTransaction2(func(sess *session) error { - sql := `SELECT * FROM preferences WHERE user_id = ? ` + - `AND org_id = ?` + var prefs m.Preferences + exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs) - var prefResults = make([]m.Preferences, 0) - - resultsErr := sess.Sql(sql, cmd.UserId, cmd.OrgId).Find(&prefResults) - - if resultsErr != nil { - return resultsErr - } - - var savePref m.Preferences - var affectedRows int64 - var saveErr error - - if len(prefResults) == 0 { - savePref.UserId = cmd.UserId - savePref.OrgId = cmd.OrgId - savePref.Preference = cmd.Preference - savePref = SetPreferencesModel(savePref, false) - affectedRows, saveErr = sess.Insert(&savePref) + if !exists { + prefs = m.Preferences{ + UserId: cmd.UserId, + OrgId: cmd.OrgId, + HomeDashboardId: cmd.HomeDashboardId, + Timezone: cmd.Timezone, + Theme: cmd.Theme, + } + _, err = sess.Insert(&prefs) + return err } else { - savePref = prefResults[0] - savePref.Preference = cmd.Preference - savePref = SetPreferencesModel(savePref, true) - affectedRows, saveErr = sess.Id(savePref.Id).Update(&savePref) + prefs.HomeDashboardId = cmd.HomeDashboardId + prefs.Timezone = cmd.Timezone + prefs.Theme = cmd.Theme + _, err = sess.Id(prefs.Id).Update(&prefs) + return err } - - if affectedRows == 0 { - return m.ErrPreferencesNotFound - } - - return saveErr }) } - -func SetPreferencesModel(pref m.Preferences, updating bool) m.Preferences { - - if updating { - pref.Version = pref.Version + 1 - } else { - pref.Version = 0 - pref.Created = time.Now() - } - pref.Updated = time.Now() - - return pref -} diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index e8e1ddc9011..aec626bfe2a 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -48,7 +48,7 @@
  • View JSON
  • Make Editable
  • Save As...
  • -
  • Save As Home
  • +
  • Set As Home
  • Delete dashboard
  • diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 4960b20397a..c6faaf9a738 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -104,22 +104,9 @@ export class DashNavCtrl { }; $scope.saveDashboardAsHome = function() { - var orgId = 'org-' + contextSrv.user.orgId; - backendSrv.get('/api/preferences').then(function(prefs) { - - // Checking if the preferences already exists or not - if (prefs.userId === 0 && prefs.orgId === 0 && prefs.preference === null) { - prefs.preference = {}; - } - if (prefs.preference == null) { - prefs.preference = { - home_dashboard_id: $scope.dashboard.id - }; - } else { - var orgPrefs = prefs.preference; - orgPrefs.home_dashboard = $scope.dashboard.id; - } - backendSrv.put('api/preferences', prefs); + // TODO: this backend method needs to be implemented + backendSrv.post('/api/preferences/set-home-dash', { + dashboardId: $scope.dashboard.id }); }; From 749fd618a949ff5a49b1fe293710fffcf3213f03 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Wed, 16 Mar 2016 23:22:27 -0700 Subject: [PATCH 010/142] Saving home dashboard id in table --- pkg/api/api.go | 7 ++++++- pkg/api/preferences.go | 18 ++++++++++++++++-- pkg/models/preferences.go | 6 +++--- pkg/services/sqlstore/preferences.go | 5 +++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 45ef5e9030d..e360a43f78b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -160,7 +160,12 @@ func Register(r *macaron.Macaron) { r.Delete("/:id", wrap(DeleteApiKey)) }, reqOrgAdmin) - r.Combo("/preferences").Get(GetPreferences).Put(bind(m.SavePreferencesCommand{}), wrap(SavePreferences)) + // Preferences + r.Group("/preferences", func() { + r.Get("/", wrap(GetPreferences)) + r.Put("/", bind(m.SavePreferencesCommand{}), wrap(SavePreferences)) + r.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard)) + }) // Data sources r.Group("/datasources", func() { diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index 7738d1f987a..a3d398dd556 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -7,7 +7,7 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -// PUT /api/user/prefs +// PUT /api/preferences func SavePreferences(c *middleware.Context, cmd m.SavePreferencesCommand) Response { cmd.UserId = c.UserId @@ -21,7 +21,7 @@ func SavePreferences(c *middleware.Context, cmd m.SavePreferencesCommand) Respon } -// GET /api/user/prefs +// GET /api/preferences func GetPreferences(c *middleware.Context) { query := m.GetPreferencesQuery{UserId: c.UserId, OrgId: c.OrgId} @@ -38,3 +38,17 @@ func GetPreferences(c *middleware.Context) { c.JSON(200, dto) } + +// POST /api/preferences/set-home-dash +func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Response { + + cmd.UserId = c.UserId + cmd.OrgId = c.OrgId + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to set home dashboard", err) + } + + return ApiSuccess("Home dashboard set") + +} diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go index 5163835daf0..ac3dcf1481d 100644 --- a/pkg/models/preferences.go +++ b/pkg/models/preferences.go @@ -39,7 +39,7 @@ type SavePreferencesCommand struct { UserId int64 OrgId int64 - HomeDashboardId int64 - Timezone string - Theme string + HomeDashboardId int64 `json:"dashboardId"` + Timezone string `json:"timezone"` + Theme string `json:"theme"` } diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index dfe960d1f39..f744b2eb50f 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -1,6 +1,7 @@ package sqlstore import ( + "time" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) @@ -41,6 +42,8 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { HomeDashboardId: cmd.HomeDashboardId, Timezone: cmd.Timezone, Theme: cmd.Theme, + Created: time.Now(), + Updated: time.Now(), } _, err = sess.Insert(&prefs) return err @@ -48,6 +51,8 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { prefs.HomeDashboardId = cmd.HomeDashboardId prefs.Timezone = cmd.Timezone prefs.Theme = cmd.Theme + prefs.Updated = time.Now() + prefs.Version += 1 _, err = sess.Id(prefs.Id).Update(&prefs) return err } From 9dd6aefcec34a2fb8770e54c170be7c6f7917a60 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Wed, 16 Mar 2016 23:35:06 -0700 Subject: [PATCH 011/142] Minor corrections --- pkg/api/api.go | 12 ++++++------ pkg/api/preferences.go | 12 ++++++------ pkg/models/preferences.go | 6 +++--- pkg/services/sqlstore/preferences.go | 10 +++++----- public/app/features/dashboard/dashnav/dashnav.ts | 2 +- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index e360a43f78b..0abc4bd6026 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -160,12 +160,12 @@ func Register(r *macaron.Macaron) { r.Delete("/:id", wrap(DeleteApiKey)) }, reqOrgAdmin) - // Preferences - r.Group("/preferences", func() { - r.Get("/", wrap(GetPreferences)) - r.Put("/", bind(m.SavePreferencesCommand{}), wrap(SavePreferences)) - r.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard)) - }) + // Preferences + r.Group("/preferences", func() { + r.Get("/", wrap(GetPreferences)) + r.Put("/", bind(m.SavePreferencesCommand{}), wrap(SavePreferences)) + r.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard)) + }) // Data sources r.Group("/datasources", func() { diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index a3d398dd556..74419492d09 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -42,13 +42,13 @@ func GetPreferences(c *middleware.Context) { // POST /api/preferences/set-home-dash func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Response { - cmd.UserId = c.UserId - cmd.OrgId = c.OrgId + cmd.UserId = c.UserId + cmd.OrgId = c.OrgId - if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to set home dashboard", err) - } + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to set home dashboard", err) + } - return ApiSuccess("Home dashboard set") + return ApiSuccess("Home dashboard set") } diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go index ac3dcf1481d..523a3bfc83f 100644 --- a/pkg/models/preferences.go +++ b/pkg/models/preferences.go @@ -39,7 +39,7 @@ type SavePreferencesCommand struct { UserId int64 OrgId int64 - HomeDashboardId int64 `json:"dashboardId"` - Timezone string `json:"timezone"` - Theme string `json:"theme"` + HomeDashboardId int64 `json:"homeDashboardId"` + Timezone string `json:"timezone"` + Theme string `json:"theme"` } diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index f744b2eb50f..8882495329f 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -1,9 +1,9 @@ package sqlstore import ( - "time" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "time" ) func init() { @@ -42,8 +42,8 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { HomeDashboardId: cmd.HomeDashboardId, Timezone: cmd.Timezone, Theme: cmd.Theme, - Created: time.Now(), - Updated: time.Now(), + Created: time.Now(), + Updated: time.Now(), } _, err = sess.Insert(&prefs) return err @@ -51,8 +51,8 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { prefs.HomeDashboardId = cmd.HomeDashboardId prefs.Timezone = cmd.Timezone prefs.Theme = cmd.Theme - prefs.Updated = time.Now() - prefs.Version += 1 + prefs.Updated = time.Now() + prefs.Version += 1 _, err = sess.Id(prefs.Id).Update(&prefs) return err } diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index c6faaf9a738..14dfa8cc67a 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -106,7 +106,7 @@ export class DashNavCtrl { $scope.saveDashboardAsHome = function() { // TODO: this backend method needs to be implemented backendSrv.post('/api/preferences/set-home-dash', { - dashboardId: $scope.dashboard.id + homeDashboardId: $scope.dashboard.id }); }; From 66621d762e84c20e3835ba7386d9e5aeb5ff64d3 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Thu, 17 Mar 2016 01:01:58 -0700 Subject: [PATCH 012/142] Added getDashboardSlugById api --- pkg/api/api.go | 1 + pkg/api/dashboard.go | 14 ++++++++++++++ pkg/api/dtos/models.go | 4 ++++ pkg/models/dashboards.go | 5 +++++ pkg/services/sqlstore/dashboard.go | 15 +++++++++++++++ 5 files changed, 39 insertions(+) diff --git a/pkg/api/api.go b/pkg/api/api.go index 0abc4bd6026..a30c88eb58d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -200,6 +200,7 @@ func Register(r *macaron.Macaron) { r.Get("/home", GetHomeDashboard) r.Get("/tags", GetDashboardTags) r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) + r.Get("/id/:id", GetDashboardSlugById) }) // Dashboard snapshots diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 22f9e1e22a1..d6c0fa0a827 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -204,3 +204,17 @@ func GetDashboardTags(c *middleware.Context) { c.JSON(200, query.Result) } + +func GetDashboardSlugById(c *middleware.Context) { + dashId := c.ParamsInt64(":id") + query := m.GetDashboardSlugByIdQuery{Id: dashId} + err := bus.Dispatch(&query) + if err != nil { + c.JsonApiErr(500, "Failed to get slug from database", err) + return + } + + slug := dtos.DashboardSlug{Slug: query.Result} + + c.JSON(200, &slug) +} diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 26295dd3d3c..9fb48e059b4 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -57,6 +57,10 @@ type DashboardFullWithMeta struct { Dashboard *simplejson.Json `json:"dashboard"` } +type DashboardSlug struct { + Slug string `json:"slug"` +} + type DataSource struct { Id int64 `json:"id"` OrgId int64 `json:"orgId"` diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 6243c729624..6b19224f934 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -148,3 +148,8 @@ type GetDashboardsQuery struct { DashboardIds []int64 Result *[]Dashboard } + +type GetDashboardSlugByIdQuery struct { + Id int64 + Result string +} diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 396d507cfd2..2e8fd748154 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -18,6 +18,7 @@ func init() { bus.AddHandler("sql", DeleteDashboard) bus.AddHandler("sql", SearchDashboards) bus.AddHandler("sql", GetDashboardTags) + bus.AddHandler("sql", GetDashboardSlugById) } func SaveDashboard(cmd *m.SaveDashboardCommand) error { @@ -255,3 +256,17 @@ func GetDashboards(query *m.GetDashboardsQuery) error { return nil } + +func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error { + dashboard := m.Dashboard{Id: query.Id} + has, err := x.Get(&dashboard) + query.Result = dashboard.Slug + + if err != nil { + return err + } else if has == false { + return m.ErrDashboardNotFound + } + + return nil +} From 4fbe954a7996e65d2a96309fda846138489f1662 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Thu, 17 Mar 2016 01:38:18 -0700 Subject: [PATCH 013/142] Home dashboard per user per org works --- pkg/api/api.go | 2 +- public/app/core/routes/dashboard_loaders.js | 22 +++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index a30c88eb58d..d06e3a5fac4 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -162,7 +162,7 @@ func Register(r *macaron.Macaron) { // Preferences r.Group("/preferences", func() { - r.Get("/", wrap(GetPreferences)) + r.Get("/", GetPreferences) r.Put("/", bind(m.SavePreferencesCommand{}), wrap(SavePreferences)) r.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard)) }) diff --git a/public/app/core/routes/dashboard_loaders.js b/public/app/core/routes/dashboard_loaders.js index 8e2157ad84b..c24ed1ea7a2 100644 --- a/public/app/core/routes/dashboard_loaders.js +++ b/public/app/core/routes/dashboard_loaders.js @@ -7,11 +7,25 @@ function (coreModule) { coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv) { if (!$routeParams.slug) { - backendSrv.get('/api/dashboards/home').then(function(result) { - var meta = result.meta; - meta.canSave = meta.canShare = meta.canStar = false; - $scope.initDashboard(result, $scope); + + backendSrv.get('/api/preferences').then(function(preferences) { + if (preferences !== null && preferences.homeDashboardId !== 0) { + backendSrv.get('/api/dashboards/id/' + preferences.homeDashboardId).then(function(dashSlug) { + $routeParams.type = 'db'; + $routeParams.slug = dashSlug.slug; + dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { + $scope.initDashboard(result, $scope); + }); + }); + } else { + backendSrv.get('/api/dashboards/home').then(function(result) { + var meta = result.meta; + meta.canSave = meta.canShare = meta.canStar = false; + $scope.initDashboard(result, $scope); + }); + } }); + return; } From cb42cfc6af43b9ec9a0b2f0416d65fc78f8ce2ed Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Thu, 17 Mar 2016 02:29:34 -0700 Subject: [PATCH 014/142] Removed unwanted api, moved logic into backend --- pkg/api/api.go | 1 - pkg/api/dashboard.go | 36 +++++++++++++-------- public/app/core/routes/dashboard_loaders.js | 22 +++++-------- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index d06e3a5fac4..0f94e3f7f57 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -200,7 +200,6 @@ func Register(r *macaron.Macaron) { r.Get("/home", GetHomeDashboard) r.Get("/tags", GetDashboardTags) r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) - r.Get("/id/:id", GetDashboardSlugById) }) // Dashboard snapshots diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index d6c0fa0a827..1ea22984521 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -159,6 +159,28 @@ func canEditDashboard(role m.RoleType) bool { } func GetHomeDashboard(c *middleware.Context) { + + // Checking if there is any preference set for home dashboard + query := m.GetPreferencesQuery{UserId: c.UserId, OrgId: c.OrgId} + + if err := bus.Dispatch(&query); err != nil { + c.JsonApiErr(500, "Failed to get preferences", err) + } + + if query.Result.HomeDashboardId != 0 { + query := m.GetDashboardSlugByIdQuery{Id: query.Result.HomeDashboardId} + err := bus.Dispatch(&query) + if err != nil { + c.JsonApiErr(500, "Failed to get slug from database", err) + return + } + + slug := dtos.DashboardSlug{Slug: query.Result} + + c.JSON(200, &slug) + return + } + filePath := path.Join(setting.StaticRootPath, "dashboards/home.json") file, err := os.Open(filePath) if err != nil { @@ -204,17 +226,3 @@ func GetDashboardTags(c *middleware.Context) { c.JSON(200, query.Result) } - -func GetDashboardSlugById(c *middleware.Context) { - dashId := c.ParamsInt64(":id") - query := m.GetDashboardSlugByIdQuery{Id: dashId} - err := bus.Dispatch(&query) - if err != nil { - c.JsonApiErr(500, "Failed to get slug from database", err) - return - } - - slug := dtos.DashboardSlug{Slug: query.Result} - - c.JSON(200, &slug) -} diff --git a/public/app/core/routes/dashboard_loaders.js b/public/app/core/routes/dashboard_loaders.js index c24ed1ea7a2..9c276a7c3fd 100644 --- a/public/app/core/routes/dashboard_loaders.js +++ b/public/app/core/routes/dashboard_loaders.js @@ -7,25 +7,19 @@ function (coreModule) { coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv) { if (!$routeParams.slug) { - - backendSrv.get('/api/preferences').then(function(preferences) { - if (preferences !== null && preferences.homeDashboardId !== 0) { - backendSrv.get('/api/dashboards/id/' + preferences.homeDashboardId).then(function(dashSlug) { - $routeParams.type = 'db'; - $routeParams.slug = dashSlug.slug; - dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { - $scope.initDashboard(result, $scope); - }); - }); + backendSrv.get('/api/dashboards/home').then(function(result) { + if (result.slug == null) { + var meta = result.meta; + meta.canSave = meta.canShare = meta.canStar = false; + $scope.initDashboard(result, $scope); } else { - backendSrv.get('/api/dashboards/home').then(function(result) { - var meta = result.meta; - meta.canSave = meta.canShare = meta.canStar = false; + $routeParams.type = 'db'; + $routeParams.slug = result.slug; + dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { $scope.initDashboard(result, $scope); }); } }); - return; } From 7023dedc0523d1db83110663744f88e9f240279a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 20 Mar 2016 11:52:19 +0100 Subject: [PATCH 015/142] feat(preferences): refactoring PR #4399 --- pkg/api/dashboard.go | 5 ++--- pkg/api/dtos/models.go | 4 ++-- pkg/services/sqlstore/dashboard.go | 14 ++++++++++---- public/app/core/routes/dashboard_loaders.js | 18 +++++++----------- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 1ea22984521..1a45ffcd020 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -175,9 +175,8 @@ func GetHomeDashboard(c *middleware.Context) { return } - slug := dtos.DashboardSlug{Slug: query.Result} - - c.JSON(200, &slug) + dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + query.Result} + c.JSON(200, &dashRedirect) return } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index b9fd49a19c8..a95bd464f35 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -57,8 +57,8 @@ type DashboardFullWithMeta struct { Dashboard *simplejson.Json `json:"dashboard"` } -type DashboardSlug struct { - Slug string `json:"slug"` +type DashboardRedirect struct { + RedirectUri string `json:"redirectUri"` } type DataSource struct { diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 2e8fd748154..a64094cb65e 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -257,16 +257,22 @@ func GetDashboards(query *m.GetDashboardsQuery) error { return nil } +type DashboardSlugDTO struct { + Slug string +} + func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error { - dashboard := m.Dashboard{Id: query.Id} - has, err := x.Get(&dashboard) - query.Result = dashboard.Slug + var rawSql = `SELECT slug from dashboard WHERE Id=?` + var slug = DashboardSlugDTO{} + + exists, err := x.Sql(rawSql, query.Id).Get(&slug) if err != nil { return err - } else if has == false { + } else if exists == false { return m.ErrDashboardNotFound } + query.Result = slug.Slug return nil } diff --git a/public/app/core/routes/dashboard_loaders.js b/public/app/core/routes/dashboard_loaders.js index 9c276a7c3fd..61cdf32c128 100644 --- a/public/app/core/routes/dashboard_loaders.js +++ b/public/app/core/routes/dashboard_loaders.js @@ -4,20 +4,16 @@ define([ function (coreModule) { "use strict"; - coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv) { + coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) { if (!$routeParams.slug) { - backendSrv.get('/api/dashboards/home').then(function(result) { - if (result.slug == null) { - var meta = result.meta; - meta.canSave = meta.canShare = meta.canStar = false; - $scope.initDashboard(result, $scope); + backendSrv.get('/api/dashboards/home').then(function(homeDash) { + if (homeDash.redirectUri) { + $location.path('dashboard/' + homeDash.redirectUri); } else { - $routeParams.type = 'db'; - $routeParams.slug = result.slug; - dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { - $scope.initDashboard(result, $scope); - }); + var meta = homeDash.meta; + meta.canSave = meta.canShare = meta.canStar = false; + $scope.initDashboard(homeDash, $scope); } }); return; From 5e431149cd5f69c2ababcc77b64ab4615eb663b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 20 Mar 2016 11:54:24 +0100 Subject: [PATCH 016/142] feat(preferences): removed unused api prefernce methods, these need to be abstracted more from an http api perpsective, they are too low level for the http api, #4399 --- pkg/api/api.go | 2 -- pkg/api/preferences.go | 34 ---------------------------------- 2 files changed, 36 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index eb3f830b6c6..5241bc1abb7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -162,8 +162,6 @@ func Register(r *macaron.Macaron) { // Preferences r.Group("/preferences", func() { - r.Get("/", GetPreferences) - r.Put("/", bind(m.SavePreferencesCommand{}), wrap(SavePreferences)) r.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard)) }) diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index 74419492d09..490da451afc 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -1,44 +1,11 @@ package api import ( - "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" ) -// PUT /api/preferences -func SavePreferences(c *middleware.Context, cmd m.SavePreferencesCommand) Response { - - cmd.UserId = c.UserId - cmd.OrgId = c.OrgId - - if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to save preferences", err) - } - - return ApiSuccess("Preferences saved") - -} - -// GET /api/preferences -func GetPreferences(c *middleware.Context) { - - query := m.GetPreferencesQuery{UserId: c.UserId, OrgId: c.OrgId} - - if err := bus.Dispatch(&query); err != nil { - c.JsonApiErr(500, "Failed to get preferences", err) - } - - dto := dtos.Preferences{ - HomeDashboardId: query.Result.HomeDashboardId, - Timezone: query.Result.Timezone, - Theme: query.Result.Theme, - } - - c.JSON(200, dto) -} - // POST /api/preferences/set-home-dash func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Response { @@ -50,5 +17,4 @@ func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Respo } return ApiSuccess("Home dashboard set") - } From 5e52aaac6f0581b9d1b683bc1c0d6efcab48ea13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 21 Mar 2016 11:07:37 +0100 Subject: [PATCH 017/142] ux(): checkbox style test --- .../app/plugins/panel/graph/styleEditor.html | 21 +- public/sass/_grafana.scss | 1 + public/sass/components/_gf-form.scss | 12 +- public/sass/components/_switch.scss | 196 ++++++++++++++++++ 4 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 public/sass/components/_switch.scss diff --git a/public/app/plugins/panel/graph/styleEditor.html b/public/app/plugins/panel/graph/styleEditor.html index 465e8d4894e..efa39b3b3f3 100644 --- a/public/app/plugins/panel/graph/styleEditor.html +++ b/public/app/plugins/panel/graph/styleEditor.html @@ -3,15 +3,30 @@
    Draw Modes
    - +
    +
    + + +
    +
    - +
    +
    + + +
    +
    - +
    +
    + + +
    +
    diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 32643962ddc..c7b4567bfb5 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -35,6 +35,7 @@ @import "components/navs"; @import "components/tabs"; @import "components/alerts"; +@import "components/switch"; @import "components/tooltip"; @import "components/tags"; @import "components/panel_graph"; diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 3d93df79a29..95bbf6cb7ea 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -50,6 +50,12 @@ $gf-form-label-margin: 0.25rem; @include border-radius($label-border-radius-sm); } +.gf-form-switch-wrapper { + margin-right: $gf-form-label-margin; + padding: $input-padding-y $input-padding-x; + background-color: $input-bg; +} + .gf-form-checkbox { flex-shrink: 0; padding: $input-padding-y $input-padding-x; @@ -139,8 +145,8 @@ $gf-form-label-margin: 0.25rem; } } -.gf-form-select-wrapper + .gf-form-select-wrapper { - margin-left: $gf-form-label-margin; +.gf-form-select-wrapper + .gf-form-select-wrapper { + margin-left: $gf-form-label-margin; } .gf-form-btn { @@ -149,3 +155,5 @@ $gf-form-label-margin: 0.25rem; flex-shrink: 0; flex-grow: 0; } + + diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss new file mode 100644 index 00000000000..be02ade9d77 --- /dev/null +++ b/public/sass/components/_switch.scss @@ -0,0 +1,196 @@ +$switch-border-radius: 1rem; +$switch-width: 3.5rem; +$switch-height: 1.5rem; + +/* ============================================================ + COMMON +============================================================ */ +.cmn-toggle { + position: absolute; + margin-left: -9999px; + visibility: hidden; +} +.cmn-toggle + label { + display: block; + position: relative; + cursor: pointer; + outline: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* ============================================================ + SWITCH 1 - ROUND +============================================================ */ +input.cmn-toggle-round + label { + padding: 2px; + width: $switch-width; + height: $switch-height; + background-color: $dark-1; + border-radius: $switch-border-radius; +} + +input.cmn-toggle-round + label:before, input.cmn-toggle-round + label:after { + display: block; + position: absolute; + top: 1px; + left: 1px; + bottom: 1px; + content: ""; +} + +input.cmn-toggle-round + label:before { + right: 1px; + background-color: $card-background; + border-radius: $switch-border-radius; + box-shadow: $card-shadow; + transition: background 0.4s; +} + +input.cmn-toggle-round + label:after { + width: $switch-height; + background-color: $dark-5; + border-radius: 100%; + box-shadow: $card-shadow; + transition: margin 0.4s; +} + +input.cmn-toggle-round:checked + label:before { + background-color: $blue; +} + +input.cmn-toggle-round:checked + label:after { + margin-left: $switch-width - $switch-height; +} + +/* ============================================================ + SWITCH 2 - ROUND FLAT +============================================================ */ +input.cmn-toggle-round-flat + label { + padding: 2px; + width: 120px; + height: 60px; + background-color: #dddddd; + -webkit-border-radius: 60px; + -moz-border-radius: 60px; + -ms-border-radius: 60px; + -o-border-radius: 60px; + border-radius: 60px; + -webkit-transition: background 0.4s; + -moz-transition: background 0.4s; + -o-transition: background 0.4s; + transition: background 0.4s; +} +input.cmn-toggle-round-flat + label:before, input.cmn-toggle-round-flat + label:after { + display: block; + position: absolute; + content: ""; +} +input.cmn-toggle-round-flat + label:before { + top: 2px; + left: 2px; + bottom: 2px; + right: 2px; + background-color: #fff; + -webkit-border-radius: 60px; + -moz-border-radius: 60px; + -ms-border-radius: 60px; + -o-border-radius: 60px; + border-radius: 60px; + -webkit-transition: background 0.4s; + -moz-transition: background 0.4s; + -o-transition: background 0.4s; + transition: background 0.4s; +} +input.cmn-toggle-round-flat + label:after { + top: 4px; + left: 4px; + bottom: 4px; + width: 52px; + background-color: #dddddd; + -webkit-border-radius: 52px; + -moz-border-radius: 52px; + -ms-border-radius: 52px; + -o-border-radius: 52px; + border-radius: 52px; + -webkit-transition: margin 0.4s, background 0.4s; + -moz-transition: margin 0.4s, background 0.4s; + -o-transition: margin 0.4s, background 0.4s; + transition: margin 0.4s, background 0.4s; +} +input.cmn-toggle-round-flat:checked + label { + background-color: #8ce196; +} +input.cmn-toggle-round-flat:checked + label:after { + margin-left: 60px; + background-color: #8ce196; +} + +/* ============================================================ + SWITCH 3 - YES NO +============================================================ */ +input.cmn-toggle-yes-no + label { + padding: 2px; + width: 120px; + height: 60px; +} +input.cmn-toggle-yes-no + label:before, input.cmn-toggle-yes-no + label:after { + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + color: #fff; + font-family: "Roboto Slab", serif; + font-size: 20px; + text-align: center; + line-height: 60px; +} +input.cmn-toggle-yes-no + label:before { + background-color: #dddddd; + content: attr(data-off); + -webkit-transition: -webkit-transform 0.5s; + -moz-transition: -moz-transform 0.5s; + -o-transition: -o-transform 0.5s; + transition: transform 0.5s; + -webkit-backface-visibility: hidden; + -moz-backface-visibility: hidden; + -ms-backface-visibility: hidden; + -o-backface-visibility: hidden; + backface-visibility: hidden; +} +input.cmn-toggle-yes-no + label:after { + background-color: #8ce196; + content: attr(data-on); + -webkit-transition: -webkit-transform 0.5s; + -moz-transition: -moz-transform 0.5s; + -o-transition: -o-transform 0.5s; + transition: transform 0.5s; + -webkit-transform: rotateY(180deg); + -moz-transform: rotateY(180deg); + -ms-transform: rotateY(180deg); + -o-transform: rotateY(180deg); + transform: rotateY(180deg); + -webkit-backface-visibility: hidden; + -moz-backface-visibility: hidden; + -ms-backface-visibility: hidden; + -o-backface-visibility: hidden; + backface-visibility: hidden; +} +input.cmn-toggle-yes-no:checked + label:before { + -webkit-transform: rotateY(180deg); + -moz-transform: rotateY(180deg); + -ms-transform: rotateY(180deg); + -o-transform: rotateY(180deg); + transform: rotateY(180deg); +} +input.cmn-toggle-yes-no:checked + label:after { + -webkit-transform: rotateY(0); + -moz-transform: rotateY(0); + -ms-transform: rotateY(0); + -o-transform: rotateY(0); + transform: rotateY(0); +} From 530b6a5088c5244827d942428b5857756084a500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 21 Mar 2016 11:27:53 +0100 Subject: [PATCH 018/142] ux(): another checkbox design --- .../app/plugins/panel/graph/styleEditor.html | 14 ++- public/sass/base/_forms.scss | 2 +- public/sass/components/_gf-form.scss | 2 - public/sass/components/_switch.scss | 111 +++--------------- 4 files changed, 28 insertions(+), 101 deletions(-) diff --git a/public/app/plugins/panel/graph/styleEditor.html b/public/app/plugins/panel/graph/styleEditor.html index efa39b3b3f3..e10071fc0e7 100644 --- a/public/app/plugins/panel/graph/styleEditor.html +++ b/public/app/plugins/panel/graph/styleEditor.html @@ -80,11 +80,21 @@
    Multiple Series
    - +
    +
    + + +
    +
    - +
    +
    + + +
    +
    diff --git a/public/sass/base/_forms.scss b/public/sass/base/_forms.scss index 4f1d0778e0b..3891adc4085 100644 --- a/public/sass/base/_forms.scss +++ b/public/sass/base/_forms.scss @@ -174,7 +174,7 @@ label.cr1 { cursor:pointer; } -input[type="checkbox"]:checked+label { +input[type="checkbox"].cr1:checked+label { background: url($checkboxImageUrl) 0px -18px no-repeat; } diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 95bbf6cb7ea..cd500052d9a 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -52,8 +52,6 @@ $gf-form-label-margin: 0.25rem; .gf-form-switch-wrapper { margin-right: $gf-form-label-margin; - padding: $input-padding-y $input-padding-x; - background-color: $input-bg; } .gf-form-checkbox { diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index be02ade9d77..01b2f535b48 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -65,77 +65,17 @@ input.cmn-toggle-round:checked + label:after { margin-left: $switch-width - $switch-height; } -/* ============================================================ - SWITCH 2 - ROUND FLAT -============================================================ */ -input.cmn-toggle-round-flat + label { - padding: 2px; - width: 120px; - height: 60px; - background-color: #dddddd; - -webkit-border-radius: 60px; - -moz-border-radius: 60px; - -ms-border-radius: 60px; - -o-border-radius: 60px; - border-radius: 60px; - -webkit-transition: background 0.4s; - -moz-transition: background 0.4s; - -o-transition: background 0.4s; - transition: background 0.4s; -} -input.cmn-toggle-round-flat + label:before, input.cmn-toggle-round-flat + label:after { - display: block; - position: absolute; - content: ""; -} -input.cmn-toggle-round-flat + label:before { - top: 2px; - left: 2px; - bottom: 2px; - right: 2px; - background-color: #fff; - -webkit-border-radius: 60px; - -moz-border-radius: 60px; - -ms-border-radius: 60px; - -o-border-radius: 60px; - border-radius: 60px; - -webkit-transition: background 0.4s; - -moz-transition: background 0.4s; - -o-transition: background 0.4s; - transition: background 0.4s; -} -input.cmn-toggle-round-flat + label:after { - top: 4px; - left: 4px; - bottom: 4px; - width: 52px; - background-color: #dddddd; - -webkit-border-radius: 52px; - -moz-border-radius: 52px; - -ms-border-radius: 52px; - -o-border-radius: 52px; - border-radius: 52px; - -webkit-transition: margin 0.4s, background 0.4s; - -moz-transition: margin 0.4s, background 0.4s; - -o-transition: margin 0.4s, background 0.4s; - transition: margin 0.4s, background 0.4s; -} -input.cmn-toggle-round-flat:checked + label { - background-color: #8ce196; -} -input.cmn-toggle-round-flat:checked + label:after { - margin-left: 60px; - background-color: #8ce196; -} - /* ============================================================ SWITCH 3 - YES NO ============================================================ */ input.cmn-toggle-yes-no + label { padding: 2px; - width: 120px; - height: 60px; + width: 5rem; + height: 2.6rem; + padding: $input-padding-y $input-padding-x; + background-color: $input-bg; } + input.cmn-toggle-yes-no + label:before, input.cmn-toggle-yes-no + label:after { display: block; position: absolute; @@ -144,53 +84,32 @@ input.cmn-toggle-yes-no + label:before, input.cmn-toggle-yes-no + label:after { bottom: 0; right: 0; color: #fff; - font-family: "Roboto Slab", serif; - font-size: 20px; + font-family: "Open Sans"; + font-size: $font-size-sm; text-align: center; - line-height: 60px; + line-height: 2.6rem; } + input.cmn-toggle-yes-no + label:before { - background-color: #dddddd; + @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl); content: attr(data-off); - -webkit-transition: -webkit-transform 0.5s; - -moz-transition: -moz-transform 0.5s; - -o-transition: -o-transform 0.5s; transition: transform 0.5s; - -webkit-backface-visibility: hidden; - -moz-backface-visibility: hidden; - -ms-backface-visibility: hidden; - -o-backface-visibility: hidden; backface-visibility: hidden; } + input.cmn-toggle-yes-no + label:after { - background-color: #8ce196; + //@include buttonBackground($btn-success-bg, $btn-success-bg-hl); + @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); content: attr(data-on); - -webkit-transition: -webkit-transform 0.5s; - -moz-transition: -moz-transform 0.5s; - -o-transition: -o-transform 0.5s; transition: transform 0.5s; - -webkit-transform: rotateY(180deg); - -moz-transform: rotateY(180deg); - -ms-transform: rotateY(180deg); - -o-transform: rotateY(180deg); transform: rotateY(180deg); - -webkit-backface-visibility: hidden; - -moz-backface-visibility: hidden; - -ms-backface-visibility: hidden; - -o-backface-visibility: hidden; backface-visibility: hidden; } + input.cmn-toggle-yes-no:checked + label:before { - -webkit-transform: rotateY(180deg); - -moz-transform: rotateY(180deg); - -ms-transform: rotateY(180deg); - -o-transform: rotateY(180deg); transform: rotateY(180deg); } + input.cmn-toggle-yes-no:checked + label:after { - -webkit-transform: rotateY(0); - -moz-transform: rotateY(0); - -ms-transform: rotateY(0); - -o-transform: rotateY(0); transform: rotateY(0); } From ae255a7adf04509494150fdf0050d1e08a94768f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 21 Mar 2016 13:37:56 +0100 Subject: [PATCH 019/142] ux(): new checkbox tweaks --- .../app/plugins/panel/graph/styleEditor.html | 238 +++++++++--------- public/sass/_variables.dark.scss | 2 +- public/sass/components/_switch.scss | 155 ++++-------- 3 files changed, 168 insertions(+), 227 deletions(-) diff --git a/public/app/plugins/panel/graph/styleEditor.html b/public/app/plugins/panel/graph/styleEditor.html index e10071fc0e7..395d393c21a 100644 --- a/public/app/plugins/panel/graph/styleEditor.html +++ b/public/app/plugins/panel/graph/styleEditor.html @@ -3,146 +3,136 @@
    Draw Modes
    -
    -
    - - -
    +
    + +
    -
    -
    - - -
    +
    + +
    -
    -
    - -
    -
    - - -
    +
    +
    + +
    + +
    -
    -
    -
    -
    Mode Options
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - - -
    -
    -
    -
    Misc options
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    +
    +
    +
    +
    Mode Options
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + + +
    +
    +
    +
    Misc options
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    -
    -
    Multiple Series
    -
    - -
    -
    - - -
    +
    +
    Multiple Series
    +
    + +
    + +
    -
    -
    - -
    -
    - - -
    +
    +
    + +
    + +
    -
    -
    - -
    - -
    -
    -
    +
    +
    + +
    + +
    +
    +
    -
    -
    Series specific overrides Regex match example: /server[0-3]/i
    -
    -
    -
      -
    • - -
    • +
      +
      Series specific overrides Regex match example: /server[0-3]/i
      +
      +
      +
        +
      • + +
      • -
      • - alias or regex -
      • +
      • + alias or regex +
      • -
      • - -
      • +
      • + +
      • -
      • - - - Color: - - - {{option.name}}: {{option.value}} - -
      • +
      • + + + Color: + + + {{option.name}}: {{option.value}} + +
      • - -
      -
      -
      -
      + +
    +
    +
    +
    - -
    + +
    diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 84b01692069..b727c59fd33 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -124,7 +124,7 @@ $btn-primary-bg: $brand-primary; $btn-primary-bg-hl: lighten($brand-primary, 8%); $btn-secondary-bg: $blue-dark; -$btn-secondary-bg-hl: lighten($blue-dark, 3%); +$btn-secondary-bg-hl: lighten($blue-dark, 5%); $btn-success-bg: lighten($green, 3%); $btn-success-bg-hl: darken($green, 3%); diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index 01b2f535b48..b65639a5cc1 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -2,114 +2,65 @@ $switch-border-radius: 1rem; $switch-width: 3.5rem; $switch-height: 1.5rem; -/* ============================================================ - COMMON -============================================================ */ -.cmn-toggle { - position: absolute; - margin-left: -9999px; - visibility: hidden; -} -.cmn-toggle + label { - display: block; - position: relative; - cursor: pointer; - outline: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -/* ============================================================ - SWITCH 1 - ROUND -============================================================ */ -input.cmn-toggle-round + label { - padding: 2px; - width: $switch-width; - height: $switch-height; - background-color: $dark-1; - border-radius: $switch-border-radius; -} - -input.cmn-toggle-round + label:before, input.cmn-toggle-round + label:after { - display: block; - position: absolute; - top: 1px; - left: 1px; - bottom: 1px; - content: ""; -} - -input.cmn-toggle-round + label:before { - right: 1px; - background-color: $card-background; - border-radius: $switch-border-radius; - box-shadow: $card-shadow; - transition: background 0.4s; -} - -input.cmn-toggle-round + label:after { - width: $switch-height; - background-color: $dark-5; - border-radius: 100%; - box-shadow: $card-shadow; - transition: margin 0.4s; -} - -input.cmn-toggle-round:checked + label:before { - background-color: $blue; -} - -input.cmn-toggle-round:checked + label:after { - margin-left: $switch-width - $switch-height; -} - /* ============================================================ SWITCH 3 - YES NO ============================================================ */ -input.cmn-toggle-yes-no + label { - padding: 2px; - width: 5rem; - height: 2.6rem; - padding: $input-padding-y $input-padding-x; - background-color: $input-bg; -} -input.cmn-toggle-yes-no + label:before, input.cmn-toggle-yes-no + label:after { - display: block; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - color: #fff; - font-family: "Open Sans"; - font-size: $font-size-sm; - text-align: center; - line-height: 2.6rem; -} +.gf-form-switch { + position: relative; -input.cmn-toggle-yes-no + label:before { - @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl); - content: attr(data-off); - transition: transform 0.5s; - backface-visibility: hidden; -} + input { + position: absolute; + margin-left: -9999px; + visibility: hidden; + display: none; + } -input.cmn-toggle-yes-no + label:after { - //@include buttonBackground($btn-success-bg, $btn-success-bg-hl); - @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); - content: attr(data-on); - transition: transform 0.5s; - transform: rotateY(180deg); - backface-visibility: hidden; -} + input + label { + display: block; + position: relative; + cursor: pointer; + outline: none; + user-select: none; + width: 5rem; + height: 2.65rem; + background-color: $page-bg; + } -input.cmn-toggle-yes-no:checked + label:before { - transform: rotateY(180deg); -} + input + label:before, input + label:after { + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + color: #fff; + font-family: "Open Sans"; + font-size: $font-size-sm; + text-align: center; + line-height: 2.6rem; + } -input.cmn-toggle-yes-no:checked + label:after { - transform: rotateY(0); + input + label:before { + @include buttonBackground($input-bg, lighten($input-bg, 5%)); + content: attr(data-off); + transition: transform 0.5s; + backface-visibility: hidden; + } + + input + label:after { + @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); + content: attr(data-on); + transition: transform 0.5s; + transform: rotateY(180deg); + backface-visibility: hidden; + } + + input:checked + label:before { + transform: rotateY(180deg); + } + + input:checked + label:after { + transform: rotateY(0); + } } From fc983f97514bc451b976157c1107b375a3a36338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 21 Mar 2016 14:08:18 +0100 Subject: [PATCH 020/142] ux(): checkbox v2 tweaks --- public/app/features/panel/partials/panelTime.html | 10 +++++++--- public/app/plugins/panel/graph/styleEditor.html | 15 +++++++++------ public/sass/components/_switch.scss | 6 ++++-- public/sass/utils/_widths.scss | 4 ++-- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/public/app/features/panel/partials/panelTime.html b/public/app/features/panel/partials/panelTime.html index 7fd398a9282..77a554faa86 100644 --- a/public/app/features/panel/partials/panelTime.html +++ b/public/app/features/panel/partials/panelTime.html @@ -9,7 +9,7 @@ + ng-change="ctrl.refresh()" ng-model-onblur>
    @@ -20,14 +20,18 @@ Amount + ng-change="ctrl.refresh()" ng-model-onblur>
    - + +
    + + +
    diff --git a/public/app/plugins/panel/graph/styleEditor.html b/public/app/plugins/panel/graph/styleEditor.html index 395d393c21a..f1079341f6d 100644 --- a/public/app/plugins/panel/graph/styleEditor.html +++ b/public/app/plugins/panel/graph/styleEditor.html @@ -2,14 +2,14 @@
    Draw Modes
    - +
    - +
    @@ -27,25 +27,28 @@
    Mode Options
    -
    +
    -
    +
    -
    +
    - +
    + + +
    diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index b65639a5cc1..74184a4fcde 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -8,6 +8,9 @@ $switch-height: 1.5rem; .gf-form-switch { position: relative; + max-width: 4.5rem; + flex-grow: 1; + min-width: 4.0rem; input { position: absolute; @@ -22,7 +25,7 @@ $switch-height: 1.5rem; cursor: pointer; outline: none; user-select: none; - width: 5rem; + width: 100%; height: 2.65rem; background-color: $page-bg; } @@ -35,7 +38,6 @@ $switch-height: 1.5rem; bottom: 0; right: 0; color: #fff; - font-family: "Open Sans"; font-size: $font-size-sm; text-align: center; line-height: 2.6rem; diff --git a/public/sass/utils/_widths.scss b/public/sass/utils/_widths.scss index bd6fc78ef90..b4bdbdb6ccc 100644 --- a/public/sass/utils/_widths.scss +++ b/public/sass/utils/_widths.scss @@ -6,13 +6,13 @@ // widths @for $i from 1 through 30 { .width-#{$i} { - width: ($spacer * $i) - $gf-form-margin; + width: ($spacer * $i) - $gf-form-margin !important; } } @for $i from 1 through 30 { .max-width-#{$i} { - max-width: ($spacer * $i) - $gf-form-margin; + max-width: ($spacer * $i) - $gf-form-margin !important; flex-grow: 1; } } From 98206ab49f704c66b00aeb3f7a186ec4a2677ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 21 Mar 2016 21:24:06 +0100 Subject: [PATCH 021/142] ux(): created new gf-form-switch directive clean/refactoring --- public/app/core/components/switch.ts | 47 +++++++++++++ public/app/core/core.ts | 2 + .../features/panel/partials/panelTime.html | 17 ++--- .../app/plugins/panel/graph/styleEditor.html | 66 +++++++------------ public/sass/components/_gf-form.scss | 12 ++-- 5 files changed, 86 insertions(+), 58 deletions(-) create mode 100644 public/app/core/components/switch.ts diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts new file mode 100644 index 00000000000..72e54857e26 --- /dev/null +++ b/public/app/core/components/switch.ts @@ -0,0 +1,47 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; + +var template = ` + +
    + + +
    +`; + +export class SwitchCtrl { + onChange: any; + + internalOnChange() { + return new Promise(resolve => { + setTimeout(() => { + this.onChange(); + resolve(); + }); + }); + } + +} + +export function switchDirective() { + return { + restrict: 'E', + controller: SwitchCtrl, + controllerAs: 'ctrl', + bindToController: true, + scope: { + checked: "=", + label: "@", + labelClass: "@", + switchClass: "@", + onChange: "&", + }, + template: template, + }; +} + +coreModule.directive('gfFormSwitch', switchDirective); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 2ab7ea01410..b0922af3f39 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -29,6 +29,7 @@ import {colorPicker} from './components/colorpicker'; import {navbarDirective} from './components/navbar/navbar'; import {arrayJoin} from './directives/array_join'; import {layoutSelector} from './components/layout_selector/layout_selector'; +import {switchDirective} from './components/switch'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -44,5 +45,6 @@ export { searchDirective, colorPicker, layoutSelector, + switchDirective, infoPopover }; diff --git a/public/app/features/panel/partials/panelTime.html b/public/app/features/panel/partials/panelTime.html index 77a554faa86..4047558317f 100644 --- a/public/app/features/panel/partials/panelTime.html +++ b/public/app/features/panel/partials/panelTime.html @@ -23,15 +23,16 @@ ng-change="ctrl.refresh()" ng-model-onblur>
    -
    - - - - -
    - - +
    +
    + + +
    + +
    diff --git a/public/app/plugins/panel/graph/styleEditor.html b/public/app/plugins/panel/graph/styleEditor.html index f1079341f6d..17a9d702329 100644 --- a/public/app/plugins/panel/graph/styleEditor.html +++ b/public/app/plugins/panel/graph/styleEditor.html @@ -1,27 +1,18 @@
    Draw Modes
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    + + + + + +
    Mode Options
    @@ -43,13 +34,10 @@
    -
    - -
    - - -
    -
    + +
    Misc options
    @@ -75,20 +63,14 @@
    Multiple Series
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    + + + +
    diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index cd500052d9a..cecbf6e5996 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -1,9 +1,7 @@ $gf-form-margin: 0.25rem; -$gf-form-label-margin: 0.25rem; .gf-form { margin-bottom: $gf-form-margin; - margin-right: $gf-form-margin; display: flex; flex-direction: row; align-items: center; @@ -38,20 +36,20 @@ $gf-form-label-margin: 0.25rem; .gf-form-label { padding: $input-padding-y $input-padding-x; + margin-right: $gf-form-margin; line-height: $input-line-height; flex-shrink: 0; background-color: $input-label-bg; display: block; font-size: $font-size-sm; - margin-right: $gf-form-label-margin; border: $input-btn-border-width solid transparent; @include border-radius($label-border-radius-sm); } .gf-form-switch-wrapper { - margin-right: $gf-form-label-margin; + margin-right: $gf-form-margin; } .gf-form-checkbox { @@ -71,6 +69,7 @@ $gf-form-label-margin: 0.25rem; display: block; width: 100%; padding: $input-padding-y $input-padding-x; + margin-right: $gf-form-margin; font-size: $font-size-base; line-height: $input-line-height; color: $input-color; @@ -113,6 +112,7 @@ $gf-form-label-margin: 0.25rem; } .gf-form-select-wrapper { + margin-right: $gf-form-margin; position: relative; background-color: $input-bg; @@ -143,10 +143,6 @@ $gf-form-label-margin: 0.25rem; } } -.gf-form-select-wrapper + .gf-form-select-wrapper { - margin-left: $gf-form-label-margin; -} - .gf-form-btn { padding: $input-padding-y $input-padding-x; line-height: $input-line-height; From 64312ceded9342850fd3533f0743a1f818277b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 21 Mar 2016 21:36:30 +0100 Subject: [PATCH 022/142] ux(): updated http settings view with new checkboxes --- .../features/dashboard/partials/settings.html | 12 +++--------- .../plugins/partials/ds_http_settings.html | 18 ++++++++++++------ public/sass/components/_gf-form.scss | 7 +++---- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index deccd22305b..8cf19141e38 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -41,15 +41,9 @@
    On/Off Toggles
    -
    - -
    -
    - -
    -
    - -
    + + +
    diff --git a/public/app/features/plugins/partials/ds_http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html index 65c205714bc..c49de6967c9 100644 --- a/public/app/features/plugins/partials/ds_http_settings.html +++ b/public/app/features/plugins/partials/ds_http_settings.html @@ -29,12 +29,18 @@
    -
    - - Http Auth - - - Tip for with credentials +
    +
    + +
    + + + +
    diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index cecbf6e5996..f92e712eabf 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -48,10 +48,6 @@ $gf-form-margin: 0.25rem; @include border-radius($label-border-radius-sm); } -.gf-form-switch-wrapper { - margin-right: $gf-form-margin; -} - .gf-form-checkbox { flex-shrink: 0; padding: $input-padding-y $input-padding-x; @@ -150,4 +146,7 @@ $gf-form-margin: 0.25rem; flex-grow: 0; } +.gf-form-switch { + margin-right: $gf-form-margin; +} From 2bf94d8db4d269e0f5af423ffb8c972162c8bbe0 Mon Sep 17 00:00:00 2001 From: David Warden Date: Thu, 17 Mar 2016 10:45:39 -0400 Subject: [PATCH 023/142] version bump go-ldap to v2.2.1 --- Godeps/Godeps.json | 4 +- .../src/github.com/go-ldap/ldap/.travis.yml | 5 +- .../src/github.com/go-ldap/ldap/README.md | 41 +-- .../src/github.com/go-ldap/ldap/add.go | 104 +++++++ .../src/github.com/go-ldap/ldap/client.go | 23 ++ .../src/github.com/go-ldap/ldap/conn.go | 9 +- .../src/github.com/go-ldap/ldap/control.go | 32 +++ .../src/github.com/go-ldap/ldap/del.go | 79 +++++ .../src/github.com/go-ldap/ldap/dn.go | 12 +- .../src/github.com/go-ldap/ldap/dn_test.go | 66 ++--- .../src/github.com/go-ldap/ldap/error.go | 137 +++++++++ .../github.com/go-ldap/ldap/example_test.go | 4 +- .../src/github.com/go-ldap/ldap/filter.go | 272 +++++++++++++++--- .../github.com/go-ldap/ldap/filter_test.go | 222 ++++++++++++-- .../src/github.com/go-ldap/ldap/ldap.go | 121 +------- .../src/github.com/go-ldap/ldap/ldap_test.go | 78 +++-- .../src/github.com/go-ldap/ldap/search.go | 61 +++- .../github.com/go-ldap/ldap/search_test.go | 31 ++ 18 files changed, 1028 insertions(+), 273 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/go-ldap/ldap/add.go create mode 100644 Godeps/_workspace/src/github.com/go-ldap/ldap/client.go create mode 100644 Godeps/_workspace/src/github.com/go-ldap/ldap/del.go create mode 100644 Godeps/_workspace/src/github.com/go-ldap/ldap/error.go create mode 100644 Godeps/_workspace/src/github.com/go-ldap/ldap/search_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index b5cb1a460df..c054540ecf4 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -159,8 +159,8 @@ }, { "ImportPath": "github.com/go-ldap/ldap", - "Comment": "v1-19-g83e6542", - "Rev": "83e65426fd1c06626e88aa8a085e5bfed0208e29" + "Comment": "v2.2.1", + "Rev": "07a7330929b9ee80495c88a4439657d89c7dbd87" }, { "ImportPath": "github.com/go-macaron/binding", diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml b/Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml index f90ee667c09..3a5402596d0 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml @@ -2,10 +2,13 @@ language: go go: - 1.2 - 1.3 + - 1.4 + - 1.5 - tip +go_import_path: gopkg.in/ldap.v2 install: - go get gopkg.in/asn1-ber.v1 - - go get gopkg.in/ldap.v1 + - go get gopkg.in/ldap.v2 - go get code.google.com/p/go.tools/cmd/cover || go get golang.org/x/tools/cmd/cover - go build -v ./... script: diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/README.md b/Godeps/_workspace/src/github.com/go-ldap/ldap/README.md index c940520461e..f49b4d6a1b3 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/README.md +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/README.md @@ -1,8 +1,20 @@ -[![GoDoc](https://godoc.org/gopkg.in/ldap.v1?status.svg)](https://godoc.org/gopkg.in/ldap.v1) [![Build Status](https://travis-ci.org/go-ldap/ldap.svg)](https://travis-ci.org/go-ldap/ldap) +[![GoDoc](https://godoc.org/gopkg.in/ldap.v2?status.svg)](https://godoc.org/gopkg.in/ldap.v2) +[![Build Status](https://travis-ci.org/go-ldap/ldap.svg)](https://travis-ci.org/go-ldap/ldap) # Basic LDAP v3 functionality for the GO programming language. -## Required Librarys: +## Install + +For the latest version use: + + go get gopkg.in/ldap.v2 + +Import the latest version with: + + import "gopkg.in/ldap.v2" + + +## Required Libraries: - gopkg.in/asn1-ber.v1 @@ -14,6 +26,9 @@ - Compiling string filters to LDAP filters - Paging Search Results - Modify Requests / Responses + - Add Requests / Responses + - Delete Requests / Responses + - Better Unicode support ## Examples: @@ -26,23 +41,15 @@ ## TODO: - - Add Requests / Responses - - Delete Requests / Responses - - Modify DN Requests / Responses - - Compare Requests / Responses - - Implement Tests / Benchmarks + - [x] Add Requests / Responses + - [x] Delete Requests / Responses + - [x] Modify DN Requests / Responses + - [ ] Compare Requests / Responses + - [ ] Implement Tests / Benchmarks + + --- -This feature is disabled at the moment, because in some cases the "Search Request Done" packet will be handled before the last "Search Request Entry": - - - Mulitple internal goroutines to handle network traffic - Makes library goroutine safe - Can perform multiple search requests at the same time and return - the results to the proper goroutine. All requests are blocking requests, - so the goroutine does not need special handling - ---- - The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/) The design is licensed under the Creative Commons 3.0 Attributions license. Read this article for more details: http://blog.golang.org/gopher diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/add.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/add.go new file mode 100644 index 00000000000..643ce5ffe4c --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/add.go @@ -0,0 +1,104 @@ +// +// https://tools.ietf.org/html/rfc4511 +// +// AddRequest ::= [APPLICATION 8] SEQUENCE { +// entry LDAPDN, +// attributes AttributeList } +// +// AttributeList ::= SEQUENCE OF attribute Attribute + +package ldap + +import ( + "errors" + "log" + + "gopkg.in/asn1-ber.v1" +) + +type Attribute struct { + attrType string + attrVals []string +} + +func (a *Attribute) encode() *ber.Packet { + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute") + seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.attrType, "Type")) + set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") + for _, value := range a.attrVals { + set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) + } + seq.AppendChild(set) + return seq +} + +type AddRequest struct { + dn string + attributes []Attribute +} + +func (a AddRequest) encode() *ber.Packet { + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationAddRequest, nil, "Add Request") + request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.dn, "DN")) + attributes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") + for _, attribute := range a.attributes { + attributes.AppendChild(attribute.encode()) + } + request.AppendChild(attributes) + return request +} + +func (a *AddRequest) Attribute(attrType string, attrVals []string) { + a.attributes = append(a.attributes, Attribute{attrType: attrType, attrVals: attrVals}) +} + +func NewAddRequest(dn string) *AddRequest { + return &AddRequest{ + dn: dn, + } + +} + +func (l *Conn) Add(addRequest *AddRequest) error { + messageID := l.nextMessageID() + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) + packet.AppendChild(addRequest.encode()) + + l.Debug.PrintPacket(packet) + + channel, err := l.sendMessage(packet) + if err != nil { + return err + } + if channel == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not send message")) + } + defer l.finishMessage(messageID) + + l.Debug.Printf("%d: waiting for response", messageID) + packet = <-channel + l.Debug.Printf("%d: got response %p", messageID, packet) + if packet == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return err + } + ber.PrintPacket(packet) + } + + if packet.Children[1].Tag == ApplicationAddResponse { + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return NewError(resultCode, errors.New(resultDescription)) + } + } else { + log.Printf("Unexpected Response: %d", packet.Children[1].Tag) + } + + l.Debug.Printf("%d: returning", messageID) + return nil +} diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/client.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/client.go new file mode 100644 index 00000000000..d3401f9e61e --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/client.go @@ -0,0 +1,23 @@ +package ldap + +import "crypto/tls" + +// Client knows how to interact with an LDAP server +type Client interface { + Start() + StartTLS(config *tls.Config) error + Close() + + Bind(username, password string) error + SimpleBind(simpleBindRequest *SimpleBindRequest) (*SimpleBindResult, error) + + Add(addRequest *AddRequest) error + Del(delRequest *DelRequest) error + Modify(modifyRequest *ModifyRequest) error + + Compare(dn, attribute, value string) (bool, error) + PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error) + + Search(searchRequest *SearchRequest) (*SearchResult, error) + SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) +} diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go index c51e1afe87d..2f16443f6e4 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go @@ -8,11 +8,12 @@ import ( "crypto/tls" "errors" "fmt" - "gopkg.in/asn1-ber.v1" "log" "net" "sync" "time" + + "gopkg.in/asn1-ber.v1" ) const ( @@ -53,6 +54,8 @@ type Conn struct { messageMutex sync.Mutex } +var _ Client = &Conn{} + // DefaultTimeout is a package-level variable that sets the timeout value // used for the Dial and DialTLS methods. // @@ -176,7 +179,7 @@ func (l *Conn) StartTLS(config *tls.Config) error { ber.PrintPacket(packet) } - if packet.Children[1].Children[0].Value.(int64) == 0 { + if resultCode, message := getLDAPResultCode(packet); resultCode == LDAPResultSuccess { conn := tls.Client(l.conn, config) if err := conn.Handshake(); err != nil { @@ -186,6 +189,8 @@ func (l *Conn) StartTLS(config *tls.Config) error { l.isTLS = true l.conn = conn + } else { + return NewError(resultCode, fmt.Errorf("ldap: cannot StartTLS (%s)", message)) } go l.reader() diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/control.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/control.go index 562fbe43090..4d82980933e 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/control.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/control.go @@ -16,11 +16,13 @@ const ( ControlTypeBeheraPasswordPolicy = "1.3.6.1.4.1.42.2.27.8.5.1" ControlTypeVChuPasswordMustChange = "2.16.840.1.113730.3.4.4" ControlTypeVChuPasswordWarning = "2.16.840.1.113730.3.4.5" + ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2" ) var ControlTypeMap = map[string]string{ ControlTypePaging: "Paging", ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft", + ControlTypeManageDsaIT: "Manage DSA IT", } type Control interface { @@ -165,6 +167,36 @@ func (c *ControlVChuPasswordWarning) String() string { c.Expire) } +type ControlManageDsaIT struct { + Criticality bool +} + +func (c *ControlManageDsaIT) GetControlType() string { + return ControlTypeManageDsaIT +} + +func (c *ControlManageDsaIT) Encode() *ber.Packet { + //FIXME + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeManageDsaIT, "Control Type ("+ControlTypeMap[ControlTypeManageDsaIT]+")")) + if c.Criticality { + packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) + } + return packet +} + +func (c *ControlManageDsaIT) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t", + ControlTypeMap[ControlTypeManageDsaIT], + ControlTypeManageDsaIT, + c.Criticality) +} + +func NewControlManageDsaIT(Criticality bool) *ControlManageDsaIT { + return &ControlManageDsaIT{Criticality: Criticality} +} + func FindControl(controls []Control, controlType string) Control { for _, c := range controls { if c.GetControlType() == controlType { diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/del.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/del.go new file mode 100644 index 00000000000..2f0eae1cd29 --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/del.go @@ -0,0 +1,79 @@ +// +// https://tools.ietf.org/html/rfc4511 +// +// DelRequest ::= [APPLICATION 10] LDAPDN + +package ldap + +import ( + "errors" + "log" + + "gopkg.in/asn1-ber.v1" +) + +type DelRequest struct { + DN string + Controls []Control +} + +func (d DelRequest) encode() *ber.Packet { + request := ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationDelRequest, d.DN, "Del Request") + request.Data.Write([]byte(d.DN)) + return request +} + +func NewDelRequest(DN string, + Controls []Control) *DelRequest { + return &DelRequest{ + DN: DN, + Controls: Controls, + } +} + +func (l *Conn) Del(delRequest *DelRequest) error { + messageID := l.nextMessageID() + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) + packet.AppendChild(delRequest.encode()) + if delRequest.Controls != nil { + packet.AppendChild(encodeControls(delRequest.Controls)) + } + + l.Debug.PrintPacket(packet) + + channel, err := l.sendMessage(packet) + if err != nil { + return err + } + if channel == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not send message")) + } + defer l.finishMessage(messageID) + + l.Debug.Printf("%d: waiting for response", messageID) + packet = <-channel + l.Debug.Printf("%d: got response %p", messageID, packet) + if packet == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return err + } + ber.PrintPacket(packet) + } + + if packet.Children[1].Tag == ApplicationDelResponse { + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return NewError(resultCode, errors.New(resultDescription)) + } + } else { + log.Printf("Unexpected Response: %d", packet.Children[1].Tag) + } + + l.Debug.Printf("%d: returning", messageID) + return nil +} diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go index 31d52db4b23..5d83c5e9ab9 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go @@ -47,17 +47,17 @@ package ldap import ( "bytes" + enchex "encoding/hex" "errors" "fmt" "strings" - enchex "encoding/hex" ber "gopkg.in/asn1-ber.v1" ) type AttributeTypeAndValue struct { - Type string - Value string + Type string + Value string } type RelativeDN struct { @@ -71,7 +71,7 @@ type DN struct { func ParseDN(str string) (*DN, error) { dn := new(DN) dn.RDNs = make([]*RelativeDN, 0) - rdn := new (RelativeDN) + rdn := new(RelativeDN) rdn.Attributes = make([]*AttributeTypeAndValue, 0) buffer := bytes.Buffer{} attribute := new(AttributeTypeAndValue) @@ -115,7 +115,7 @@ func ParseDN(str string) (*DN, error) { index := strings.IndexAny(str[i:], ",+") data := str if index > 0 { - data = str[i:i+index] + data = str[i : i+index] } else { data = str[i:] } @@ -126,7 +126,7 @@ func ParseDN(str string) (*DN, error) { } packet := ber.DecodePacket(raw_ber) buffer.WriteString(packet.Data.String()) - i += len(data)-1 + i += len(data) - 1 } } else if char == ',' || char == '+' { // We're done with this RDN or value, push it diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go index 6740e1819c7..39817c42741 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go @@ -1,38 +1,40 @@ -package ldap +package ldap_test import ( "reflect" "testing" + + "gopkg.in/ldap.v2" ) func TestSuccessfulDNParsing(t *testing.T) { - testcases := map[string]DN { - "": DN{[]*RelativeDN{}}, - "cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"cn", "Jim, \"Hasse Hö\" Hansson!"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"dc", "dummy"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"dc", "com"}, }},}}, - "UID=jsmith,DC=example,DC=net": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"UID", "jsmith"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "example"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}}, - "OU=Sales+CN=J. Smith,DC=example,DC=net": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{ - &AttributeTypeAndValue{"OU", "Sales"}, - &AttributeTypeAndValue{"CN", "J. Smith"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "example"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}}, - "1.3.6.1.4.1.1466.0=#04024869": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"},}},}}, - "1.3.6.1.4.1.1466.0=#04024869,DC=net": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}}, - "CN=Lu\\C4\\8Di\\C4\\87": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"CN", "Lučić"},}},}}, + testcases := map[string]ldap.DN{ + "": ldap.DN{[]*ldap.RelativeDN{}}, + "cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"cn", "Jim, \"Hasse Hö\" Hansson!"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"dc", "dummy"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"dc", "com"}}}}}, + "UID=jsmith,DC=example,DC=net": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"UID", "jsmith"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "example"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}}, + "OU=Sales+CN=J. Smith,DC=example,DC=net": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{ + &ldap.AttributeTypeAndValue{"OU", "Sales"}, + &ldap.AttributeTypeAndValue{"CN", "J. Smith"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "example"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}}, + "1.3.6.1.4.1.1466.0=#04024869": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}}}}, + "1.3.6.1.4.1.1466.0=#04024869,DC=net": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}}, + "CN=Lu\\C4\\8Di\\C4\\87": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"CN", "Lučić"}}}}}, } for test, answer := range testcases { - dn, err := ParseDN(test) + dn, err := ldap.ParseDN(test) if err != nil { t.Errorf(err.Error()) continue @@ -49,16 +51,16 @@ func TestSuccessfulDNParsing(t *testing.T) { } func TestErrorDNParsing(t *testing.T) { - testcases := map[string]string { - "*": "DN ended with incomplete type, value pair", - "cn=Jim\\0Test": "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'", - "cn=Jim\\0": "Got corrupted escaped character", + testcases := map[string]string{ + "*": "DN ended with incomplete type, value pair", + "cn=Jim\\0Test": "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'", + "cn=Jim\\0": "Got corrupted escaped character", "DC=example,=net": "DN ended with incomplete type, value pair", - "1=#0402486": "Failed to decode BER encoding: encoding/hex: odd length hex string", + "1=#0402486": "Failed to decode BER encoding: encoding/hex: odd length hex string", } for test, answer := range testcases { - _, err := ParseDN(test) + _, err := ldap.ParseDN(test) if err == nil { t.Errorf("Expected %s to fail parsing but succeeded\n", test) } else if err.Error() != answer { @@ -66,5 +68,3 @@ func TestErrorDNParsing(t *testing.T) { } } } - - diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/error.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/error.go new file mode 100644 index 00000000000..2dbc30ac085 --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/error.go @@ -0,0 +1,137 @@ +package ldap + +import ( + "fmt" + + "gopkg.in/asn1-ber.v1" +) + +// LDAP Result Codes +const ( + LDAPResultSuccess = 0 + LDAPResultOperationsError = 1 + LDAPResultProtocolError = 2 + LDAPResultTimeLimitExceeded = 3 + LDAPResultSizeLimitExceeded = 4 + LDAPResultCompareFalse = 5 + LDAPResultCompareTrue = 6 + LDAPResultAuthMethodNotSupported = 7 + LDAPResultStrongAuthRequired = 8 + LDAPResultReferral = 10 + LDAPResultAdminLimitExceeded = 11 + LDAPResultUnavailableCriticalExtension = 12 + LDAPResultConfidentialityRequired = 13 + LDAPResultSaslBindInProgress = 14 + LDAPResultNoSuchAttribute = 16 + LDAPResultUndefinedAttributeType = 17 + LDAPResultInappropriateMatching = 18 + LDAPResultConstraintViolation = 19 + LDAPResultAttributeOrValueExists = 20 + LDAPResultInvalidAttributeSyntax = 21 + LDAPResultNoSuchObject = 32 + LDAPResultAliasProblem = 33 + LDAPResultInvalidDNSyntax = 34 + LDAPResultAliasDereferencingProblem = 36 + LDAPResultInappropriateAuthentication = 48 + LDAPResultInvalidCredentials = 49 + LDAPResultInsufficientAccessRights = 50 + LDAPResultBusy = 51 + LDAPResultUnavailable = 52 + LDAPResultUnwillingToPerform = 53 + LDAPResultLoopDetect = 54 + LDAPResultNamingViolation = 64 + LDAPResultObjectClassViolation = 65 + LDAPResultNotAllowedOnNonLeaf = 66 + LDAPResultNotAllowedOnRDN = 67 + LDAPResultEntryAlreadyExists = 68 + LDAPResultObjectClassModsProhibited = 69 + LDAPResultAffectsMultipleDSAs = 71 + LDAPResultOther = 80 + + ErrorNetwork = 200 + ErrorFilterCompile = 201 + ErrorFilterDecompile = 202 + ErrorDebugging = 203 + ErrorUnexpectedMessage = 204 + ErrorUnexpectedResponse = 205 +) + +var LDAPResultCodeMap = map[uint8]string{ + LDAPResultSuccess: "Success", + LDAPResultOperationsError: "Operations Error", + LDAPResultProtocolError: "Protocol Error", + LDAPResultTimeLimitExceeded: "Time Limit Exceeded", + LDAPResultSizeLimitExceeded: "Size Limit Exceeded", + LDAPResultCompareFalse: "Compare False", + LDAPResultCompareTrue: "Compare True", + LDAPResultAuthMethodNotSupported: "Auth Method Not Supported", + LDAPResultStrongAuthRequired: "Strong Auth Required", + LDAPResultReferral: "Referral", + LDAPResultAdminLimitExceeded: "Admin Limit Exceeded", + LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension", + LDAPResultConfidentialityRequired: "Confidentiality Required", + LDAPResultSaslBindInProgress: "Sasl Bind In Progress", + LDAPResultNoSuchAttribute: "No Such Attribute", + LDAPResultUndefinedAttributeType: "Undefined Attribute Type", + LDAPResultInappropriateMatching: "Inappropriate Matching", + LDAPResultConstraintViolation: "Constraint Violation", + LDAPResultAttributeOrValueExists: "Attribute Or Value Exists", + LDAPResultInvalidAttributeSyntax: "Invalid Attribute Syntax", + LDAPResultNoSuchObject: "No Such Object", + LDAPResultAliasProblem: "Alias Problem", + LDAPResultInvalidDNSyntax: "Invalid DN Syntax", + LDAPResultAliasDereferencingProblem: "Alias Dereferencing Problem", + LDAPResultInappropriateAuthentication: "Inappropriate Authentication", + LDAPResultInvalidCredentials: "Invalid Credentials", + LDAPResultInsufficientAccessRights: "Insufficient Access Rights", + LDAPResultBusy: "Busy", + LDAPResultUnavailable: "Unavailable", + LDAPResultUnwillingToPerform: "Unwilling To Perform", + LDAPResultLoopDetect: "Loop Detect", + LDAPResultNamingViolation: "Naming Violation", + LDAPResultObjectClassViolation: "Object Class Violation", + LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", + LDAPResultNotAllowedOnRDN: "Not Allowed On RDN", + LDAPResultEntryAlreadyExists: "Entry Already Exists", + LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", + LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", + LDAPResultOther: "Other", +} + +func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) { + if len(packet.Children) >= 2 { + response := packet.Children[1] + if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 { + // Children[1].Children[2] is the diagnosticMessage which is guaranteed to exist as seen here: https://tools.ietf.org/html/rfc4511#section-4.1.9 + return uint8(response.Children[0].Value.(int64)), response.Children[2].Value.(string) + } + } + + return ErrorNetwork, "Invalid packet format" +} + +type Error struct { + Err error + ResultCode uint8 +} + +func (e *Error) Error() string { + return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) +} + +func NewError(resultCode uint8, err error) error { + return &Error{ResultCode: resultCode, Err: err} +} + +func IsErrorWithCode(err error, desiredResultCode uint8) bool { + if err == nil { + return false + } + + serverError, ok := err.(*Error) + if !ok { + return false + } + + return serverError.ResultCode == desiredResultCode +} diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go index 0d441f43b99..b018a966489 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go @@ -5,10 +5,10 @@ import ( "fmt" "log" - "github.com/go-ldap/ldap" + "gopkg.in/ldap.v2" ) -// ExampleConn_Bind demonstrats how to bind a connection to an ldap user +// ExampleConn_Bind demonstrates how to bind a connection to an ldap user // allowing access to restricted attrabutes that user has access to func ExampleConn_Bind() { l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go index 1ee1ff89d87..63bcec1e3ae 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go @@ -5,9 +5,12 @@ package ldap import ( + "bytes" + hexpac "encoding/hex" "errors" "fmt" "strings" + "unicode/utf8" "gopkg.in/asn1-ber.v1" ) @@ -50,6 +53,20 @@ var FilterSubstringsMap = map[uint64]string{ FilterSubstringsFinal: "Substrings Final", } +const ( + MatchingRuleAssertionMatchingRule = 1 + MatchingRuleAssertionType = 2 + MatchingRuleAssertionMatchValue = 3 + MatchingRuleAssertionDNAttributes = 4 +) + +var MatchingRuleAssertionMap = map[uint64]string{ + MatchingRuleAssertionMatchingRule: "Matching Rule Assertion Matching Rule", + MatchingRuleAssertionType: "Matching Rule Assertion Type", + MatchingRuleAssertionMatchValue: "Matching Rule Assertion Match Value", + MatchingRuleAssertionDNAttributes: "Matching Rule Assertion DN Attributes", +} + func CompileFilter(filter string) (*ber.Packet, error) { if len(filter) == 0 || filter[0] != '(' { return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('")) @@ -108,7 +125,7 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) { if i == 0 && child.Tag != FilterSubstringsInitial { ret += "*" } - ret += ber.DecodeString(child.Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(child.Data.Bytes())) if child.Tag != FilterSubstringsFinal { ret += "*" } @@ -116,22 +133,53 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) { case FilterEqualityMatch: ret += ber.DecodeString(packet.Children[0].Data.Bytes()) ret += "=" - ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) case FilterGreaterOrEqual: ret += ber.DecodeString(packet.Children[0].Data.Bytes()) ret += ">=" - ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) case FilterLessOrEqual: ret += ber.DecodeString(packet.Children[0].Data.Bytes()) ret += "<=" - ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) case FilterPresent: ret += ber.DecodeString(packet.Data.Bytes()) ret += "=*" case FilterApproxMatch: ret += ber.DecodeString(packet.Children[0].Data.Bytes()) ret += "~=" - ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) + case FilterExtensibleMatch: + attr := "" + dnAttributes := false + matchingRule := "" + value := "" + + for _, child := range packet.Children { + switch child.Tag { + case MatchingRuleAssertionMatchingRule: + matchingRule = ber.DecodeString(child.Data.Bytes()) + case MatchingRuleAssertionType: + attr = ber.DecodeString(child.Data.Bytes()) + case MatchingRuleAssertionMatchValue: + value = ber.DecodeString(child.Data.Bytes()) + case MatchingRuleAssertionDNAttributes: + dnAttributes = child.Value.(bool) + } + } + + if len(attr) > 0 { + ret += attr + } + if dnAttributes { + ret += ":dn" + } + if len(matchingRule) > 0 { + ret += ":" + ret += matchingRule + } + ret += ":=" + ret += EscapeFilter(value) } ret += ")" @@ -155,58 +203,143 @@ func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) { } func compileFilter(filter string, pos int) (*ber.Packet, int, error) { - var packet *ber.Packet - var err error + var ( + packet *ber.Packet + err error + ) defer func() { if r := recover(); r != nil { err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter")) } }() - newPos := pos - switch filter[pos] { + + currentRune, currentWidth := utf8.DecodeRuneInString(filter[newPos:]) + + switch currentRune { + case utf8.RuneError: + return nil, 0, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos)) case '(': - packet, newPos, err = compileFilter(filter, pos+1) + packet, newPos, err = compileFilter(filter, pos+currentWidth) newPos++ return packet, newPos, err case '&': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd]) - newPos, err = compileFilterSet(filter, pos+1, packet) + newPos, err = compileFilterSet(filter, pos+currentWidth, packet) return packet, newPos, err case '|': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[FilterOr]) - newPos, err = compileFilterSet(filter, pos+1, packet) + newPos, err = compileFilterSet(filter, pos+currentWidth, packet) return packet, newPos, err case '!': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterNot, nil, FilterMap[FilterNot]) var child *ber.Packet - child, newPos, err = compileFilter(filter, pos+1) + child, newPos, err = compileFilter(filter, pos+currentWidth) packet.AppendChild(child) return packet, newPos, err default: + READING_ATTR := 0 + READING_EXTENSIBLE_MATCHING_RULE := 1 + READING_CONDITION := 2 + + state := READING_ATTR + attribute := "" + extensibleDNAttributes := false + extensibleMatchingRule := "" condition := "" - for newPos < len(filter) && filter[newPos] != ')' { - switch { - case packet != nil: - condition += fmt.Sprintf("%c", filter[newPos]) - case filter[newPos] == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) - case filter[newPos] == '>' && filter[newPos+1] == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) - newPos++ - case filter[newPos] == '<' && filter[newPos+1] == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) - newPos++ - case filter[newPos] == '~' && filter[newPos+1] == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterLessOrEqual]) - newPos++ - case packet == nil: - attribute += fmt.Sprintf("%c", filter[newPos]) + + for newPos < len(filter) { + remainingFilter := filter[newPos:] + currentRune, currentWidth = utf8.DecodeRuneInString(remainingFilter) + if currentRune == ')' { + break + } + if currentRune == utf8.RuneError { + return packet, newPos, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos)) + } + + switch state { + case READING_ATTR: + switch { + // Extensible rule, with only DN-matching + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + extensibleDNAttributes = true + state = READING_CONDITION + newPos += 5 + + // Extensible rule, with DN-matching and a matching OID + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:"): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + extensibleDNAttributes = true + state = READING_EXTENSIBLE_MATCHING_RULE + newPos += 4 + + // Extensible rule, with attr only + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + state = READING_CONDITION + newPos += 2 + + // Extensible rule, with no DN attribute matching + case currentRune == ':': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + state = READING_EXTENSIBLE_MATCHING_RULE + newPos += 1 + + // Equality condition + case currentRune == '=': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) + state = READING_CONDITION + newPos += 1 + + // Greater-than or equal + case currentRune == '>' && strings.HasPrefix(remainingFilter, ">="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) + state = READING_CONDITION + newPos += 2 + + // Less-than or equal + case currentRune == '<' && strings.HasPrefix(remainingFilter, "<="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) + state = READING_CONDITION + newPos += 2 + + // Approx + case currentRune == '~' && strings.HasPrefix(remainingFilter, "~="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterApproxMatch]) + state = READING_CONDITION + newPos += 2 + + // Still reading the attribute name + default: + attribute += fmt.Sprintf("%c", currentRune) + newPos += currentWidth + } + + case READING_EXTENSIBLE_MATCHING_RULE: + switch { + + // Matching rule OID is done + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): + state = READING_CONDITION + newPos += 2 + + // Still reading the matching rule oid + default: + extensibleMatchingRule += fmt.Sprintf("%c", currentRune) + newPos += currentWidth + } + + case READING_CONDITION: + // append to the condition + condition += fmt.Sprintf("%c", currentRune) + newPos += currentWidth } - newPos++ } + if newPos == len(filter) { err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) return packet, newPos, err @@ -217,6 +350,36 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { } switch { + case packet.Tag == FilterExtensibleMatch: + // MatchingRuleAssertion ::= SEQUENCE { + // matchingRule [1] MatchingRuleID OPTIONAL, + // type [2] AttributeDescription OPTIONAL, + // matchValue [3] AssertionValue, + // dnAttributes [4] BOOLEAN DEFAULT FALSE + // } + + // Include the matching rule oid, if specified + if len(extensibleMatchingRule) > 0 { + packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchingRule, extensibleMatchingRule, MatchingRuleAssertionMap[MatchingRuleAssertionMatchingRule])) + } + + // Include the attribute, if specified + if len(attribute) > 0 { + packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionType, attribute, MatchingRuleAssertionMap[MatchingRuleAssertionType])) + } + + // Add the value (only required child) + encodedString, err := escapedStringToEncodedBytes(condition) + if err != nil { + return packet, newPos, err + } + packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchValue, encodedString, MatchingRuleAssertionMap[MatchingRuleAssertionMatchValue])) + + // Defaults to false, so only include in the sequence if true + if extensibleDNAttributes { + packet.AppendChild(ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionDNAttributes, extensibleDNAttributes, MatchingRuleAssertionMap[MatchingRuleAssertionDNAttributes])) + } + case packet.Tag == FilterEqualityMatch && condition == "*": packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute, FilterMap[FilterPresent]) case packet.Tag == FilterEqualityMatch && strings.Contains(condition, "*"): @@ -238,15 +401,56 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { default: tag = FilterSubstringsAny } - seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, part, FilterSubstringsMap[uint64(tag)])) + encodedString, err := escapedStringToEncodedBytes(part) + if err != nil { + return packet, newPos, err + } + seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, encodedString, FilterSubstringsMap[uint64(tag)])) } packet.AppendChild(seq) default: + encodedString, err := escapedStringToEncodedBytes(condition) + if err != nil { + return packet, newPos, err + } packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, condition, "Condition")) + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, encodedString, "Condition")) } - newPos++ + newPos += currentWidth return packet, newPos, err } } + +// Convert from "ABC\xx\xx\xx" form to literal bytes for transport +func escapedStringToEncodedBytes(escapedString string) (string, error) { + var buffer bytes.Buffer + i := 0 + for i < len(escapedString) { + currentRune, currentWidth := utf8.DecodeRuneInString(escapedString[i:]) + if currentRune == utf8.RuneError { + return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", i)) + } + + // Check for escaped hex characters and convert them to their literal value for transport. + if currentRune == '\\' { + // http://tools.ietf.org/search/rfc4515 + // \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not + // being a member of UTF1SUBSET. + if i+2 > len(escapedString) { + return "", NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter")) + } + if escByte, decodeErr := hexpac.DecodeString(escapedString[i+1 : i+3]); decodeErr != nil { + return "", NewError(ErrorFilterCompile, errors.New("ldap: invalid characters for escape in filter")) + } else { + buffer.WriteByte(escByte[0]) + i += 2 // +1 from end of loop, so 3 total for \xx. + } + } else { + buffer.WriteRune(currentRune) + } + + i += currentWidth + } + return buffer.String(), nil +} diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go index 673ef080235..ae1b79b0c0d 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go @@ -1,54 +1,220 @@ -package ldap +package ldap_test import ( + "strings" "testing" "gopkg.in/asn1-ber.v1" + "gopkg.in/ldap.v2" ) type compileTest struct { - filterStr string - filterType int + filterStr string + + expectedFilter string + expectedType int + expectedErr string } var testFilters = []compileTest{ - compileTest{filterStr: "(&(sn=Miller)(givenName=Bob))", filterType: FilterAnd}, - compileTest{filterStr: "(|(sn=Miller)(givenName=Bob))", filterType: FilterOr}, - compileTest{filterStr: "(!(sn=Miller))", filterType: FilterNot}, - compileTest{filterStr: "(sn=Miller)", filterType: FilterEqualityMatch}, - compileTest{filterStr: "(sn=Mill*)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=*Mill)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=*Mill*)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=*i*le*)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=Mi*l*r)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=Mi*le*)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=*i*ler)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn>=Miller)", filterType: FilterGreaterOrEqual}, - compileTest{filterStr: "(sn<=Miller)", filterType: FilterLessOrEqual}, - compileTest{filterStr: "(sn=*)", filterType: FilterPresent}, - compileTest{filterStr: "(sn~=Miller)", filterType: FilterApproxMatch}, + compileTest{ + filterStr: "(&(sn=Miller)(givenName=Bob))", + expectedFilter: "(&(sn=Miller)(givenName=Bob))", + expectedType: ldap.FilterAnd, + }, + compileTest{ + filterStr: "(|(sn=Miller)(givenName=Bob))", + expectedFilter: "(|(sn=Miller)(givenName=Bob))", + expectedType: ldap.FilterOr, + }, + compileTest{ + filterStr: "(!(sn=Miller))", + expectedFilter: "(!(sn=Miller))", + expectedType: ldap.FilterNot, + }, + compileTest{ + filterStr: "(sn=Miller)", + expectedFilter: "(sn=Miller)", + expectedType: ldap.FilterEqualityMatch, + }, + compileTest{ + filterStr: "(sn=Mill*)", + expectedFilter: "(sn=Mill*)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=*Mill)", + expectedFilter: "(sn=*Mill)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=*Mill*)", + expectedFilter: "(sn=*Mill*)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=*i*le*)", + expectedFilter: "(sn=*i*le*)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=Mi*l*r)", + expectedFilter: "(sn=Mi*l*r)", + expectedType: ldap.FilterSubstrings, + }, + // substring filters escape properly + compileTest{ + filterStr: `(sn=Mi*함*r)`, + expectedFilter: `(sn=Mi*\ed\95\a8*r)`, + expectedType: ldap.FilterSubstrings, + }, + // already escaped substring filters don't get double-escaped + compileTest{ + filterStr: `(sn=Mi*\ed\95\a8*r)`, + expectedFilter: `(sn=Mi*\ed\95\a8*r)`, + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=Mi*le*)", + expectedFilter: "(sn=Mi*le*)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=*i*ler)", + expectedFilter: "(sn=*i*ler)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn>=Miller)", + expectedFilter: "(sn>=Miller)", + expectedType: ldap.FilterGreaterOrEqual, + }, + compileTest{ + filterStr: "(sn<=Miller)", + expectedFilter: "(sn<=Miller)", + expectedType: ldap.FilterLessOrEqual, + }, + compileTest{ + filterStr: "(sn=*)", + expectedFilter: "(sn=*)", + expectedType: ldap.FilterPresent, + }, + compileTest{ + filterStr: "(sn~=Miller)", + expectedFilter: "(sn~=Miller)", + expectedType: ldap.FilterApproxMatch, + }, + compileTest{ + filterStr: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`, + expectedFilter: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`, + expectedType: ldap.FilterEqualityMatch, + }, + compileTest{ + filterStr: `(objectGUID=абвгдеёжзийклмнопрстуфхцчшщъыьэюя)`, + expectedFilter: `(objectGUID=\d0\b0\d0\b1\d0\b2\d0\b3\d0\b4\d0\b5\d1\91\d0\b6\d0\b7\d0\b8\d0\b9\d0\ba\d0\bb\d0\bc\d0\bd\d0\be\d0\bf\d1\80\d1\81\d1\82\d1\83\d1\84\d1\85\d1\86\d1\87\d1\88\d1\89\d1\8a\d1\8b\d1\8c\d1\8d\d1\8e\d1\8f)`, + expectedType: ldap.FilterEqualityMatch, + }, + compileTest{ + filterStr: `(objectGUID=함수목록)`, + expectedFilter: `(objectGUID=\ed\95\a8\ec\88\98\eb\aa\a9\eb\a1\9d)`, + expectedType: ldap.FilterEqualityMatch, + }, + compileTest{ + filterStr: `(objectGUID=`, + expectedFilter: ``, + expectedType: 0, + expectedErr: "unexpected end of filter", + }, + compileTest{ + filterStr: `(objectGUID=함수목록`, + expectedFilter: ``, + expectedType: 0, + expectedErr: "unexpected end of filter", + }, + compileTest{ + filterStr: `(&(objectclass=inetorgperson)(cn=中文))`, + expectedFilter: `(&(objectclass=inetorgperson)(cn=\e4\b8\ad\e6\96\87))`, + expectedType: 0, + }, + // attr extension + compileTest{ + filterStr: `(memberOf:=foo)`, + expectedFilter: `(memberOf:=foo)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+named matching rule extension + compileTest{ + filterStr: `(memberOf:test:=foo)`, + expectedFilter: `(memberOf:test:=foo)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+oid matching rule extension + compileTest{ + filterStr: `(cn:1.2.3.4.5:=Fred Flintstone)`, + expectedFilter: `(cn:1.2.3.4.5:=Fred Flintstone)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+dn+oid matching rule extension + compileTest{ + filterStr: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, + expectedFilter: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+dn extension + compileTest{ + filterStr: `(o:dn:=Ace Industry)`, + expectedFilter: `(o:dn:=Ace Industry)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // dn extension + compileTest{ + filterStr: `(:dn:2.4.6.8.10:=Dino)`, + expectedFilter: `(:dn:2.4.6.8.10:=Dino)`, + expectedType: ldap.FilterExtensibleMatch, + }, + compileTest{ + filterStr: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`, + expectedFilter: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // compileTest{ filterStr: "()", filterType: FilterExtensibleMatch }, } +var testInvalidFilters = []string{ + `(objectGUID=\zz)`, + `(objectGUID=\a)`, +} + func TestFilter(t *testing.T) { // Test Compiler and Decompiler for _, i := range testFilters { - filter, err := CompileFilter(i.filterStr) + filter, err := ldap.CompileFilter(i.filterStr) if err != nil { - t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) - } else if filter.Tag != ber.Tag(i.filterType) { - t.Errorf("%q Expected %q got %q", i.filterStr, FilterMap[uint64(i.filterType)], FilterMap[uint64(filter.Tag)]) + if i.expectedErr == "" || !strings.Contains(err.Error(), i.expectedErr) { + t.Errorf("Problem compiling '%s' - '%v' (expected error to contain '%v')", i.filterStr, err, i.expectedErr) + } + } else if filter.Tag != ber.Tag(i.expectedType) { + t.Errorf("%q Expected %q got %q", i.filterStr, ldap.FilterMap[uint64(i.expectedType)], ldap.FilterMap[uint64(filter.Tag)]) } else { - o, err := DecompileFilter(filter) + o, err := ldap.DecompileFilter(filter) if err != nil { t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) - } else if i.filterStr != o { - t.Errorf("%q expected, got %q", i.filterStr, o) + } else if i.expectedFilter != o { + t.Errorf("%q expected, got %q", i.expectedFilter, o) } } } } +func TestInvalidFilter(t *testing.T) { + for _, filterStr := range testInvalidFilters { + if _, err := ldap.CompileFilter(filterStr); err == nil { + t.Errorf("Problem compiling %s - expected err", filterStr) + } + } +} + func BenchmarkFilterCompile(b *testing.B) { b.StopTimer() filters := make([]string, len(testFilters)) @@ -61,7 +227,7 @@ func BenchmarkFilterCompile(b *testing.B) { maxIdx := len(filters) b.StartTimer() for i := 0; i < b.N; i++ { - CompileFilter(filters[i%maxIdx]) + ldap.CompileFilter(filters[i%maxIdx]) } } @@ -71,12 +237,12 @@ func BenchmarkFilterDecompile(b *testing.B) { // Test Compiler and Decompiler for idx, i := range testFilters { - filters[idx], _ = CompileFilter(i.filterStr) + filters[idx], _ = ldap.CompileFilter(i.filterStr) } maxIdx := len(filters) b.StartTimer() for i := 0; i < b.N; i++ { - DecompileFilter(filters[i%maxIdx]) + ldap.DecompileFilter(filters[i%maxIdx]) } } diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go index e91972ff4c1..1620aaea6ee 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go @@ -6,7 +6,6 @@ package ldap import ( "errors" - "fmt" "io/ioutil" "os" @@ -60,98 +59,6 @@ var ApplicationMap = map[uint8]string{ ApplicationExtendedResponse: "Extended Response", } -// LDAP Result Codes -const ( - LDAPResultSuccess = 0 - LDAPResultOperationsError = 1 - LDAPResultProtocolError = 2 - LDAPResultTimeLimitExceeded = 3 - LDAPResultSizeLimitExceeded = 4 - LDAPResultCompareFalse = 5 - LDAPResultCompareTrue = 6 - LDAPResultAuthMethodNotSupported = 7 - LDAPResultStrongAuthRequired = 8 - LDAPResultReferral = 10 - LDAPResultAdminLimitExceeded = 11 - LDAPResultUnavailableCriticalExtension = 12 - LDAPResultConfidentialityRequired = 13 - LDAPResultSaslBindInProgress = 14 - LDAPResultNoSuchAttribute = 16 - LDAPResultUndefinedAttributeType = 17 - LDAPResultInappropriateMatching = 18 - LDAPResultConstraintViolation = 19 - LDAPResultAttributeOrValueExists = 20 - LDAPResultInvalidAttributeSyntax = 21 - LDAPResultNoSuchObject = 32 - LDAPResultAliasProblem = 33 - LDAPResultInvalidDNSyntax = 34 - LDAPResultAliasDereferencingProblem = 36 - LDAPResultInappropriateAuthentication = 48 - LDAPResultInvalidCredentials = 49 - LDAPResultInsufficientAccessRights = 50 - LDAPResultBusy = 51 - LDAPResultUnavailable = 52 - LDAPResultUnwillingToPerform = 53 - LDAPResultLoopDetect = 54 - LDAPResultNamingViolation = 64 - LDAPResultObjectClassViolation = 65 - LDAPResultNotAllowedOnNonLeaf = 66 - LDAPResultNotAllowedOnRDN = 67 - LDAPResultEntryAlreadyExists = 68 - LDAPResultObjectClassModsProhibited = 69 - LDAPResultAffectsMultipleDSAs = 71 - LDAPResultOther = 80 - - ErrorNetwork = 200 - ErrorFilterCompile = 201 - ErrorFilterDecompile = 202 - ErrorDebugging = 203 - ErrorUnexpectedMessage = 204 - ErrorUnexpectedResponse = 205 -) - -var LDAPResultCodeMap = map[uint8]string{ - LDAPResultSuccess: "Success", - LDAPResultOperationsError: "Operations Error", - LDAPResultProtocolError: "Protocol Error", - LDAPResultTimeLimitExceeded: "Time Limit Exceeded", - LDAPResultSizeLimitExceeded: "Size Limit Exceeded", - LDAPResultCompareFalse: "Compare False", - LDAPResultCompareTrue: "Compare True", - LDAPResultAuthMethodNotSupported: "Auth Method Not Supported", - LDAPResultStrongAuthRequired: "Strong Auth Required", - LDAPResultReferral: "Referral", - LDAPResultAdminLimitExceeded: "Admin Limit Exceeded", - LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension", - LDAPResultConfidentialityRequired: "Confidentiality Required", - LDAPResultSaslBindInProgress: "Sasl Bind In Progress", - LDAPResultNoSuchAttribute: "No Such Attribute", - LDAPResultUndefinedAttributeType: "Undefined Attribute Type", - LDAPResultInappropriateMatching: "Inappropriate Matching", - LDAPResultConstraintViolation: "Constraint Violation", - LDAPResultAttributeOrValueExists: "Attribute Or Value Exists", - LDAPResultInvalidAttributeSyntax: "Invalid Attribute Syntax", - LDAPResultNoSuchObject: "No Such Object", - LDAPResultAliasProblem: "Alias Problem", - LDAPResultInvalidDNSyntax: "Invalid DN Syntax", - LDAPResultAliasDereferencingProblem: "Alias Dereferencing Problem", - LDAPResultInappropriateAuthentication: "Inappropriate Authentication", - LDAPResultInvalidCredentials: "Invalid Credentials", - LDAPResultInsufficientAccessRights: "Insufficient Access Rights", - LDAPResultBusy: "Busy", - LDAPResultUnavailable: "Unavailable", - LDAPResultUnwillingToPerform: "Unwilling To Perform", - LDAPResultLoopDetect: "Loop Detect", - LDAPResultNamingViolation: "Naming Violation", - LDAPResultObjectClassViolation: "Object Class Violation", - LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", - LDAPResultNotAllowedOnRDN: "Not Allowed On RDN", - LDAPResultEntryAlreadyExists: "Entry Already Exists", - LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", - LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", - LDAPResultOther: "Other", -} - // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10) const ( BeheraPasswordExpired = 0 @@ -318,8 +225,8 @@ func addRequestDescriptions(packet *ber.Packet) { } func addDefaultLDAPResponseDescriptions(packet *ber.Packet) { - resultCode := packet.Children[1].Children[0].Value.(int64) - packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[uint8(resultCode)] + ")" + resultCode, _ := getLDAPResultCode(packet) + packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[resultCode] + ")" packet.Children[1].Children[1].Description = "Matched DN" packet.Children[1].Children[2].Description = "Error Message" if len(packet.Children[1].Children) > 3 { @@ -343,30 +250,6 @@ func DebugBinaryFile(fileName string) error { return nil } -type Error struct { - Err error - ResultCode uint8 -} - -func (e *Error) Error() string { - return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) -} - -func NewError(resultCode uint8, err error) error { - return &Error{ResultCode: resultCode, Err: err} -} - -func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) { - if len(packet.Children) >= 2 { - response := packet.Children[1] - if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 { - return uint8(response.Children[0].Value.(int64)), response.Children[2].Value.(string) - } - } - - return ErrorNetwork, "Invalid packet format" -} - var hex = "0123456789abcdef" func mustEscape(c byte) bool { diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go index e9933f99a69..9f430518001 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go @@ -1,9 +1,11 @@ -package ldap +package ldap_test import ( "crypto/tls" "fmt" "testing" + + "gopkg.in/ldap.v2" ) var ldapServer = "ldap.itd.umich.edu" @@ -21,7 +23,7 @@ var attributes = []string{ func TestDial(t *testing.T) { fmt.Printf("TestDial: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return @@ -32,7 +34,7 @@ func TestDial(t *testing.T) { func TestDialTLS(t *testing.T) { fmt.Printf("TestDialTLS: starting...\n") - l, err := DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true}) + l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true}) if err != nil { t.Errorf(err.Error()) return @@ -43,7 +45,7 @@ func TestDialTLS(t *testing.T) { func TestStartTLS(t *testing.T) { fmt.Printf("TestStartTLS: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return @@ -58,16 +60,16 @@ func TestStartTLS(t *testing.T) { func TestSearch(t *testing.T) { fmt.Printf("TestSearch: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return } defer l.Close() - searchRequest := NewSearchRequest( + searchRequest := ldap.NewSearchRequest( baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, filter[0], attributes, nil) @@ -83,16 +85,16 @@ func TestSearch(t *testing.T) { func TestSearchStartTLS(t *testing.T) { fmt.Printf("TestSearchStartTLS: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return } defer l.Close() - searchRequest := NewSearchRequest( + searchRequest := ldap.NewSearchRequest( baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, filter[0], attributes, nil) @@ -123,7 +125,7 @@ func TestSearchStartTLS(t *testing.T) { func TestSearchWithPaging(t *testing.T) { fmt.Printf("TestSearchWithPaging: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return @@ -136,9 +138,9 @@ func TestSearchWithPaging(t *testing.T) { return } - searchRequest := NewSearchRequest( + searchRequest := ldap.NewSearchRequest( baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, filter[2], attributes, nil) @@ -149,12 +151,38 @@ func TestSearchWithPaging(t *testing.T) { } fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) + + searchRequest = ldap.NewSearchRequest( + baseDN, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, + filter[2], + attributes, + []ldap.Control{ldap.NewControlPaging(5)}) + sr, err = l.SearchWithPaging(searchRequest, 5) + if err != nil { + t.Errorf(err.Error()) + return + } + + fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) + + searchRequest = ldap.NewSearchRequest( + baseDN, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, + filter[2], + attributes, + []ldap.Control{ldap.NewControlPaging(500)}) + sr, err = l.SearchWithPaging(searchRequest, 5) + if err == nil { + t.Errorf("expected an error when paging size in control in search request doesn't match size given in call, got none") + return + } } -func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) { - searchRequest := NewSearchRequest( +func searchGoroutine(t *testing.T, l *ldap.Conn, results chan *ldap.SearchResult, i int) { + searchRequest := ldap.NewSearchRequest( baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, filter[i], attributes, nil) @@ -169,17 +197,17 @@ func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) { func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) { fmt.Printf("TestMultiGoroutineSearch: starting...\n") - var l *Conn + var l *ldap.Conn var err error if TLS { - l, err = DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true}) + l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true}) if err != nil { t.Errorf(err.Error()) return } defer l.Close() } else { - l, err = Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return @@ -195,9 +223,9 @@ func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) { } } - results := make([]chan *SearchResult, len(filter)) + results := make([]chan *ldap.SearchResult, len(filter)) for i := range filter { - results[i] = make(chan *SearchResult) + results[i] = make(chan *ldap.SearchResult) go searchGoroutine(t, l, results[i], i) } for i := range filter { @@ -217,17 +245,17 @@ func TestMultiGoroutineSearch(t *testing.T) { } func TestEscapeFilter(t *testing.T) { - if got, want := EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want { + if got, want := ldap.EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want { t.Errorf("Got %s, expected %s", want, got) } - if got, want := EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want { + if got, want := ldap.EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want { t.Errorf("Got %s, expected %s", want, got) } } func TestCompare(t *testing.T) { fmt.Printf("TestCompare: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Fatal(err.Error()) } @@ -243,5 +271,5 @@ func TestCompare(t *testing.T) { return } - fmt.Printf("TestCompare: -> num of entries = %d\n", sr) + fmt.Printf("TestCompare: -> %v\n", sr) } diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/search.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/search.go index 5ae3c449477..23a2cf2b202 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/search.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/search.go @@ -62,6 +62,7 @@ package ldap import ( "errors" "fmt" + "sort" "strings" "gopkg.in/asn1-ber.v1" @@ -93,6 +94,26 @@ var DerefMap = map[int]string{ DerefAlways: "DerefAlways", } +// NewEntry returns an Entry object with the specified distinguished name and attribute key-value pairs. +// The map of attributes is accessed in alphabetical order of the keys in order to ensure that, for the +// same input map of attributes, the output entry will contain the same order of attributes +func NewEntry(dn string, attributes map[string][]string) *Entry { + var attributeNames []string + for attributeName := range attributes { + attributeNames = append(attributeNames, attributeName) + } + sort.Strings(attributeNames) + + var encodedAttributes []*EntryAttribute + for _, attributeName := range attributeNames { + encodedAttributes = append(encodedAttributes, NewEntryAttribute(attributeName, attributes[attributeName])) + } + return &Entry{ + DN: dn, + Attributes: encodedAttributes, + } +} + type Entry struct { DN string Attributes []*EntryAttribute @@ -146,6 +167,19 @@ func (e *Entry) PrettyPrint(indent int) { } } +// NewEntryAttribute returns a new EntryAttribute with the desired key-value pair +func NewEntryAttribute(name string, values []string) *EntryAttribute { + var bytes [][]byte + for _, value := range values { + bytes = append(bytes, []byte(value)) + } + return &EntryAttribute{ + Name: name, + Values: values, + ByteValues: bytes, + } +} + type EntryAttribute struct { Name string Values []string @@ -234,13 +268,32 @@ func NewSearchRequest( } } +// SearchWithPaging accepts a search request and desired page size in order to execute LDAP queries to fulfill the +// search request. All paged LDAP query responses will be buffered and the final result will be returned atomically. +// The following four cases are possible given the arguments: +// - given SearchRequest missing a control of type ControlTypePaging: we will add one with the desired paging size +// - given SearchRequest contains a control of type ControlTypePaging that isn't actually a ControlPaging: fail without issuing any queries +// - given SearchRequest contains a control of type ControlTypePaging with pagingSize equal to the size requested: no change to the search request +// - given SearchRequest contains a control of type ControlTypePaging with pagingSize not equal to the size requested: fail without issuing any queries +// A requested pagingSize of 0 is interpreted as no limit by LDAP servers. func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) { - if searchRequest.Controls == nil { - searchRequest.Controls = make([]Control, 0) + var pagingControl *ControlPaging + + control := FindControl(searchRequest.Controls, ControlTypePaging) + if control == nil { + pagingControl = NewControlPaging(pagingSize) + searchRequest.Controls = append(searchRequest.Controls, pagingControl) + } else { + castControl, ok := control.(*ControlPaging) + if !ok { + return nil, fmt.Errorf("Expected paging control to be of type *ControlPaging, got %v", control) + } + if castControl.PagingSize != pagingSize { + return nil, fmt.Errorf("Paging size given in search request (%d) conflicts with size given in search call (%d)", castControl.PagingSize, pagingSize) + } + pagingControl = castControl } - pagingControl := NewControlPaging(pagingSize) - searchRequest.Controls = append(searchRequest.Controls, pagingControl) searchResult := new(SearchResult) for { result, err := l.Search(searchRequest) diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/search_test.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/search_test.go new file mode 100644 index 00000000000..efb8147d1a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/search_test.go @@ -0,0 +1,31 @@ +package ldap + +import ( + "reflect" + "testing" +) + +// TestNewEntry tests that repeated calls to NewEntry return the same value with the same input +func TestNewEntry(t *testing.T) { + dn := "testDN" + attributes := map[string][]string{ + "alpha": {"value"}, + "beta": {"value"}, + "gamma": {"value"}, + "delta": {"value"}, + "epsilon": {"value"}, + } + exectedEntry := NewEntry(dn, attributes) + + iteration := 0 + for { + if iteration == 100 { + break + } + testEntry := NewEntry(dn, attributes) + if !reflect.DeepEqual(exectedEntry, testEntry) { + t.Fatalf("consequent calls to NewEntry did not yield the same result:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", exectedEntry, testEntry) + } + iteration = iteration + 1 + } +} From 5b5cf9f0061ad9349da786f4a56f817595567489 Mon Sep 17 00:00:00 2001 From: David Warden Date: Thu, 17 Mar 2016 13:38:32 -0400 Subject: [PATCH 024/142] documentation for ldap nested groups --- conf/ldap.toml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/conf/ldap.toml b/conf/ldap.toml index aa8a9679d68..5cc374b83da 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -28,6 +28,23 @@ search_base_dns = ["dc=grafana,dc=org"] # This is done by enabling group_search_filter below. You must also set member_of= "cn" # in [servers.attributes] below. +# Users with nested/recursive group membership must have an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN +# and set group_search_filter below in such a way that it returns the groups the submitted username is a member of. +# +# Nested Groups + Active Directory (AD) Example: +# +# AD groups store the Distinguished Names (DNs) of members, so your filter will need to know the DN +# for the user based only on the submitted username. Multiple DN templates can be +# searched by combining filters with the LDAP or operator. +# +# Some examples: +# group_search_filter = "(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])" +# group_search_filter = "(|(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])(member:1.2.840.113556.1.4.1941:=CN=%s,[another user container/OU]))" +# +# You might also want to change member_of in [servers.attributes] to "distinguishedName". This +# does not appear to be necessary but it will show you more accurate group membership if +# verbose_logging is enabled. + ## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available) # group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))" ## An array of the base DNs to search through for groups. Typically uses ou=groups From 87aca5bf1b3fc142fbe56571f60d77e5f6a9da39 Mon Sep 17 00:00:00 2001 From: David Warden Date: Wed, 23 Mar 2016 08:21:25 -0400 Subject: [PATCH 025/142] new config option for source of %s in group_search_filter, useful for nested LDAP groups --- conf/ldap.toml | 28 +++++++++++++++++----------- pkg/login/ldap.go | 7 ++++++- pkg/login/settings.go | 5 +++-- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/conf/ldap.toml b/conf/ldap.toml index 5cc374b83da..395179e219f 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -28,25 +28,31 @@ search_base_dns = ["dc=grafana,dc=org"] # This is done by enabling group_search_filter below. You must also set member_of= "cn" # in [servers.attributes] below. -# Users with nested/recursive group membership must have an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN -# and set group_search_filter below in such a way that it returns the groups the submitted username is a member of. +# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN +# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of +# below in such a way that the user's recursive group membership is considered. # # Nested Groups + Active Directory (AD) Example: # -# AD groups store the Distinguished Names (DNs) of members, so your filter will need to know the DN -# for the user based only on the submitted username. Multiple DN templates can be -# searched by combining filters with the LDAP or operator. +# AD groups store the Distinguished Names (DNs) of members, so your filter must +# recursively search your groups for the authenticating user's DN. For example: # -# Some examples: -# group_search_filter = "(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])" -# group_search_filter = "(|(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])(member:1.2.840.113556.1.4.1941:=CN=%s,[another user container/OU]))" +# group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)" +# group_search_filter_user_attribute = "distinguishedName" +# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] # -# You might also want to change member_of in [servers.attributes] to "distinguishedName". This -# does not appear to be necessary but it will show you more accurate group membership if -# verbose_logging is enabled. +# [servers.attributes] +# ... +# member_of = "distinguishedName" ## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available) # group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))" +## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter. +## Defaults to the value of username in [server.attributes] +## Valid options are any of your values in [servers.attributes] +## If you are using nested groups you probably want to set this and member_of in +## [servers.attributes] to "distinguishedName" +# group_search_filter_user_attribute = "distinguishedName" ## An array of the base DNs to search through for groups. Typically uses ou=groups # group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index ef611ed358f..48f226ccfa5 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -318,7 +318,12 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) { // If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups var groupSearchResult *ldap.SearchResult for _, groupSearchBase := range a.server.GroupSearchBaseDNs { - filter := strings.Replace(a.server.GroupSearchFilter, "%s", username, -1) + var filter_replace string + filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult) + if a.server.GroupSearchFilterUserAttribute == "" { + filter_replace = getLdapAttr(a.server.Attr.Username, searchResult) + } + filter := strings.Replace(a.server.GroupSearchFilter, "%s", filter_replace, -1) if ldapCfg.VerboseLogging { log.Info("LDAP: Searching for user's groups: %s", filter) diff --git a/pkg/login/settings.go b/pkg/login/settings.go index b181dac3281..a42476157fe 100644 --- a/pkg/login/settings.go +++ b/pkg/login/settings.go @@ -27,8 +27,9 @@ type LdapServerConf struct { SearchFilter string `toml:"search_filter"` SearchBaseDNs []string `toml:"search_base_dns"` - GroupSearchFilter string `toml:"group_search_filter"` - GroupSearchBaseDNs []string `toml:"group_search_base_dns"` + GroupSearchFilter string `toml:"group_search_filter"` + GroupSearchFilterUserAttribute string `toml:"group_search_filter_user_attribute"` + GroupSearchBaseDNs []string `toml:"group_search_base_dns"` LdapGroups []*LdapGroupToOrgRole `toml:"group_mappings"` } From e65fd45c65818c2bf56912cf219cd709dba03537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 21:10:06 +0100 Subject: [PATCH 026/142] ux(): checkbox experiment --- public/sass/components/_switch.scss | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index 74184a4fcde..400ded49a16 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -40,20 +40,35 @@ $switch-height: 1.5rem; color: #fff; font-size: $font-size-sm; text-align: center; - line-height: 2.6rem; + line-height: 2.8rem; + font-size: 150%; } input + label:before { @include buttonBackground($input-bg, lighten($input-bg, 5%)); - content: attr(data-off); - transition: transform 0.5s; + //content: attr(data-off); + font-family: 'FontAwesome'; + //content: "\f00c"; + + //content: "\f096"; // square-o + content: "\f046"; // check-square-o + color: darken($text-color-weak, 17%); + transition: transform 0.4s; backface-visibility: hidden; + text-shadow: 0px 0px 5px rgb(45, 45, 45); } input + label:after { - @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); - content: attr(data-on); - transition: transform 0.5s; + @include buttonBackground($input-bg, lighten($input-bg, 5%)); + //@include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); + //content: attr(data-on); + //content: "\f00c"; + content: "\f046"; // check-square-o + color: #FF8600; + text-shadow: 0px 0px 5px rgb(45, 45, 45); + + font-family: 'FontAwesome'; + transition: transform 0.4s; transform: rotateY(180deg); backface-visibility: hidden; } From c2ef2ef287e55b167c5ed7c279d0242c51867401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 21:18:37 +0100 Subject: [PATCH 027/142] ux(): updated checkbox v3 experiment --- public/sass/components/_switch.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index 400ded49a16..2cc509b25ce 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -50,9 +50,9 @@ $switch-height: 1.5rem; font-family: 'FontAwesome'; //content: "\f00c"; - //content: "\f096"; // square-o - content: "\f046"; // check-square-o - color: darken($text-color-weak, 17%); + content: "\f096"; // square-o + //content: "\f046"; // check-square-o + color: darken($text-color-weak, 10%); transition: transform 0.4s; backface-visibility: hidden; text-shadow: 0px 0px 5px rgb(45, 45, 45); From b3b63f836af939891278c601ae724fe85ade8da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 22:28:38 +0100 Subject: [PATCH 028/142] ux(): checkbox v3 with hover style --- public/sass/components/_switch.scss | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index 2cc509b25ce..c43ef01794d 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -31,6 +31,8 @@ $switch-height: 1.5rem; } input + label:before, input + label:after { + @include buttonBackground($input-bg, $input-bg); + display: block; position: absolute; top: 0; @@ -44,8 +46,14 @@ $switch-height: 1.5rem; font-size: 150%; } + &:hover { + input + label:before, input + label:after { + @include buttonBackground($input-bg, lighten($input-bg, 5%)); + color: lighten(#FF8600, 10%); + } + } + input + label:before { - @include buttonBackground($input-bg, lighten($input-bg, 5%)); //content: attr(data-off); font-family: 'FontAwesome'; //content: "\f00c"; @@ -59,7 +67,6 @@ $switch-height: 1.5rem; } input + label:after { - @include buttonBackground($input-bg, lighten($input-bg, 5%)); //@include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); //content: attr(data-on); //content: "\f00c"; From ee7afcbe9e97e39fe96234302654fa85ee8d4eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 22:40:48 +0100 Subject: [PATCH 029/142] ux() some final tweaks --- public/sass/components/_switch.scss | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index c43ef01794d..fd4a77ca3af 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -47,10 +47,18 @@ $switch-height: 1.5rem; } &:hover { - input + label:before, input + label:after { + input + label:before { + @include buttonBackground($input-bg, lighten($input-bg, 5%)); + color: $text-color; + text-shadow: 1px 1px 4px rgb(45, 45, 45); + } + + input + label:after { @include buttonBackground($input-bg, lighten($input-bg, 5%)); color: lighten(#FF8600, 10%); + text-shadow: 1px 1px 4px rgb(25, 25, 25); } + } input + label:before { @@ -63,7 +71,7 @@ $switch-height: 1.5rem; color: darken($text-color-weak, 10%); transition: transform 0.4s; backface-visibility: hidden; - text-shadow: 0px 0px 5px rgb(45, 45, 45); + text-shadow: 1px 1px 4px rgb(45, 45, 45); } input + label:after { @@ -72,7 +80,7 @@ $switch-height: 1.5rem; //content: "\f00c"; content: "\f046"; // check-square-o color: #FF8600; - text-shadow: 0px 0px 5px rgb(45, 45, 45); + text-shadow: 1px 1px 4px rgb(25, 25, 25); font-family: 'FontAwesome'; transition: transform 0.4s; From 45186255cfd5398f16d34b8a93b798e3b31519f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volker=20Fr=C3=B6hlich?= Date: Thu, 24 Mar 2016 17:58:03 +0100 Subject: [PATCH 030/142] Solve issue 3728 Conditionally add a format element to ES annotation queries --- public/app/plugins/datasource/elasticsearch/datasource.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index b048c206edb..c7c2351d0cc 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -74,6 +74,10 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes to: options.range.to.valueOf(), }; + if (this.esVersion >= 2) { + range[timeField]["format"] = "epoch_millis"; + } + var queryInterpolated = templateSrv.replace(queryString); var filter = { "bool": { "must": [{ "range": range }] } }; var query = { "bool": { "should": [{ "query_string": { "query": queryInterpolated } }] } }; From 70d536c6c470b20f45e86d6ea8c2e3080898abc9 Mon Sep 17 00:00:00 2001 From: Trent White Date: Fri, 25 Mar 2016 11:54:50 -0400 Subject: [PATCH 031/142] add card style css for dashlist panels and remove dark border on dashlist items to match the dropdown dashboard picker list --- public/sass/components/_panel_dashlist.scss | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/public/sass/components/_panel_dashlist.scss b/public/sass/components/_panel_dashlist.scss index f00b59735c1..5e75c73e12d 100644 --- a/public/sass/components/_panel_dashlist.scss +++ b/public/sass/components/_panel_dashlist.scss @@ -1,3 +1,17 @@ +panel-plugin-dashlist .panel-container { + border-radius: 4px; + border:none; + box-shadow: $card-shadow; + .panel-header { + text-align: left !important; + font-size: 1.25rem; + padding: 0 20px 10px 20px; + .panel-title { + font-weight: 400; + } + } +} + .dashlist-item { } @@ -7,7 +21,6 @@ margin: 5px; padding: 7px; background-color: $tight-form-bg; - border: 1px solid $tight-form-border; .fa { float: right; padding-top: 3px; From 90c79432428fb26f03cb88bdf55f254ca95a310e Mon Sep 17 00:00:00 2001 From: Trent White Date: Fri, 25 Mar 2016 12:12:46 -0400 Subject: [PATCH 032/142] backing out card style on dashlist --- public/sass/components/_panel_dashlist.scss | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/public/sass/components/_panel_dashlist.scss b/public/sass/components/_panel_dashlist.scss index 5e75c73e12d..a69d1654e04 100644 --- a/public/sass/components/_panel_dashlist.scss +++ b/public/sass/components/_panel_dashlist.scss @@ -1,17 +1,3 @@ -panel-plugin-dashlist .panel-container { - border-radius: 4px; - border:none; - box-shadow: $card-shadow; - .panel-header { - text-align: left !important; - font-size: 1.25rem; - padding: 0 20px 10px 20px; - .panel-title { - font-weight: 400; - } - } -} - .dashlist-item { } From 5407052e0cdac12acafaf94b5970f151954d1484 Mon Sep 17 00:00:00 2001 From: Matt Toback Date: Fri, 25 Mar 2016 17:41:31 -0400 Subject: [PATCH 033/142] Tightened up the bottom padding on playlist headers - distance to bottom border --- public/sass/pages/_playlist.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/pages/_playlist.scss b/public/sass/pages/_playlist.scss index 412fbcaef50..d0d8f04f130 100644 --- a/public/sass/pages/_playlist.scss +++ b/public/sass/pages/_playlist.scss @@ -118,7 +118,7 @@ .playlist-column-header { border-bottom: thin solid $gray-1; - padding-bottom: 10px; + padding-bottom: 3px; margin-bottom: 15px; } From 4ced5bbbc0808769053f796e5e55251239a9b75e Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Sat, 26 Mar 2016 20:15:36 -0700 Subject: [PATCH 034/142] Fixed some minor typos in docs --- docs/sources/plugins/developing_plugins.md | 2 +- docs/sources/plugins/installation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/plugins/developing_plugins.md b/docs/sources/plugins/developing_plugins.md index def5b825df9..43a99956d55 100644 --- a/docs/sources/plugins/developing_plugins.md +++ b/docs/sources/plugins/developing_plugins.md @@ -42,6 +42,6 @@ This makes it possible to have both built and src content in the same plugin fol ## Boilerplate We currently have three different examples that you can fork/download to get started developing your grafana plugin. - - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for quering json data from backends) + - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for querying json data from backends) - [piechart-panel](https://github.com/grafana/piechart-panel) - [example-app](https://github.com/grafana/example-app) diff --git a/docs/sources/plugins/installation.md b/docs/sources/plugins/installation.md index 7a4ed1889f9..7c60ccca7d2 100644 --- a/docs/sources/plugins/installation.md +++ b/docs/sources/plugins/installation.md @@ -11,7 +11,7 @@ page_keywords: grafana, plugins, documentation The easiest way to install plugins is by using the CLI tool grafana-cli which is bundled with grafana. Before any modification take place after modifying plugins, grafana-server needs to be restarted. ### Grafana plugin directory -On Linux systems the grafana-cli will assume that the grafana plugin directory is "/var/lib/grafana/plugins". It's possible to override the directory which grafana-cli will operate on by specifing the --path flag. On Windows systems this parameter have to be specified for every call. +On Linux systems the grafana-cli will assume that the grafana plugin directory is "/var/lib/grafana/plugins". It's possible to override the directory which grafana-cli will operate on by specifying the --path flag. On Windows systems this parameter have to be specified for every call. ### Grafana-cli commands From 2cb3505dce2bbfadedafa14e7858187ab2650eb8 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Sun, 27 Mar 2016 17:20:33 -0700 Subject: [PATCH 035/142] Examples for listing CloudWatch resources in templates. --- docs/sources/datasources/cloudwatch.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 351b08eb2aa..c69d3579784 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -69,8 +69,22 @@ Name | Description For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html). -The `ec2_instance_attribute` query take `filters` in JSON format. -You can specify [pre-defined filters of ec2:DescribeInstances](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). +## Example templated Queries + +Example dimension queries which will return list of resources for individual AWS Services: + +Service | Query +------- | ----- +EBS | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)` +ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)` +RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)` +RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)` +S3 | `dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)` + +## ec2_instance_attribute JSON filters + +The `ec2_instance_attribute` query take `filters` in JSON format. +You can specify [pre-defined filters of ec2:DescribeInstances](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). Specify like `{ filter_name1: [ filter_value1 ], filter_name2: [ filter_value2 ] }` Example `ec2_instance_attribute()` query From 896605f838aac9566aa3154e35dee42aa89a2ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 28 Mar 2016 10:23:36 +0200 Subject: [PATCH 036/142] ux(): minor fix for row menu in pinned sidemenu state --- public/sass/components/_sidemenu.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index f937ca9c284..685406bb098 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -30,7 +30,7 @@ min-height: calc(100% - 54px); } .dashboard-container { - padding-left: $side-menu-width + 0.2rem; + padding-left: $side-menu-width + 0.5rem; } .page-container { margin-left: $side-menu-width; From 6ab1937ff5ab23766acf2e6d82d9cde24b434e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 28 Mar 2016 11:15:48 +0200 Subject: [PATCH 037/142] changelog(): comment about possible breaking change with templated dashboards --- CHANGELOG.md | 1 + public/sass/_variables.light.scss | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5f8b0d0d0..9959767c087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring an update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info. * **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3523](https://github.com/grafana/grafana/issues/3523) * **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524) +* **Templating**: Templating value formats (glob/regex/pipe etc) are now handled automatically and not specified by the user, this makes variable values possible to reuse in many contexts. It can in some edge cases break existing dashboards that have template variables that do not reload on dashboard load. To fix any issue just go into template variable options and update the variable (so it's values are reloaded.). ### Enhancements * **LDAP**: Support for nested LDAP Groups, closes [#4401](https://github.com/grafana/grafana/issues/4401), [#3808](https://github.com/grafana/grafana/issues/3808) diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 69584df580a..9071472b9b3 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -54,12 +54,12 @@ $critical: #EC2128; // Scaffolding // ------------------------- -$body-bg: $white; -$page-bg: $white; -$body-color: $gray-1; -$text-color: $gray-1; -$text-color-strong: $white; -$text-color-weak: $gray-1; +$body-bg: $white; +$page-bg: $white; +$body-color: $gray-1; +$text-color: $gray-1; +$text-color-strong: $white; +$text-color-weak: $gray-1; $text-color-emphasis: $dark-5; // gradients From 14326b626e7a6e8c80a8a1b529b85f3600050212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 28 Mar 2016 13:10:42 +0200 Subject: [PATCH 038/142] ux(): checkbox now works in dark and white theme --- pkg/api/api.go | 2 + public/app/core/components/switch.ts | 7 +- .../features/dashboard/partials/settings.html | 12 +-- .../features/plugins/partials/ds_edit.html | 89 ++++++++++--------- public/sass/_variables.dark.scss | 15 ++-- public/sass/_variables.light.scss | 4 + public/sass/components/_gf-form.scss | 13 --- public/sass/components/_switch.scss | 25 ++---- 8 files changed, 81 insertions(+), 86 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 06aee594d42..23a65c6a972 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -45,6 +45,8 @@ func Register(r *macaron.Macaron) { r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index) r.Get("/admin/stats", reqGrafanaAdmin, Index) + r.Get("/styleguide", reqSignedIn, Index) + r.Get("/plugins", reqSignedIn, Index) r.Get("/plugins/:id/edit", reqSignedIn, Index) r.Get("/plugins/:id/page/:page", reqSignedIn, Index) diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts index 72e54857e26..dea9c0611cb 100644 --- a/public/app/core/components/switch.ts +++ b/public/app/core/components/switch.ts @@ -7,7 +7,7 @@ import coreModule from 'app/core/core_module'; var template = ` -
    +
    @@ -15,6 +15,11 @@ var template = ` export class SwitchCtrl { onChange: any; + checked: any; + + constructor() { + console.log('property: ' + this.checked, this); + } internalOnChange() { return new Promise(resolve => { diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index 8cf19141e38..3ab3a582e49 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -56,19 +56,19 @@
    Title -
    +
    - - - +
    diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html index 1866fad50a0..6b8ae631a00 100644 --- a/public/app/features/plugins/partials/ds_edit.html +++ b/public/app/features/plugins/partials/ds_edit.html @@ -27,58 +27,59 @@
    -
    - Name - - - The name is used when you select the data source in panels. - The Default data source is preselected in new - panels. - +
    +
    + Name + + + The name is used when you select the data source in panels. + The Default data source is preselected in new + panels. + +
    + +
    - -
    +
    + Type +
    + +
    +
    +
    -
    - Type -
    - -
    -
    -
    + + + + - - - - +
    +
    Testing....
    +
    Test results
    +
    +
    {{ctrl.testing.title}}
    +
    +
    +
    -
    -
    Testing....
    -
    Test results
    -
    -
    {{ctrl.testing.title}}
    -
    -
    -
    - -
    - - - +
    + + + - Cancel -
    + + Cancel +
    - -
    + +
    -
    - -
    +
    + +
    diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 337184b9824..18b2931373d 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -48,14 +48,17 @@ $critical: #ed2e18; // Scaffolding // ------------------------- -$body-bg: rgb(20,20,20); -$page-bg: $dark-2; -$body-color: $gray-4; -$text-color: $gray-4; -$text-color-strong: $white; -$text-color-weak: $gray-2; +$body-bg: rgb(20,20,20); +$page-bg: $dark-2; +$body-color: $gray-4; +$text-color: $gray-4; +$text-color-strong: $white; +$text-color-weak: $gray-2; +$text-color-faint: $gray-1; $text-color-emphasis: $gray-5; +$text-shadow-strong: 1px 1px 4px $black; +$text-shadow-faint: 1px 1px 4px rgb(45, 45, 45); // gradients $brand-gradient: linear-gradient(to right, rgba(255,213,0,0.7) 0%, rgba(255,68,0,0.7) 99%, rgba(255,68,0,0.7) 100%); diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 9071472b9b3..8fef6ae72eb 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -60,8 +60,12 @@ $body-color: $gray-1; $text-color: $gray-1; $text-color-strong: $white; $text-color-weak: $gray-1; +$text-color-faint: $gray-3; $text-color-emphasis: $dark-5; +$text-shadow-strong: none; +$text-shadow-faint: none; + // gradients $brand-gradient: linear-gradient(to right, rgba(255,213,0,1.0) 0%, rgba(255,68,0,1.0) 99%, rgba(255,68,0,1.0) 100%); $page-gradient: linear-gradient(60deg, transparent 70%, darken($page-bg, 4%) 98%); diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index eaf17c6be80..083ef594f6d 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -141,18 +141,10 @@ $gf-form-margin: 0.25rem; } } -<<<<<<< HEAD -||||||| merged common ancestors -.gf-form-select-wrapper + .gf-form-select-wrapper { - margin-left: $gf-form-label-margin; -} - -======= .gf-form--v-stretch { align-items: stretch; } ->>>>>>> master .gf-form-btn { margin-right: $gf-form-margin; padding: $input-padding-y $input-padding-x; @@ -160,15 +152,11 @@ $gf-form-margin: 0.25rem; flex-shrink: 0; flex-grow: 0; } -<<<<<<< HEAD .gf-form-switch { margin-right: $gf-form-margin; } -||||||| merged common ancestors -======= - .natural-language-input { &input[type="number"] { font-size: $font-size-base; @@ -178,4 +166,3 @@ $gf-form-margin: 0.25rem; } } ->>>>>>> master diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index fd4a77ca3af..1f81bc2597e 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -50,40 +50,33 @@ $switch-height: 1.5rem; input + label:before { @include buttonBackground($input-bg, lighten($input-bg, 5%)); color: $text-color; - text-shadow: 1px 1px 4px rgb(45, 45, 45); + text-shadow: $text-shadow-faint; } input + label:after { @include buttonBackground($input-bg, lighten($input-bg, 5%)); - color: lighten(#FF8600, 10%); - text-shadow: 1px 1px 4px rgb(25, 25, 25); + color: lighten($orange, 10%); + text-shadow: $text-shadow-strong; } } input + label:before { - //content: attr(data-off); font-family: 'FontAwesome'; - //content: "\f00c"; - content: "\f096"; // square-o - //content: "\f046"; // check-square-o - color: darken($text-color-weak, 10%); - transition: transform 0.4s; + color: $text-color-faint; + //transition: transform 0.4s; backface-visibility: hidden; - text-shadow: 1px 1px 4px rgb(45, 45, 45); + text-shadow: $text-shadow-faint; } input + label:after { - //@include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); - //content: attr(data-on); - //content: "\f00c"; content: "\f046"; // check-square-o - color: #FF8600; - text-shadow: 1px 1px 4px rgb(25, 25, 25); + color: $orange; + text-shadow: $text-shadow-strong; font-family: 'FontAwesome'; - transition: transform 0.4s; + //transition: transform 0.4s; transform: rotateY(180deg); backface-visibility: hidden; } From 70b66382ebabce0a39da70516ea48f22e496ad60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 28 Mar 2016 17:08:31 +0200 Subject: [PATCH 039/142] tech(): updated angularjs --- bower.json | 8 +- public/app/core/components/switch.ts | 9 +- public/sass/components/_switch.scss | 4 +- public/vendor/angular-mocks/.bower.json | 12 +- public/vendor/angular-mocks/angular-mocks.js | 286 +++++-- public/vendor/angular-mocks/bower.json | 4 +- public/vendor/angular-mocks/package.json | 2 +- public/vendor/angular-route/.bower.json | 12 +- public/vendor/angular-route/angular-route.js | 14 +- .../vendor/angular-route/angular-route.min.js | 16 +- .../angular-route/angular-route.min.js.map | 4 +- public/vendor/angular-route/bower.json | 4 +- public/vendor/angular-route/package.json | 2 +- public/vendor/angular-sanitize/.bower.json | 12 +- .../angular-sanitize/angular-sanitize.js | 2 +- .../angular-sanitize/angular-sanitize.min.js | 2 +- public/vendor/angular-sanitize/bower.json | 4 +- public/vendor/angular-sanitize/package.json | 2 +- public/vendor/angular/.bower.json | 10 +- public/vendor/angular/angular.js | 733 ++++++++++++------ public/vendor/angular/angular.min.js | 596 +++++++------- public/vendor/angular/angular.min.js.gzip | Bin 54457 -> 54974 bytes public/vendor/angular/angular.min.js.map | 6 +- public/vendor/angular/bower.json | 2 +- public/vendor/angular/package.json | 2 +- 25 files changed, 1073 insertions(+), 675 deletions(-) diff --git a/bower.json b/bower.json index 9e0b307c80c..557ee9c9f8f 100644 --- a/bower.json +++ b/bower.json @@ -14,10 +14,10 @@ ], "dependencies": { "jquery": "~2.1.4", - "angular": "~1.5.1", - "angular-route": "~1.5.1", - "angular-mocks": "~1.5.1", - "angular-sanitize": "~1.5.1", + "angular": "~1.5.3", + "angular-route": "~1.5.3", + "angular-mocks": "~1.5.3", + "angular-sanitize": "~1.5.3", "angular-bindonce": "~0.3.3" } } diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts index dea9c0611cb..718837c7600 100644 --- a/public/app/core/components/switch.ts +++ b/public/app/core/components/switch.ts @@ -7,7 +7,7 @@ import coreModule from 'app/core/core_module'; var template = ` -
    +
    @@ -16,9 +16,12 @@ var template = ` export class SwitchCtrl { onChange: any; checked: any; + show: any; - constructor() { - console.log('property: ' + this.checked, this); + constructor($element) { + // hack to workaround animation + // happening on first show + this.show = true; } internalOnChange() { diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index 1f81bc2597e..4216029bab2 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -65,7 +65,7 @@ $switch-height: 1.5rem; font-family: 'FontAwesome'; content: "\f096"; // square-o color: $text-color-faint; - //transition: transform 0.4s; + transition: transform 0.4s; backface-visibility: hidden; text-shadow: $text-shadow-faint; } @@ -76,7 +76,7 @@ $switch-height: 1.5rem; text-shadow: $text-shadow-strong; font-family: 'FontAwesome'; - //transition: transform 0.4s; + transition: transform 0.4s; transform: rotateY(180deg); backface-visibility: hidden; } diff --git a/public/vendor/angular-mocks/.bower.json b/public/vendor/angular-mocks/.bower.json index 9831478983e..03fc650628a 100644 --- a/public/vendor/angular-mocks/.bower.json +++ b/public/vendor/angular-mocks/.bower.json @@ -1,20 +1,20 @@ { "name": "angular-mocks", - "version": "1.5.1-build.4601+sha.c966876", + "version": "1.5.3", "license": "MIT", "main": "./angular-mocks.js", "ignore": [], "dependencies": { - "angular": "1.5.1-build.4601+sha.c966876" + "angular": "1.5.3" }, "homepage": "https://github.com/angular/bower-angular-mocks", - "_release": "1.5.1-build.4601+sha.c966876", + "_release": "1.5.3", "_resolution": { "type": "version", - "tag": "v1.5.1-build.4601+sha.c966876", - "commit": "ff7c5c2ac686293829786d26d844391e45c37c11" + "tag": "v1.5.3", + "commit": "319557fe710cecc11e12c772cc1abb8098d29ccb" }, "_source": "git://github.com/angular/bower-angular-mocks.git", - "_target": "~1.5.1", + "_target": "~1.5.3", "_originalSource": "angular-mocks" } \ No newline at end of file diff --git a/public/vendor/angular-mocks/angular-mocks.js b/public/vendor/angular-mocks/angular-mocks.js index ff28e1a7892..a1fc2fcf7ae 100644 --- a/public/vendor/angular-mocks/angular-mocks.js +++ b/public/vendor/angular-mocks/angular-mocks.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.5.1-build.4601+sha.c966876 + * @license AngularJS v1.5.3 * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ @@ -134,12 +134,12 @@ angular.mock.$Browser = function() { }; angular.mock.$Browser.prototype = { -/** - * @name $browser#poll - * - * @description - * run all fns in pollFns - */ + /** + * @name $browser#poll + * + * @description + * run all fns in pollFns + */ poll: function poll() { angular.forEach(this.pollFns, function(pollFn) { pollFn(); @@ -552,7 +552,7 @@ angular.mock.$IntervalProvider = function() { * This directive should go inside the anonymous function but a bug in JSHint means that it would * not be enacted early enough to prevent the warning. */ -var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; +var R_ISO8061_STR = /^(-?\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; function jsonStringToDate(string) { var match; @@ -578,7 +578,7 @@ function toInt(str) { return parseInt(str, 10); } -function padNumber(num, digits, trim) { +function padNumberInMock(num, digits, trim) { var neg = ''; if (num < 0) { neg = '-'; @@ -727,13 +727,13 @@ angular.mock.TzDate = function(offset, timestamp) { // provide this method only on browsers that already have it if (self.toISOString) { self.toISOString = function() { - return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; + return padNumberInMock(self.origDate.getUTCFullYear(), 4) + '-' + + padNumberInMock(self.origDate.getUTCMonth() + 1, 2) + '-' + + padNumberInMock(self.origDate.getUTCDate(), 2) + 'T' + + padNumberInMock(self.origDate.getUTCHours(), 2) + ':' + + padNumberInMock(self.origDate.getUTCMinutes(), 2) + ':' + + padNumberInMock(self.origDate.getUTCSeconds(), 2) + '.' + + padNumberInMock(self.origDate.getUTCMilliseconds(), 3) + 'Z'; }; } @@ -1328,7 +1328,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { } // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType) { + var xhr = new MockXhr(), expectation = expectations[0], wasExpected = false; @@ -1392,7 +1393,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { // if $browser specified, we do auto flush all requests ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); } else if (definition.passThrough) { - $delegate(method, url, data, callback, headers, timeout, withCredentials); + $delegate(method, url, data, callback, headers, timeout, withCredentials, responseType); } else throw new Error('No response defined !'); return; } @@ -2095,10 +2096,12 @@ angular.mock.$RAFDecorator = ['$delegate', function($delegate) { /** * */ +var originalRootElement; angular.mock.$RootElementProvider = function() { - this.$get = function() { - return angular.element('
    '); - }; + this.$get = ['$injector', function($injector) { + originalRootElement = angular.element('
    ').data('$injector', $injector); + return originalRootElement; + }]; }; /** @@ -2127,7 +2130,7 @@ angular.mock.$RootElementProvider = function() { * * myMod.controller('MyDirectiveController', ['$log', function($log) { * $log.info(this.name); - * })]; + * }]); * * * // In a test ... @@ -2137,7 +2140,7 @@ angular.mock.$RootElementProvider = function() { * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' }); * expect(ctrl.name).toEqual('Clark Kent'); * expect($log.info.logs).toEqual(['Clark Kent']); - * }); + * })); * }); * * ``` @@ -2565,11 +2568,16 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { }]; -if (window.jasmine || window.mocha) { +!(function(jasmineOrMocha) { + + if (!jasmineOrMocha) { + return; + } var currentSpec = null, + injectorState = new InjectorState(), annotatedFunctions = [], - isSpecRunning = function() { + wasInjectorCreated = function() { return !!currentSpec; }; @@ -2581,48 +2589,6 @@ if (window.jasmine || window.mocha) { return angular.mock.$$annotate.apply(this, arguments); }; - - (window.beforeEach || window.setup)(function() { - annotatedFunctions = []; - currentSpec = this; - }); - - (window.afterEach || window.teardown)(function() { - var injector = currentSpec.$injector; - - annotatedFunctions.forEach(function(fn) { - delete fn.$inject; - }); - - angular.forEach(currentSpec.$modules, function(module) { - if (module && module.$$hashKey) { - module.$$hashKey = undefined; - } - }); - - currentSpec.$injector = null; - currentSpec.$modules = null; - currentSpec.$providerInjector = null; - currentSpec = null; - - if (injector) { - injector.get('$rootElement').off(); - injector.get('$rootScope').$destroy(); - } - - // clean up jquery's fragment cache - angular.forEach(angular.element.fragments, function(val, key) { - delete angular.element.fragments[key]; - }); - - MockXhr.$$lastInstance = null; - - angular.forEach(angular.callbacks, function(val, key) { - delete angular.callbacks[key]; - }); - angular.callbacks.counter = 0; - }); - /** * @ngdoc function * @name angular.mock.module @@ -2643,9 +2609,9 @@ if (window.jasmine || window.mocha) { * {@link auto.$provide $provide}.value, the key being the string name (or token) to associate * with the value on the injector. */ - window.module = angular.mock.module = function() { + var module = window.module = angular.mock.module = function() { var moduleFns = Array.prototype.slice.call(arguments, 0); - return isSpecRunning() ? workFn() : workFn; + return wasInjectorCreated() ? workFn() : workFn; ///////////////////// function workFn() { if (currentSpec.$injector) { @@ -2654,11 +2620,11 @@ if (window.jasmine || window.mocha) { var fn, modules = currentSpec.$modules || (currentSpec.$modules = []); angular.forEach(moduleFns, function(module) { if (angular.isObject(module) && !angular.isArray(module)) { - fn = function($provide) { + fn = ['$provide', function($provide) { angular.forEach(module, function(value, key) { $provide.value(key, value); }); - }; + }]; } else { fn = module; } @@ -2672,6 +2638,165 @@ if (window.jasmine || window.mocha) { } }; + module.$$beforeAllHook = (window.before || window.beforeAll); + module.$$afterAllHook = (window.after || window.afterAll); + + // purely for testing ngMock itself + module.$$currentSpec = function(to) { + if (arguments.length === 0) return to; + currentSpec = to; + }; + + /** + * @ngdoc function + * @name angular.mock.module.sharedInjector + * @description + * + * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha + * + * This function ensures a single injector will be used for all tests in a given describe context. + * This contrasts with the default behaviour where a new injector is created per test case. + * + * Use sharedInjector when you want to take advantage of Jasmine's `beforeAll()`, or mocha's + * `before()` methods. Call `module.sharedInjector()` before you setup any other hooks that + * will create (i.e call `module()`) or use (i.e call `inject()`) the injector. + * + * You cannot call `sharedInjector()` from within a context already using `sharedInjector()`. + * + * ## Example + * + * Typically beforeAll is used to make many assertions about a single operation. This can + * cut down test run-time as the test setup doesn't need to be re-run, and enabling focussed + * tests each with a single assertion. + * + * ```js + * describe("Deep Thought", function() { + * + * module.sharedInjector(); + * + * beforeAll(module("UltimateQuestion")); + * + * beforeAll(inject(function(DeepThought) { + * expect(DeepThought.answer).toBeUndefined(); + * DeepThought.generateAnswer(); + * })); + * + * it("has calculated the answer correctly", inject(function(DeepThought) { + * // Because of sharedInjector, we have access to the instance of the DeepThought service + * // that was provided to the beforeAll() hook. Therefore we can test the generated answer + * expect(DeepThought.answer).toBe(42); + * })); + * + * it("has calculated the answer within the expected time", inject(function(DeepThought) { + * expect(DeepThought.runTimeMillennia).toBeLessThan(8000); + * })); + * + * it("has double checked the answer", inject(function(DeepThought) { + * expect(DeepThought.absolutelySureItIsTheRightAnswer).toBe(true); + * })); + * + * }); + * + * ``` + */ + module.sharedInjector = function() { + if (!(module.$$beforeAllHook && module.$$afterAllHook)) { + throw Error("sharedInjector() cannot be used unless your test runner defines beforeAll/afterAll"); + } + + var initialized = false; + + module.$$beforeAllHook(function() { + if (injectorState.shared) { + injectorState.sharedError = Error("sharedInjector() cannot be called inside a context that has already called sharedInjector()"); + throw injectorState.sharedError; + } + initialized = true; + currentSpec = this; + injectorState.shared = true; + }); + + module.$$afterAllHook(function() { + if (initialized) { + injectorState = new InjectorState(); + module.$$cleanup(); + } else { + injectorState.sharedError = null; + } + }); + }; + + module.$$beforeEach = function() { + if (injectorState.shared && currentSpec && currentSpec != this) { + var state = currentSpec; + currentSpec = this; + angular.forEach(["$injector","$modules","$providerInjector", "$injectorStrict"], function(k) { + currentSpec[k] = state[k]; + state[k] = null; + }); + } else { + currentSpec = this; + originalRootElement = null; + annotatedFunctions = []; + } + }; + + module.$$afterEach = function() { + if (injectorState.cleanupAfterEach()) { + module.$$cleanup(); + } + }; + + module.$$cleanup = function() { + var injector = currentSpec.$injector; + + annotatedFunctions.forEach(function(fn) { + delete fn.$inject; + }); + + angular.forEach(currentSpec.$modules, function(module) { + if (module && module.$$hashKey) { + module.$$hashKey = undefined; + } + }); + + currentSpec.$injector = null; + currentSpec.$modules = null; + currentSpec.$providerInjector = null; + currentSpec = null; + + if (injector) { + // Ensure `$rootElement` is instantiated, before checking `originalRootElement` + var $rootElement = injector.get('$rootElement'); + var rootNode = $rootElement && $rootElement[0]; + var cleanUpNodes = !originalRootElement ? [] : [originalRootElement[0]]; + if (rootNode && (!originalRootElement || rootNode !== originalRootElement[0])) { + cleanUpNodes.push(rootNode); + } + angular.element.cleanData(cleanUpNodes); + + // Ensure `$destroy()` is available, before calling it + // (a mocked `$rootScope` might not implement it (or not even be an object at all)) + var $rootScope = injector.get('$rootScope'); + if ($rootScope && $rootScope.$destroy) $rootScope.$destroy(); + } + + // clean up jquery's fragment cache + angular.forEach(angular.element.fragments, function(val, key) { + delete angular.element.fragments[key]; + }); + + MockXhr.$$lastInstance = null; + + angular.forEach(angular.callbacks, function(val, key) { + delete angular.callbacks[key]; + }); + angular.callbacks.counter = 0; + }; + + (window.beforeEach || window.setup)(module.$$beforeEach); + (window.afterEach || window.teardown)(module.$$afterEach); + /** * @ngdoc function * @name angular.mock.inject @@ -2774,14 +2899,14 @@ if (window.jasmine || window.mocha) { window.inject = angular.mock.inject = function() { var blockFns = Array.prototype.slice.call(arguments, 0); var errorForStack = new Error('Declaration Location'); - return isSpecRunning() ? workFn.call(currentSpec) : workFn; + return wasInjectorCreated() ? workFn.call(currentSpec) : workFn; ///////////////////// function workFn() { var modules = currentSpec.$modules || []; var strictDi = !!currentSpec.$injectorStrict; - modules.unshift(function($injector) { + modules.unshift(['$injector', function($injector) { currentSpec.$providerInjector = $injector; - }); + }]); modules.unshift('ngMock'); modules.unshift('ng'); var injector = currentSpec.$injector; @@ -2822,7 +2947,7 @@ if (window.jasmine || window.mocha) { angular.mock.inject.strictDi = function(value) { value = arguments.length ? !!value : true; - return isSpecRunning() ? workFn() : workFn; + return wasInjectorCreated() ? workFn() : workFn; function workFn() { if (value !== currentSpec.$injectorStrict) { @@ -2834,7 +2959,16 @@ if (window.jasmine || window.mocha) { } } }; -} + + function InjectorState() { + this.shared = false; + this.sharedError = null; + + this.cleanupAfterEach = function() { + return !this.shared || this.sharedError; + }; + } +})(window.jasmine || window.mocha); })(window, window.angular); diff --git a/public/vendor/angular-mocks/bower.json b/public/vendor/angular-mocks/bower.json index ac8b75413db..4ce65bbc809 100644 --- a/public/vendor/angular-mocks/bower.json +++ b/public/vendor/angular-mocks/bower.json @@ -1,10 +1,10 @@ { "name": "angular-mocks", - "version": "1.5.1-build.4601+sha.c966876", + "version": "1.5.3", "license": "MIT", "main": "./angular-mocks.js", "ignore": [], "dependencies": { - "angular": "1.5.1-build.4601+sha.c966876" + "angular": "1.5.3" } } diff --git a/public/vendor/angular-mocks/package.json b/public/vendor/angular-mocks/package.json index eda7287688f..42d78038650 100644 --- a/public/vendor/angular-mocks/package.json +++ b/public/vendor/angular-mocks/package.json @@ -1,6 +1,6 @@ { "name": "angular-mocks", - "version": "1.5.1-build.4601+sha.c966876", + "version": "1.5.3", "description": "AngularJS mocks for testing", "main": "angular-mocks.js", "scripts": { diff --git a/public/vendor/angular-route/.bower.json b/public/vendor/angular-route/.bower.json index 43ecb04633b..0abe0215778 100644 --- a/public/vendor/angular-route/.bower.json +++ b/public/vendor/angular-route/.bower.json @@ -1,20 +1,20 @@ { "name": "angular-route", - "version": "1.5.1-build.4601+sha.c966876", + "version": "1.5.3", "license": "MIT", "main": "./angular-route.js", "ignore": [], "dependencies": { - "angular": "1.5.1-build.4601+sha.c966876" + "angular": "1.5.3" }, "homepage": "https://github.com/angular/bower-angular-route", - "_release": "1.5.1-build.4601+sha.c966876", + "_release": "1.5.3", "_resolution": { "type": "version", - "tag": "v1.5.1-build.4601+sha.c966876", - "commit": "967fdabf084ac9f37c6b984d8893ebfebde5fc02" + "tag": "v1.5.3", + "commit": "750e4833612071d30993c8e4a547a6982eba3b84" }, "_source": "git://github.com/angular/bower-angular-route.git", - "_target": "~1.5.1", + "_target": "~1.5.3", "_originalSource": "angular-route" } \ No newline at end of file diff --git a/public/vendor/angular-route/angular-route.js b/public/vendor/angular-route/angular-route.js index cde0787fecf..3c9a4e8595e 100644 --- a/public/vendor/angular-route/angular-route.js +++ b/public/vendor/angular-route/angular-route.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.5.1-build.4601+sha.c966876 + * @license AngularJS v1.5.3 * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ @@ -22,7 +22,11 @@ */ /* global -ngRouteModule */ var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider), + provider('$route', $RouteProvider). + // Ensure `$route` will be instantiated in time to capture the initial + // `$locationChangeSuccess` event. This is necessary in case `ngView` is + // included in an asynchronously loaded template. + run(['$route', angular.noop]), $routeMinErr = angular.$$minErr('ngRoute'); /** @@ -218,9 +222,9 @@ function $RouteProvider() { path = path .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { - var optional = option === '?' ? option : null; - var star = option === '*' ? option : null; + .replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g, function(_, slash, key, option) { + var optional = (option === '?' || option === '*?') ? '?' : null; + var star = (option === '*' || option === '*?') ? '*' : null; keys.push({ name: key, optional: !!optional }); slash = slash || ''; return '' diff --git a/public/vendor/angular-route/angular-route.min.js b/public/vendor/angular-route/angular-route.min.js index d4888631eca..5d2e84f9498 100644 --- a/public/vendor/angular-route/angular-route.min.js +++ b/public/vendor/angular-route/angular-route.min.js @@ -1,15 +1,15 @@ /* - AngularJS v1.5.1-build.4601+sha.c966876 + AngularJS v1.5.3 (c) 2010-2016 Google, Inc. http://angularjs.org License: MIT */ (function(r,d,C){'use strict';function x(s,h,g){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,c,b,f,y){function k(){n&&(g.cancel(n),n=null);l&&(l.$destroy(),l=null);m&&(n=g.leave(m),n.then(function(){n=null}),m=null)}function z(){var b=s.current&&s.current.locals;if(d.isDefined(b&&b.$template)){var b=a.$new(),f=s.current;m=y(b,function(b){g.enter(b,null,m||c).then(function(){!d.isDefined(u)||u&&!a.$eval(u)||h()});k()});l=f.scope=b;l.$emit("$viewContentLoaded"); l.$eval(v)}else k()}var l,m,n,u=b.autoscroll,v=b.onload||"";a.$on("$routeChangeSuccess",z);z()}}}function A(d,h,g){return{restrict:"ECA",priority:-400,link:function(a,c){var b=g.current,f=b.locals;c.html(f.$template);var y=d(c.contents());if(b.controller){f.$scope=a;var k=h(b.controller,f);b.controllerAs&&(a[b.controllerAs]=k);c.data("$ngControllerController",k);c.children().data("$ngControllerController",k)}a[b.resolveAs||"$resolve"]=f;y(a)}}}r=d.module("ngRoute",["ng"]).provider("$route",function(){function s(a, -c){return d.extend(Object.create(a),c)}function h(a,d){var b=d.caseInsensitiveMatch,f={originalPath:a,regexp:a},g=f.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,d,b,c){a="?"===c?c:null;c="*"===c?c:null;g.push({name:b,optional:!!a});d=d||"";return""+(a?"":d)+"(?:"+(a?d:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");f.regexp=new RegExp("^"+a+"$",b?"i":"");return f}var g={};this.when=function(a,c){var b=d.copy(c);d.isUndefined(b.reloadOnSearch)&& -(b.reloadOnSearch=!0);d.isUndefined(b.caseInsensitiveMatch)&&(b.caseInsensitiveMatch=this.caseInsensitiveMatch);g[a]=d.extend(b,a&&h(a,b));if(a){var f="/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";g[f]=d.extend({redirectTo:a},h(f,b))}return this};this.caseInsensitiveMatch=!1;this.otherwise=function(a){"string"===typeof a&&(a={redirectTo:a});this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(a,c,b,f,h,k,r){function l(b){var e= -t.current;(x=(p=n())&&e&&p.$$route===e.$$route&&d.equals(p.pathParams,e.pathParams)&&!p.reloadOnSearch&&!v)||!e&&!p||a.$broadcast("$routeChangeStart",p,e).defaultPrevented&&b&&b.preventDefault()}function m(){var w=t.current,e=p;if(x)w.params=e.params,d.copy(w.params,b),a.$broadcast("$routeUpdate",w);else if(e||w)v=!1,(t.current=e)&&e.redirectTo&&(d.isString(e.redirectTo)?c.path(u(e.redirectTo,e.params)).search(e.params).replace():c.url(e.redirectTo(e.pathParams,c.path(),c.search())).replace()),f.when(e).then(function(){if(e){var a= -d.extend({},e.resolve),b,c;d.forEach(a,function(b,e){a[e]=d.isString(b)?h.get(b):h.invoke(b,null,null,e)});d.isDefined(b=e.template)?d.isFunction(b)&&(b=b(e.params)):d.isDefined(c=e.templateUrl)&&(d.isFunction(c)&&(c=c(e.params)),d.isDefined(c)&&(e.loadedTemplateUrl=r.valueOf(c),b=k(c)));d.isDefined(b)&&(a.$template=b);return f.all(a)}}).then(function(c){e==t.current&&(e&&(e.locals=c,d.copy(e.params,b)),a.$broadcast("$routeChangeSuccess",e,w))},function(b){e==t.current&&a.$broadcast("$routeChangeError", -e,w,b)})}function n(){var a,b;d.forEach(g,function(f,g){var q;if(q=!b){var h=c.path();q=f.keys;var l={};if(f.regexp)if(h=f.regexp.exec(h)){for(var k=1,n=h.length;k + * **Note:** Angular does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline. + * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference). + * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest + * function will be reflected on the scope and in any templates where the object is data-bound. + * To prevent his, transform functions should have no side-effects. + * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return. + *
    + * * ### Default Transformations * * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and @@ -10614,26 +10778,35 @@ function $HttpProvider() { * * ## Caching * - * To enable caching, set the request configuration `cache` property to `true` (to use default - * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). - * When the cache is enabled, `$http` stores the response from the server in the specified - * cache. The next time the same request is made, the response is served from the cache without - * sending a request to the server. + * {@link ng.$http `$http`} responses are not cached by default. To enable caching, you must + * set the config.cache value or the default cache value to TRUE or to a cache object (created + * with {@link ng.$cacheFactory `$cacheFactory`}). If defined, the value of config.cache takes + * precedence over the default cache value. * - * Note that even if the response is served from cache, delivery of the data is asynchronous in - * the same way that real requests are. + * In order to: + * * cache all responses - set the default cache value to TRUE or to a cache object + * * cache a specific response - set config.cache value to TRUE or to a cache object * - * If there are multiple GET requests for the same URL that should be cached using the same - * cache, but the cache is not populated yet, only one request to the server will be made and - * the remaining requests will be fulfilled using the response from the first request. + * If caching is enabled, but neither the default cache nor config.cache are set to a cache object, + * then the default `$cacheFactory($http)` object is used. * - * You can change the default cache to a new object (built with - * {@link ng.$cacheFactory `$cacheFactory`}) by updating the - * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set - * their `cache` property to `true` will now use this cache object. + * The default cache value can be set by updating the + * {@link ng.$http#defaults `$http.defaults.cache`} property or the + * {@link $httpProvider#defaults `$httpProvider.defaults.cache`} property. + * + * When caching is enabled, {@link ng.$http `$http`} stores the response from the server using + * the relevant cache object. The next time the same request is made, the response is returned + * from the cache without sending a request to the server. + * + * Take note that: + * + * * Only GET and JSONP requests are cached. + * * The cache key is the request URL including search parameters; headers are not considered. + * * Cached responses are returned asynchronously, in the same way as responses from the server. + * * If multiple identical requests are made using the same cache, which is not yet populated, + * one request will be made to the server and remaining requests will return the same response. + * * A cache-control header on the response does not affect if or how responses are cached. * - * If you set the default cache to `false` then only requests that specify their own custom - * cache object will be cached. * * ## Interceptors * @@ -10803,7 +10976,7 @@ function $HttpProvider() { * transform function or an array of such functions. The transform function takes the http * response body, headers and status and returns its transformed (typically deserialized) version. * See {@link ng.$http#overriding-the-default-transformations-per-request - * Overriding the Default TransformationjqLiks} + * Overriding the Default Transformations} * - **paramSerializer** - `{string|function(Object):string}` - A function used to * prepare the string representation of request parameters (specified as an object). * If specified as string, it is interpreted as function registered with the @@ -10811,10 +10984,9 @@ function $HttpProvider() { * by registering it as a {@link auto.$provide#service service}. * The default serializer is the {@link $httpParamSerializer $httpParamSerializer}; * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike} - * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. + * - **cache** – `{boolean|Object}` – A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response. + * See {@link $http#caching $http Caching} for more information. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the @@ -13725,8 +13897,10 @@ AST.prototype = { primary = this.arrayDeclaration(); } else if (this.expect('{')) { primary = this.object(); - } else if (this.constants.hasOwnProperty(this.peek().text)) { - primary = copy(this.constants[this.consume().text]); + } else if (this.selfReferential.hasOwnProperty(this.peek().text)) { + primary = copy(this.selfReferential[this.consume().text]); + } else if (this.options.literals.hasOwnProperty(this.peek().text)) { + primary = { type: AST.Literal, value: this.options.literals[this.consume().text]}; } else if (this.peek().identifier) { primary = this.identifier(); } else if (this.peek().constant) { @@ -13878,15 +14052,7 @@ AST.prototype = { return false; }, - - /* `undefined` is not a constant, it is an identifier, - * but using it as an identifier is not supported - */ - constants: { - 'true': { type: AST.Literal, value: true }, - 'false': { type: AST.Literal, value: false }, - 'null': { type: AST.Literal, value: null }, - 'undefined': {type: AST.Literal, value: undefined }, + selfReferential: { 'this': {type: AST.ThisExpression }, '$locals': {type: AST.LocalsExpression } } @@ -14576,7 +14742,7 @@ ASTInterpreter.prototype = { forEach(ast.body, function(expression) { expressions.push(self.recurse(expression.expression)); }); - var fn = ast.body.length === 0 ? function() {} : + var fn = ast.body.length === 0 ? noop : ast.body.length === 1 ? expressions[0] : function(scope, locals) { var lastValue; @@ -14717,7 +14883,7 @@ ASTInterpreter.prototype = { return context ? {value: locals} : locals; }; case AST.NGValueParameter: - return function(scope, locals, assign, inputs) { + return function(scope, locals, assign) { return context ? {value: assign} : assign; }; } @@ -14931,7 +15097,7 @@ var Parser = function(lexer, $filter, options) { this.lexer = lexer; this.$filter = $filter; this.options = options; - this.ast = new AST(this.lexer); + this.ast = new AST(lexer, options); this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : new ASTCompiler(this.ast, $filter); }; @@ -15008,16 +15174,39 @@ function getValueOf(value) { function $ParseProvider() { var cacheDefault = createMap(); var cacheExpensive = createMap(); + var literals = { + 'true': true, + 'false': false, + 'null': null, + 'undefined': undefined + }; + + /** + * @ngdoc method + * @name $parseProvider#addLiteral + * @description + * + * Configure $parse service to add literal values that will be present as literal at expressions. + * + * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name. + * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`. + * + **/ + this.addLiteral = function(literalName, literalValue) { + literals[literalName] = literalValue; + }; this.$get = ['$filter', function($filter) { var noUnsafeEval = csp().noUnsafeEval; var $parseOptions = { csp: noUnsafeEval, - expensiveChecks: false + expensiveChecks: false, + literals: copy(literals) }, $parseOptionsExpensive = { csp: noUnsafeEval, - expensiveChecks: true + expensiveChecks: true, + literals: copy(literals) }; var runningChecksEnabled = false; @@ -15266,15 +15455,15 @@ function $ParseProvider() { * [Kris Kowal's Q](https://github.com/kriskowal/q). * * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred - * implementations, and the other which resembles ES6 promises to some degree. + * implementations, and the other which resembles ES6 (ES2015) promises to some degree. * * # $q constructor * * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` - * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony, + * function as the first argument. This is similar to the native Promise implementation from ES6, * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). * - * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are + * While the constructor-style use is supported, not all of the supporting methods from ES6 promises are * available yet. * * It can be used like so: @@ -16627,7 +16816,7 @@ function $RootScopeProvider() { dirty, ttl = TTL, next, current, target = this, watchLog = [], - logIdx, logMsg, asyncTask; + logIdx, asyncTask; beginPhase('$digest'); // Check for changes to browser url that happened in sync before the call to $digest @@ -18412,6 +18601,10 @@ function $SceProvider() { function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, + // Chrome Packaged Apps are not allowed to access `history.pushState`. They can be detected by + // the presence of `chrome.app.runtime` (see https://developer.chrome.com/apps/api_index) + isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime, + hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, android = toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), @@ -18456,7 +18649,7 @@ function $SnifferProvider() { // so let's not use the history API also // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined // jshint -W018 - history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), + history: !!(hasHistoryPushState && !(android < 4) && !boxee), // jshint +W018 hasEvent: function(event) { // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have @@ -18482,7 +18675,7 @@ function $SnifferProvider() { }]; } -var $compileMinErr = minErr('$compile'); +var $templateRequestMinErr = minErr('$compile'); /** * @ngdoc provider @@ -18578,7 +18771,7 @@ function $TemplateRequestProvider() { function handleError(resp) { if (!ignoreRequestError) { - throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', + throw $templateRequestMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', tpl, resp.status, resp.statusText); } return $q.reject(resp); @@ -19500,7 +19693,7 @@ function currencyFilter($locale) { * Formats a number as text. * * If the input is null or undefined, it will just be returned. - * If the input is infinite (Infinity/-Infinity) the Infinity symbol '∞' is returned. + * If the input is infinite (Infinity or -Infinity), the Infinity symbol '∞' or '-∞' is returned, respectively. * If the input is not a number an empty string is returned. * * @@ -19636,18 +19829,37 @@ function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) { var digit = digits[roundAt]; if (roundAt > 0) { - digits.splice(roundAt); + // Drop fractional digits beyond `roundAt` + digits.splice(Math.max(parsedNumber.i, roundAt)); + + // Set non-fractional digits beyond `roundAt` to 0 + for (var j = roundAt; j < digits.length; j++) { + digits[j] = 0; + } } else { // We rounded to zero so reset the parsedNumber + fractionLen = Math.max(0, fractionLen); parsedNumber.i = 1; - digits.length = roundAt = fractionSize + 1; - for (var i=0; i < roundAt; i++) digits[i] = 0; + digits.length = Math.max(1, roundAt = fractionSize + 1); + digits[0] = 0; + for (var i = 1; i < roundAt; i++) digits[i] = 0; } - if (digit >= 5) digits[roundAt - 1]++; + if (digit >= 5) { + if (roundAt - 1 < 0) { + for (var k = 0; k > roundAt; k--) { + digits.unshift(0); + parsedNumber.i++; + } + digits.unshift(1); + parsedNumber.i++; + } else { + digits[roundAt - 1]++; + } + } // Pad out with zeros to get the required fraction length - for (; fractionLen < fractionSize; fractionLen++) digits.push(0); + for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0); // Do any carrying, e.g. a digit was rounded up to 10 @@ -19719,7 +19931,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { // format the integer digits with grouping separators var groups = []; - if (digits.length > pattern.lgSize) { + if (digits.length >= pattern.lgSize) { groups.unshift(digits.splice(-pattern.lgSize).join('')); } while (digits.length > pattern.gSize) { @@ -19746,11 +19958,15 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { } } -function padNumber(num, digits, trim) { +function padNumber(num, digits, trim, negWrap) { var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; + if (num < 0 || (negWrap && num <= 0)) { + if (negWrap) { + num = -num + 1; + } else { + num = -num; + neg = '-'; + } } num = '' + num; while (num.length < digits) num = ZERO_CHAR + num; @@ -19761,7 +19977,7 @@ function padNumber(num, digits, trim) { } -function dateGetter(name, size, offset, trim) { +function dateGetter(name, size, offset, trim, negWrap) { offset = offset || 0; return function(date) { var value = date['get' + name](); @@ -19769,14 +19985,15 @@ function dateGetter(name, size, offset, trim) { value += offset; } if (value === 0 && offset == -12) value = 12; - return padNumber(value, size, trim); + return padNumber(value, size, trim, negWrap); }; } -function dateStrGetter(name, shortForm) { +function dateStrGetter(name, shortForm, standAlone) { return function(date, formats) { var value = date['get' + name](); - var get = uppercase(shortForm ? ('SHORT' + name) : name); + var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : ''); + var get = uppercase(propPrefix + name); return formats[get][value]; }; @@ -19831,13 +20048,14 @@ function longEraGetter(date, formats) { } var DATE_FORMATS = { - yyyy: dateGetter('FullYear', 4), - yy: dateGetter('FullYear', 2, 0, true), - y: dateGetter('FullYear', 1), + yyyy: dateGetter('FullYear', 4, 0, false, true), + yy: dateGetter('FullYear', 2, 0, true, true), + y: dateGetter('FullYear', 1, 0, false, true), MMMM: dateStrGetter('Month'), MMM: dateStrGetter('Month', true), MM: dateGetter('Month', 2, 1), M: dateGetter('Month', 1, 1), + LLLL: dateStrGetter('Month', false, true), dd: dateGetter('Date', 2), d: dateGetter('Date', 1), HH: dateGetter('Hours', 2), @@ -19863,7 +20081,7 @@ var DATE_FORMATS = { GGGG: longEraGetter }; -var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, +var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, NUMBER_STRING = /^\-?\d+$/; /** @@ -19883,6 +20101,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+| * * `'MMM'`: Month in year (Jan-Dec) * * `'MM'`: Month in year, padded (01-12) * * `'M'`: Month in year (1-12) + * * `'LLLL'`: Stand-alone month in year (January-December) * * `'dd'`: Day in month, padded (01-31) * * `'d'`: Day in month (1-31) * * `'EEEE'`: Day in Week,(Sunday-Saturday) @@ -21564,8 +21783,8 @@ var ngFormDirective = formDirectiveFactory(true); ngModelMinErr: false, */ -// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 -var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; +// Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 +var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/; // See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987) // Note: We are being more lenient, because browsers are too. // 1. Scheme @@ -21581,12 +21800,18 @@ var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0- var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; -var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/; -var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; -var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; -var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; +var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/; +var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; +var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/; +var MONTH_REGEXP = /^(\d{4,})-(\d\d)$/; var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; +var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown'; +var PARTIAL_VALIDATION_TYPES = createMap(); +forEach('date,datetime-local,month,time,week'.split(','), function(type) { + PARTIAL_VALIDATION_TYPES[type] = true; +}); + var inputType = { /** @@ -21942,7 +22167,7 @@ var inputType = { }]);
    - +
    @@ -22663,7 +22888,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { if (!$sniffer.android) { var composing = false; - element.on('compositionstart', function(data) { + element.on('compositionstart', function() { composing = true; }); @@ -22673,6 +22898,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } + var timeout; + var listener = function(ev) { if (timeout) { $browser.defer.cancel(timeout); @@ -22702,8 +22929,6 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { if ($sniffer.hasEvent('input')) { element.on('input', listener); } else { - var timeout; - var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { @@ -22735,6 +22960,26 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // or form autocomplete on newer browser, we need "change" event to catch it element.on('change', listener); + // Some native input types (date-family) have the ability to change validity without + // firing any input/change events. + // For these event types, when native validators are present and the browser supports the type, + // check for validity changes on various DOM events. + if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) { + element.on(PARTIAL_VALIDATION_EVENTS, function(ev) { + if (!timeout) { + var validity = this[VALIDITY_STATE_PROPERTY]; + var origBadInput = validity.badInput; + var origTypeMismatch = validity.typeMismatch; + timeout = $browser.defer(function() { + timeout = null; + if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) { + listener(ev); + } + }); + } + }); + } + ctrl.$render = function() { // Workaround for Firefox validation #12102. var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue; @@ -25061,7 +25306,7 @@ forEach( */ -var ngIfDirective = ['$animate', function($animate) { +var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { return { multiElement: true, transclude: 'element', @@ -25077,7 +25322,7 @@ var ngIfDirective = ['$animate', function($animate) { if (!childScope) { $transclude(function(clone, newScope) { childScope = newScope; - clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); + clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf); // Note: We only need the first/last node of the cloned nodes. // However, we need to keep the reference to the jqlite wrapper as it might be changed later // by a directive with templateUrl when its template arrives. @@ -25876,9 +26121,9 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ }; ngModelSet = function($scope, newValue) { if (isFunction(parsedNgModel($scope))) { - invokeModelSetter($scope, {$$$p: ctrl.$modelValue}); + invokeModelSetter($scope, {$$$p: newValue}); } else { - parsedNgModelAssign($scope, ctrl.$modelValue); + parsedNgModelAssign($scope, newValue); } }; } else if (!parsedNgModel.assign) { @@ -25903,7 +26148,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * the `$viewValue` are different from last time. * * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of - * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue` + * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue` * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be * invoked if you only change a property on the objects. */ @@ -26255,7 +26500,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ setValidity(name, undefined); validatorPromises.push(promise.then(function() { setValidity(name, true); - }, function(error) { + }, function() { allValid = false; setValidity(name, false); })); @@ -26729,7 +26974,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) { }); } - element.on('blur', function(ev) { + element.on('blur', function() { if (modelCtrl.$touched) return; if ($rootScope.$$phase) { @@ -27412,8 +27657,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index]; var value = optionValues[key]; - var locals = getLocals(optionValues[key], key); - var selectValue = getTrackByValueFn(optionValues[key], locals); + var locals = getLocals(value, key); + var selectValue = getTrackByValueFn(value, locals); watchedArray.push(selectValue); // Only need to watch the displayFn if there is a specific label expression @@ -27538,14 +27783,20 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var option = options.getOptionFromViewValue(value); if (option && !option.disabled) { + // Don't update the option when it is already selected. + // For example, the browser will select the first option by default. In that case, + // most properties are set automatically - except the `selected` attribute, which we + // set always + if (selectElement[0].value !== option.selectValue) { removeUnknownOption(); removeEmptyOption(); selectElement[0].value = option.selectValue; option.element.selected = true; - option.element.setAttribute('selected', 'selected'); } + + option.element.setAttribute('selected', 'selected'); } else { if (value === null || providedEmptyOption) { removeUnknownOption(); @@ -28390,7 +28641,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, */ -var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { +var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $animate, $compile) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); @@ -28425,7 +28676,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { $$tlb: true, compile: function ngRepeatCompile($element, $attr) { var expression = $attr.ngRepeat; - var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); + var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression); var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); @@ -29142,7 +29393,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { */ -var ngSwitchDirective = ['$animate', function($animate) { +var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) { return { require: 'ngSwitch', @@ -29183,7 +29434,7 @@ var ngSwitchDirective = ['$animate', function($animate) { selectedTransclude.transclude(function(caseElement, selectedScope) { selectedScopes.push(selectedScope); var anchor = selectedTransclude.element; - caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: '); + caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen'); var block = { clone: caseElement }; selectedElements.push(block); @@ -29477,7 +29728,7 @@ function chromeHack(optionElement) { * added `
    Type -
    - +
    +
    - - + +
    diff --git a/public/sass/components/_drop.scss b/public/sass/components/_drop.scss index 34c53557d45..0af497f46f2 100644 --- a/public/sass/components/_drop.scss +++ b/public/sass/components/_drop.scss @@ -15,11 +15,7 @@ $easing: cubic-bezier(0, 0, 0.265, 1.00); display: block; } - &.drop-open.drop-help.drop-out-of-bounds, - &.drop-open-transitionend.drop-help.drop-out-of-bounds { - display: none; - } -} + } .drop-element, .drop-element * { box-sizing: border-box; @@ -38,6 +34,12 @@ $easing: cubic-bezier(0, 0, 0.265, 1.00); } } +.drop-hide-out-of-bounds { + &.drop-open.drop-help.drop-out-of-bounds, + &.drop-open-transitionend.drop-help.drop-out-of-bounds { + display: none; + } +} @include drop-theme("help", $popover-help-bg, $popover-help-color); @include drop-theme("popover", $popover-bg, $popover-color); diff --git a/public/sass/components/_old_stuff.scss b/public/sass/components/_old_stuff.scss index eae5eb558ce..bb53b657862 100644 --- a/public/sass/components/_old_stuff.scss +++ b/public/sass/components/_old_stuff.scss @@ -1,9 +1,9 @@ -div.editor-row { +.editor-row { vertical-align: top; } -div.editor-row div.section { +.section { margin-right: 20px; vertical-align: top; display: inline-block; From 0c21d2c047d9483487b6c95ec9f4457b7634acb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 Mar 2016 11:07:55 +0200 Subject: [PATCH 051/142] fix(ux): css fix for tabbed view caused edit views in dashboard to create horizontal scrollbar --- public/sass/components/_tabbed_view.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index c72dd1d4388..6c8a42a8059 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -1,7 +1,7 @@ .tabbed-view { background-color: $page-bg; background-image: $page-gradient; - margin: -($panel-margin*2); + margin: (-$panel-margin*2) (-$panel-margin); margin-bottom: $spacer*2; padding: $spacer*3; From 32c8f495ab9741af847192743692c92802a04d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 Mar 2016 12:32:40 +0200 Subject: [PATCH 052/142] ux(): redesign of axis & grid forms --- .../app/core/directives/dropdown_typeahead.js | 114 ++++++++ .../app/plugins/panel/graph/axisEditor.html | 245 +++++++----------- public/sass/components/_gf-form.scss | 18 +- 3 files changed, 228 insertions(+), 149 deletions(-) diff --git a/public/app/core/directives/dropdown_typeahead.js b/public/app/core/directives/dropdown_typeahead.js index ae825c31fca..2dc5111874e 100644 --- a/public/app/core/directives/dropdown_typeahead.js +++ b/public/app/core/directives/dropdown_typeahead.js @@ -119,4 +119,118 @@ function (_, $, coreModule) { } }; }); + + coreModule.default.directive('dropdownTypeahead2', function($compile) { + + var inputTemplate = ''; + + var buttonTemplate = ''; + + return { + scope: { + menuItems: "=dropdownTypeahead2", + dropdownTypeaheadOnSelect: "&dropdownTypeaheadOnSelect", + model: '=ngModel' + }, + link: function($scope, elem, attrs) { + var $input = $(inputTemplate); + var $button = $(buttonTemplate); + $input.appendTo(elem); + $button.appendTo(elem); + + if (attrs.linkText) { + $button.html(attrs.linkText); + } + + if (attrs.ngModel) { + $scope.$watch('model', function(newValue) { + _.each($scope.menuItems, function(item) { + _.each(item.submenu, function(subItem) { + if (subItem.value === newValue) { + $button.html(subItem.text); + } + }); + }); + }); + } + + var typeaheadValues = _.reduce($scope.menuItems, function(memo, value, index) { + if (!value.submenu) { + value.click = 'menuItemSelected(' + index + ')'; + memo.push(value.text); + } else { + _.each(value.submenu, function(item, subIndex) { + item.click = 'menuItemSelected(' + index + ',' + subIndex + ')'; + memo.push(value.text + ' ' + item.text); + }); + } + return memo; + }, []); + + $scope.menuItemSelected = function(index, subIndex) { + var menuItem = $scope.menuItems[index]; + var payload = {$item: menuItem}; + if (menuItem.submenu && subIndex !== void 0) { + payload.$subItem = menuItem.submenu[subIndex]; + } + $scope.dropdownTypeaheadOnSelect(payload); + }; + + $input.attr('data-provide', 'typeahead'); + $input.typeahead({ + source: typeaheadValues, + minLength: 1, + items: 10, + updater: function (value) { + var result = {}; + _.each($scope.menuItems, function(menuItem) { + _.each(menuItem.submenu, function(submenuItem) { + if (value === (menuItem.text + ' ' + submenuItem.text)) { + result.$subItem = submenuItem; + result.$item = menuItem; + } + }); + }); + + if (result.$item) { + $scope.$apply(function() { + $scope.dropdownTypeaheadOnSelect(result); + }); + } + + $input.trigger('blur'); + return ''; + } + }); + + $button.click(function() { + $button.hide(); + $input.show(); + $input.focus(); + }); + + $input.keyup(function() { + elem.toggleClass('open', $input.val() === ''); + }); + + $input.blur(function() { + $input.hide(); + $input.val(''); + $button.show(); + $button.focus(); + // clicking the function dropdown menu wont + // work if you remove class at once + setTimeout(function() { + elem.removeClass('open'); + }, 200); + }); + + $compile(elem.contents())($scope); + } + }; + }); }); diff --git a/public/app/plugins/panel/graph/axisEditor.html b/public/app/plugins/panel/graph/axisEditor.html index d9b7b4599e0..a0ab07e288e 100644 --- a/public/app/plugins/panel/graph/axisEditor.html +++ b/public/app/plugins/panel/graph/axisEditor.html @@ -1,157 +1,106 @@ +
    +
    +
    Left Y
    -
    -
    -
    -
      -
    • - Left Y -
    • -
    • - Unit -
    • - -
    • - Scale type -
    • -
    • - -
    • -
    • - Label -
    • -
    • - -
    • -
    -
    + +
    + +
    -
    -
      -
    • - -
    • -
    • - Y-Max -
    • -
    • - -
    • -
    • - Y-Min -
    • -
    • - -
    • -
    -
    +
    + +
    + +
    -
    -
      -
    • - Right Y -
    • -
    • - Unit -
    • - -
    • - Scale type -
    • -
    • - -
    • -
    • - Label -
    • -
    • - -
    • -
    -
    +
    +
    + + +
    +
    + + +
    -
    -
      -
    • - -
    • -
    • - Y-Max -
    • -
    • - -
    • -
    • - Y-Min -
    • -
    • - -
    • -
    -
    +
    + +
    -
    -
    -
    -
      -
    • - Show Axis -
    • -
    • - X-Axis  - - -
    • -
    • - Y-Axis  - - -
    • -
    -
    +
    +
    Right Y
    + +
    + +
    -
    -
      -
    • - Thresholds -
    • -
    • - Level 1 -
    • -
    • - -
    • -
    • - -
    • -
    • - Level 2 -
    • -
    • - +
      + +
      + +
      +
      +
      +
      + + +
      +
      + + +
      +
      +
      + + +
      +
    +
    + +
    +
    +
      +
    • + Show Axis +
    • +
    • + X-Axis  + + +
    • +
    • + Y-Axis  + + +
    • +
    +
    +
    +
    +
      +
    • + Thresholds +
    • +
    • + Level 1 +
    • +
    • + +
    • +
    • + +
    • +
    • + Level 2 +
    • +
    • +
    • @@ -186,7 +135,7 @@
    • + ng-model="ctrl.panel.legend.sideWidth" ng-change="ctrl.render()" ng-model-onblur>
    @@ -231,7 +180,7 @@
  • + ng-model="ctrl.panel.decimals" ng-change="ctrl.render()" ng-model-onblur>
  • diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 083ef594f6d..12e1fd2f32f 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -112,7 +112,6 @@ $gf-form-margin: 0.25rem; margin-right: $gf-form-margin; position: relative; background-color: $input-bg; - margin-right: $gf-form-margin; select.gf-form-input { text-indent: .01px; @@ -166,3 +165,20 @@ $gf-form-margin: 0.25rem; } } +.gf-form-dropdown-typeahead { + margin-right: $gf-form-margin; + position: relative; + background-color: $input-bg; + padding-right: $input-padding-x; + + &:after { + position: absolute; + top: 35%; + right: $input-padding-x/2; + background-color: transparent; + color: $input-color; + font: normal normal normal $font-size-sm/1 FontAwesome; + content: '\f0d7'; + pointer-events: none; + } +} From 4871a8f43e2244aa47194c125e93f540e042e098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 Mar 2016 14:54:33 +0200 Subject: [PATCH 053/142] refactor(graph): changed how graph stores panel yaxis options --- public/app/features/dashboard/dashboardSrv.js | 45 ++++- .../app/plugins/panel/graph/axisEditor.html | 171 ++++++++---------- public/app/plugins/panel/graph/graph.js | 35 ++-- public/app/plugins/panel/graph/module.ts | 40 ++-- .../plugins/panel/graph/specs/graph_specs.ts | 24 ++- public/test/specs/dashboardSrv-specs.js | 30 ++- 6 files changed, 196 insertions(+), 149 deletions(-) diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index fbd950e60b6..00610624867 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -212,7 +212,7 @@ function (angular, $, _, moment) { var i, j, k; var oldVersion = this.schemaVersion; var panelUpgrades = []; - this.schemaVersion = 11; + this.schemaVersion = 12; if (oldVersion === this.schemaVersion) { return; @@ -409,6 +409,49 @@ function (angular, $, _, moment) { }); } + if (oldVersion < 12) { + // update graph yaxes changes + panelUpgrades.push(function(panel) { + if (panel.type !== 'graph') { return; } + if (!panel.yaxes) { + panel.yaxes = [ + { + show: panel['y-axis'], + min: panel.grid.leftMin, + max: panel.grid.leftMax, + logBase: panel.grid.leftLogBase, + format: panel.y_formats[0], + label: panel.leftYAxisLabel, + }, + { + show: panel['y-axis'], + min: panel.grid.rightMin, + max: panel.grid.rightMax, + logBase: panel.grid.rightLogBase, + format: panel.y_formats[1], + label: panel.rightYAxisLabel, + } + ]; + + panel.xaxis = { + show: panel['x-axis'], + }; + + delete panel.grid.leftMin; + delete panel.grid.leftMax; + delete panel.grid.leftLogBase; + delete panel.grid.rightMin; + delete panel.grid.rightMax; + delete panel.grid.rightLogBase; + delete panel.y_formats; + delete panel.leftYAxisLabel; + delete panel.rightYAxisLabel; + delete panel['y-axis']; + delete panel['x-axis']; + } + }); + } + if (panelUpgrades.length === 0) { return; } diff --git a/public/app/plugins/panel/graph/axisEditor.html b/public/app/plugins/panel/graph/axisEditor.html index a0ab07e288e..184c762943d 100644 --- a/public/app/plugins/panel/graph/axisEditor.html +++ b/public/app/plugins/panel/graph/axisEditor.html @@ -1,116 +1,87 @@
    -
    -
    Left Y
    +
    - -
    - -
    -
    -
    - -
    - +
    Left Y
    +
    Right Y
    + + + +
    +
    + +
    -
    -
    -
    - - + +
    + +
    + +
    -
    - - + +
    +
    + + +
    +
    + + +
    +
    + +
    + +
    -
    -
    - -
    -
    Right Y
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    +
    X-Axis
    +
    -
    -
    -
      -
    • - Show Axis -
    • -
    • - X-Axis  - - -
    • -
    • - Y-Axis  - - -
    • -
    -
    -
    -
    -
      -
    • - Thresholds -
    • -
    • - Level 1 -
    • -
    • - -
    • -
    • - -
    • -
    • - Level 2 -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    -
    +
    +
    Thresholds
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + +
    diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 0f53852cec0..0443a294eed 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -115,7 +115,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { for (var i = 0; i < data.length; i++) { var series = data[i]; var axis = yaxis[series.yaxis - 1]; - var formater = kbn.valueFormats[panel.y_formats[series.yaxis - 1]]; + var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; // decimal override if (_.isNumber(panel.decimals)) { @@ -132,18 +132,18 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } // add left axis labels - if (panel.leftYAxisLabel) { + if (panel.yaxes[0].label) { var yaxisLabel = $("
    ") - .text(panel.leftYAxisLabel) + .text(panel.yaxes[0].label) .appendTo(elem); yaxisLabel.css("margin-top", yaxisLabel.width() / 2); } // add right axis labels - if (panel.rightYAxisLabel) { + if (panel.yaxes[1].label) { var rightLabel = $("
    ") - .text(panel.rightYAxisLabel) + .text(panel.yaxes[1].label) .appendTo(elem); rightLabel.css("margin-top", rightLabel.width() / 2); @@ -151,7 +151,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } function processOffsetHook(plot, gridMargin) { - if (panel.leftYAxisLabel) { gridMargin.left = 20; } + if (panel.yaxis) { gridMargin.left = 20; } if (panel.rightYAxisLabel) { gridMargin.right = 20; } } @@ -217,7 +217,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { for (var i = 0; i < data.length; i++) { var series = data[i]; - series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode, panel.y_formats); + series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); // if hidden remove points and disable stack if (ctrl.hiddenSeries[series.alias]) { @@ -340,11 +340,11 @@ function (angular, $, moment, _, kbn, GraphTooltip) { function configureAxisOptions(data, options) { var defaults = { position: 'left', - show: panel['y-axis'], - min: panel.grid.leftMin, + show: panel.yaxes[0].show, + min: panel.yaxes[0].min, index: 1, - logBase: panel.grid.leftLogBase || 1, - max: panel.percentage && panel.stack ? 100 : panel.grid.leftMax, + logBase: panel.yaxes[0].logBase || 1, + max: panel.percentage && panel.stack ? 100 : panel.yaxes[0].max, }; options.yaxes.push(defaults); @@ -352,18 +352,19 @@ function (angular, $, moment, _, kbn, GraphTooltip) { if (_.findWhere(data, {yaxis: 2})) { var secondY = _.clone(defaults); secondY.index = 2, - secondY.logBase = panel.grid.rightLogBase || 1, + secondY.show = panel.yaxes[1].show; + secondY.logBase = panel.yaxes[1].logBase || 1, secondY.position = 'right'; - secondY.min = panel.grid.rightMin; - secondY.max = panel.percentage && panel.stack ? 100 : panel.grid.rightMax; + secondY.min = panel.yaxes[1].min; + secondY.max = panel.percentage && panel.stack ? 100 : panel.yaxes[1].max; options.yaxes.push(secondY); applyLogScale(options.yaxes[1], data); - configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.y_formats[1]); + configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format); } applyLogScale(options.yaxes[0], data); - configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.y_formats[0]); + configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format); } function applyLogScale(axis, data) { @@ -456,7 +457,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { url += panel['x-axis'] ? '' : '&hideAxes=true'; url += panel['y-axis'] ? '' : '&hideYAxis=true'; - switch(panel.y_formats[0]) { + switch(panel.yaxes[0].format) { case 'bytes': url += '&yUnitSystem=binary'; break; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 29c2dccbac2..43fbddd9b24 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -17,20 +17,28 @@ var panelDefaults = { datasource: null, // sets client side (flot) or native graphite png renderer (png) renderer: 'flot', - // Show/hide the x-axis - 'x-axis' : true, - // Show/hide y-axis - 'y-axis' : true, - // y axis formats, [left axis,right axis] - y_formats : ['short', 'short'], - // grid options + yaxes: [ + { + label: null, + show: true, + logBase: 1, + min: null, + max: null, + format: 'short' + }, + { + label: null, + show: true, + logBase: 1, + min: null, + max: null, + format: 'short' + } + ], + xaxis: { + show: true + }, grid : { - leftLogBase: 1, - leftMax: null, - rightMax: null, - leftMin: null, - rightMin: null, - rightLogBase: 1, threshold1: null, threshold2: null, threshold1Color: 'rgba(216, 200, 27, 0.27)', @@ -115,8 +123,8 @@ class GraphCtrl extends MetricsPanelCtrl { } onInitEditMode() { - this.addEditorTab('Axes & Grid', 'public/app/plugins/panel/graph/axisEditor.html', 2); - this.addEditorTab('Display Styles', 'public/app/plugins/panel/graph/styleEditor.html', 3); + this.addEditorTab('Axes', 'public/app/plugins/panel/graph/axisEditor.html', 2); + this.addEditorTab('Display', 'public/app/plugins/panel/graph/styleEditor.html', 3); this.logScales = { 'linear': 1, @@ -135,7 +143,7 @@ class GraphCtrl extends MetricsPanelCtrl { } setUnitFormat(axis, subItem) { - this.panel.y_formats[axis] = subItem.value; + axis.format = subItem.value; this.render(); } diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 28b38101004..1a708696680 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -31,7 +31,21 @@ describe('grafanaGraph', function() { panel: { legend: {}, grid: { }, - y_formats: [], + yaxes: [ + { + min: null, + max: null, + format: 'short', + logBase: 1 + }, + { + min: null, + max: null, + format: 'short', + logBase: 1 + } + ], + xaxis: {}, seriesOverrides: [], tooltip: { shared: true @@ -151,13 +165,7 @@ describe('grafanaGraph', function() { graphScenario('when logBase is log 10', function(ctx) { ctx.setup(function(ctrl) { - ctrl.panel.grid = { - leftMax: null, - rightMax: null, - leftMin: null, - rightMin: null, - leftLogBase: 10, - }; + ctrl.panel.yaxes[0].logBase = 10; }); it('should apply axis transform and ticks', function() { diff --git a/public/test/specs/dashboardSrv-specs.js b/public/test/specs/dashboardSrv-specs.js index 86f54f274fb..e02495c1236 100644 --- a/public/test/specs/dashboardSrv-specs.js +++ b/public/test/specs/dashboardSrv-specs.js @@ -122,7 +122,10 @@ define([ { panels: [ { - type: 'graphite', legend: true, aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 }, + type: 'graph', legend: true, aliasYAxis: { test: 2 }, + y_formats: ['kbyte', 'ms'], + grid: {min: 1, max: 10, rightMin: 5, rightMax: 15, leftLogBase: 1, rightLogBase: 2}, + leftYAxisLabel: 'left label', targets: [{refId: 'A'}, {}], }, { @@ -172,11 +175,6 @@ define([ expect(graph.legend.show).to.be(true); }); - it('update grid options', function() { - expect(graph.grid.leftMin).to.be(1); - expect(graph.grid.leftMax).to.be(10); - }); - it('move aliasYAxis to series override', function() { expect(graph.seriesOverrides[0].alias).to.be("test"); expect(graph.seriesOverrides[0].yaxis).to.be(2); @@ -193,8 +191,24 @@ define([ expect(table.styles[1].thresholds[1]).to.be("300"); }); + it('graph grid to yaxes options', function() { + expect(graph.yaxes[0].min).to.be(1); + expect(graph.yaxes[0].max).to.be(10); + expect(graph.yaxes[0].format).to.be('kbyte'); + expect(graph.yaxes[0].label).to.be('left label'); + expect(graph.yaxes[0].logBase).to.be(1); + expect(graph.yaxes[1].min).to.be(5); + expect(graph.yaxes[1].max).to.be(15); + expect(graph.yaxes[1].format).to.be('ms'); + expect(graph.yaxes[1].logBase).to.be(2); + + expect(graph.grid.rightMax).to.be(undefined); + expect(graph.grid.rightLogBase).to.be(undefined); + expect(graph.y_formats).to.be(undefined); + }); + it('dashboard schema version should be set to latest', function() { - expect(model.schemaVersion).to.be(11); + expect(model.schemaVersion).to.be(12); }); }); @@ -248,6 +262,8 @@ define([ rows: [{ panels: [{ type: 'graph', + grid: {}, + yaxes: [{}, {}], targets: [{ "alias": "$tag_datacenter $tag_source $col", "column": "value", From d545902acc23e02a5861e4c7c54990f1affda0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 Mar 2016 15:53:41 +0200 Subject: [PATCH 054/142] ux(): updated and polished legend option forms --- public/app/core/components/switch.ts | 10 +- .../app/plugins/panel/graph/axisEditor.html | 160 ------------------ public/app/plugins/panel/graph/module.ts | 5 +- public/app/plugins/panel/graph/tab_axes.html | 70 ++++++++ .../{styleEditor.html => tab_display.html} | 12 +- .../app/plugins/panel/graph/tab_legend.html | 74 ++++++++ 6 files changed, 159 insertions(+), 172 deletions(-) delete mode 100644 public/app/plugins/panel/graph/axisEditor.html create mode 100644 public/app/plugins/panel/graph/tab_axes.html rename public/app/plugins/panel/graph/{styleEditor.html => tab_display.html} (95%) create mode 100644 public/app/plugins/panel/graph/tab_legend.html diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts index e0971c02f9b..1dddae20d49 100644 --- a/public/app/core/components/switch.ts +++ b/public/app/core/components/switch.ts @@ -7,10 +7,10 @@ import coreModule from 'app/core/core_module'; import Drop from 'tether-drop'; var template = ` - +
    - - + +
    `; @@ -18,10 +18,12 @@ export class SwitchCtrl { onChange: any; checked: any; show: any; + id: any; /** @ngInject */ - constructor() { + constructor($scope) { this.show = true; + this.id = $scope.$id; } internalOnChange() { diff --git a/public/app/plugins/panel/graph/axisEditor.html b/public/app/plugins/panel/graph/axisEditor.html deleted file mode 100644 index 184c762943d..00000000000 --- a/public/app/plugins/panel/graph/axisEditor.html +++ /dev/null @@ -1,160 +0,0 @@ -
    -
    - -
    Left Y
    -
    Right Y
    - - - -
    -
    - -
    -
    - -
    - -
    - -
    -
    - -
    -
    - - -
    -
    - - -
    -
    - -
    - - -
    -
    -
    - -
    -
    X-Axis
    - -
    -
    - -
    -
    Thresholds
    -
    -
    - - -
    -
    - -
    - -
    -
    -
    - - -
    -
    - -
    - -
    -
    - - - - - - - - - - - - - - - - -
    -
    - -
    -
    -
    -
      -
    • - Legend -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - Side width -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - Hide series -
    • -
    • - -
    • -
    • - -
    • -
    -
    -
    - -
    -
      -
    • - Values -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - Decimals -
    • -
    • - -
    • -
    -
    -
    -
    -
    diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 43fbddd9b24..c99f9070af3 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -123,8 +123,9 @@ class GraphCtrl extends MetricsPanelCtrl { } onInitEditMode() { - this.addEditorTab('Axes', 'public/app/plugins/panel/graph/axisEditor.html', 2); - this.addEditorTab('Display', 'public/app/plugins/panel/graph/styleEditor.html', 3); + this.addEditorTab('Axes', 'public/app/plugins/panel/graph/tab_axes.html', 2); + this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); + this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); this.logScales = { 'linear': 1, diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html new file mode 100644 index 00000000000..e7e35055d5c --- /dev/null +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -0,0 +1,70 @@ +
    +
    + +
    Left Y
    +
    Right Y
    + + + +
    +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    +
    +
    + +
    +
    X-Axis
    + +
    +
    + +
    +
    Thresholds
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    +
    diff --git a/public/app/plugins/panel/graph/styleEditor.html b/public/app/plugins/panel/graph/tab_display.html similarity index 95% rename from public/app/plugins/panel/graph/styleEditor.html rename to public/app/plugins/panel/graph/tab_display.html index 17a9d702329..5c021dae8e1 100644 --- a/public/app/plugins/panel/graph/styleEditor.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -1,5 +1,5 @@ -
    -
    +
    +
    Draw Modes
    -
    +
    Mode Options
    @@ -39,7 +39,7 @@ checked="ctrl.panel.steppedLine" on-change="ctrl.render()">
    -
    +
    Misc options
    @@ -61,7 +61,7 @@
    -
    +
    Multiple Series
    -
    +
    Series specific overrides Regex match example: /server[0-3]/i
    diff --git a/public/app/plugins/panel/graph/tab_legend.html b/public/app/plugins/panel/graph/tab_legend.html new file mode 100644 index 00000000000..ac9e11dcaf0 --- /dev/null +++ b/public/app/plugins/panel/graph/tab_legend.html @@ -0,0 +1,74 @@ +
    +
    +
    Options
    + + + + + + + +
    + + +
    +
    + +
    +
    Values
    + +
    + + + + + +
    + +
    + + + + + +
    + +
    + + + +
    + + +
    +
    +
    + +
    +
    Hide series
    + + + + +
    +
    From 2f3627e74b261d55f7700c035220a1c1a4030096 Mon Sep 17 00:00:00 2001 From: woodsaj Date: Tue, 29 Mar 2016 22:53:02 +0800 Subject: [PATCH 055/142] add missing ngInject comment. fixes #4501 --- public/app/features/plugins/import_list/import_list.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/plugins/import_list/import_list.ts b/public/app/features/plugins/import_list/import_list.ts index 8eb726b81c0..6c82d3faa92 100644 --- a/public/app/features/plugins/import_list/import_list.ts +++ b/public/app/features/plugins/import_list/import_list.ts @@ -10,6 +10,7 @@ export class DashImportListCtrl { plugin: any; datasource: any; + /** @ngInject */ constructor($scope, private $http, private backendSrv, private $rootScope) { this.dashboards = []; From aa62c2eff63e830eb0f383cabe8ba37e0e439b6c Mon Sep 17 00:00:00 2001 From: Sebastian Borza Date: Tue, 29 Mar 2016 12:36:21 -0500 Subject: [PATCH 056/142] Force the query check to lowercase for tag values, fix issue with templating influxdb v0.11.0 --- public/app/plugins/datasource/influxdb/response_parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts index ae6f2cb75a9..2e33b398a88 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -15,7 +15,7 @@ export default class ResponseParser { var series = influxResults.series[0]; return _.map(series.values, (value) => { if (_.isArray(value)) { - if (query.indexOf('SHOW TAG VALUES') >= 0) { + if (query.toLowerCase().indexOf('show tag values') >= 0) { return { text: (value[1] || value[0]) }; } else { return { text: value[0] }; From 2ee9376df22d6c640f4357047d377c430ae2882d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 Mar 2016 20:49:42 +0200 Subject: [PATCH 057/142] ux(): form tweaks --- public/app/plugins/panel/graph/tab_axes.html | 52 ++++++++++---------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html index e7e35055d5c..81ab3deeb31 100644 --- a/public/app/plugins/panel/graph/tab_axes.html +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -41,30 +41,32 @@
    X-Axis
    -
    -
    -
    Thresholds
    -
    -
    - - -
    -
    - -
    - -
    -
    -
    - - -
    -
    - -
    - -
    -
    -
    +
    +
    Thresholds
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    +
    From 3dbfd494144a7fb65a838219fce0e7802ffc96f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 Mar 2016 22:23:29 +0200 Subject: [PATCH 058/142] ux(): templating forms polish --- public/app/features/dashboard/dashboardSrv.js | 9 +++- .../features/dashboard/submenu/submenu.html | 4 +- public/app/features/templating/editorCtrl.js | 7 +++ .../features/templating/partials/editor.html | 48 +++++++++++-------- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 00610624867..b3194868d97 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -401,11 +401,18 @@ function (angular, $, _, moment) { }); } - if (oldVersion < 11) { + if (oldVersion < 12) { // update template variables _.each(this.templating.list, function(templateVariable) { if (templateVariable.refresh) { templateVariable.refresh = 1; } if (!templateVariable.refresh) { templateVariable.refresh = 0; } + if (templateVariable.hideVariable) { + templateVariable.hide = 2; + } else if (templateVariable.hideLabel) { + templateVariable.hide = 1; + } else { + templateVariable.hide = 0; + } }); } diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index 21a9744b359..464d8c4cecf 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -1,7 +1,7 @@
    -
    -
    -
    Rows settings
    +
    -
    -
    -
    - Title - -
    - +
    +
    Rows settings
    -
    - - - -
    -
    -
    -
    +
    +
    +
    + Title + +
    + -
    - -
    +
    + + + +
    +
    +
    +
    -
    - -
    +
    + +
    -
    -
    Dashboard info
    -
    -
    - Last updated at: - {{formatDate(dashboardMeta.updated)}} -
    -
    - Last updated by: - {{dashboardMeta.updatedBy}}  -
    -
    - Created at: - {{formatDate(dashboardMeta.created)}}  -
    -
    - Created by: - {{dashboardMeta.createdBy}}  -
    -
    - Current version: - {{dashboardMeta.version}}  -
    -
    -
    +
    + +
    + +
    +
    Dashboard info
    +
    +
    + Last updated at: + {{formatDate(dashboardMeta.updated)}} +
    +
    + Last updated by: + {{dashboardMeta.updatedBy}}  +
    +
    + Created at: + {{formatDate(dashboardMeta.created)}}  +
    +
    + Created by: + {{dashboardMeta.createdBy}}  +
    +
    + Current version: + {{dashboardMeta.version}}  +
    +
    +
    diff --git a/public/app/features/profile/partials/profile.html b/public/app/features/profile/partials/profile.html index 042344eeac6..0d6e5fb2951 100644 --- a/public/app/features/profile/partials/profile.html +++ b/public/app/features/profile/partials/profile.html @@ -10,21 +10,25 @@

    Preferences

    - Name + Name
    - Email + Email
    - Username + Username
    - UI Theme + UI Theme
    +
    + Home Dashboard + +
    From 45b90972dc2d02fc95d02b63ce953f947846f78e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 30 Mar 2016 23:33:27 +0200 Subject: [PATCH 068/142] feat(plugins): adds a readme for every native plugin --- public/app/plugins/datasource/cloudwatch/README.md | 7 +++++++ .../app/plugins/datasource/elasticsearch/README.md | 7 +++++++ public/app/plugins/datasource/grafana/README.md | 3 +++ public/app/plugins/datasource/graphite/README.md | 9 +++++++++ public/app/plugins/datasource/influxdb/README.md | 13 +++++++++++++ public/app/plugins/datasource/mixed/README.md | 3 +++ public/app/plugins/datasource/opentsdb/README.md | 7 +++++++ public/app/plugins/datasource/prometheus/README.md | 7 +++++++ public/app/plugins/panel/dashlist/README.md | 9 +++++++++ public/app/plugins/panel/graph/README.md | 7 +++++++ public/app/plugins/panel/singlestat/README.md | 9 +++++++++ public/app/plugins/panel/table/README.md | 9 +++++++++ public/app/plugins/panel/text/README.md | 5 +++++ 13 files changed, 95 insertions(+) create mode 100644 public/app/plugins/datasource/cloudwatch/README.md create mode 100644 public/app/plugins/datasource/elasticsearch/README.md create mode 100644 public/app/plugins/datasource/grafana/README.md create mode 100644 public/app/plugins/datasource/graphite/README.md create mode 100644 public/app/plugins/datasource/influxdb/README.md create mode 100644 public/app/plugins/datasource/mixed/README.md create mode 100644 public/app/plugins/datasource/opentsdb/README.md create mode 100644 public/app/plugins/datasource/prometheus/README.md create mode 100644 public/app/plugins/panel/dashlist/README.md create mode 100644 public/app/plugins/panel/graph/README.md create mode 100644 public/app/plugins/panel/singlestat/README.md create mode 100644 public/app/plugins/panel/table/README.md create mode 100644 public/app/plugins/panel/text/README.md diff --git a/public/app/plugins/datasource/cloudwatch/README.md b/public/app/plugins/datasource/cloudwatch/README.md new file mode 100644 index 00000000000..a1b7bf5cc50 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/README.md @@ -0,0 +1,7 @@ +# CloudWatch Datasource - Native Plugin + +Grafana ships with **built in** support for CloudWatch. You just have to add it as a data source and you will be ready to build dashboards for you CloudWatch metrics. + +Read more about it here: + +[http://docs.grafana.org/datasources/cloudwatch/](http://docs.grafana.org/datasources/cloudwatch/) \ No newline at end of file diff --git a/public/app/plugins/datasource/elasticsearch/README.md b/public/app/plugins/datasource/elasticsearch/README.md new file mode 100644 index 00000000000..21978025eeb --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/README.md @@ -0,0 +1,7 @@ +# CloudWatch Datasource - Native Plugin + +Grafana ships with **advanced support** for Elasticsearch. You can do many types of simple or complex elasticsearch queries to visualize logs or metrics stored in Elasticsearch. You can also annotate your graphs with log events stored in Elasticsearch. + +Read more about it here: + +[http://docs.grafana.org/datasources/elasticsearch/](http://docs.grafana.org/datasources/elasticsearch/) \ No newline at end of file diff --git a/public/app/plugins/datasource/grafana/README.md b/public/app/plugins/datasource/grafana/README.md new file mode 100644 index 00000000000..a8319c8dec5 --- /dev/null +++ b/public/app/plugins/datasource/grafana/README.md @@ -0,0 +1,3 @@ +# Grafana Fake Data Datasource - Native Plugin + +This is the built in Fake Data Datasource that is used before any datasources are set up in your Grafana installation. It means you can create a graph without any data and still get an idea of what it would look like. diff --git a/public/app/plugins/datasource/graphite/README.md b/public/app/plugins/datasource/graphite/README.md new file mode 100644 index 00000000000..c27c5789bca --- /dev/null +++ b/public/app/plugins/datasource/graphite/README.md @@ -0,0 +1,9 @@ +# Graphite Datasource - Native Plugin + +Grafana ships with **built in** support for Graphite (of course!). + +Grafana has an advanced Graphite query editor that lets you quickly navigate the metric space, add functions, change function parameters and much more. The editor can handle all types of graphite queries. It can even handle complex nested queries through the use of query references. + +Read more about it here: + +[http://docs.grafana.org/datasources/graphite/](http://docs.grafana.org/datasources/graphite/) \ No newline at end of file diff --git a/public/app/plugins/datasource/influxdb/README.md b/public/app/plugins/datasource/influxdb/README.md new file mode 100644 index 00000000000..45eaa51eb0f --- /dev/null +++ b/public/app/plugins/datasource/influxdb/README.md @@ -0,0 +1,13 @@ +# InfluxDB Datasource - Native Plugin + +Grafana ships with **built in** support for InfluxDB 0.9. + +There are currently two separate datasources for InfluxDB in Grafana: InfluxDB 0.8.x and InfluxDB 0.9.x. The API and capabilities of InfluxDB 0.9.x are completely different from InfluxDB 0.8.x which is why Grafana handles them as different data sources. + +This is the plugin for InfluxDB 0.9. It is rapidly evolving and we continue to track its API. + +InfluxDB 0.8 is no longer maintained by InfluxDB Inc, but we provide support as a convenience to existing users. You can find it [here](https://www.grafana.net/plugins/grafana-influxdb-08-datasource). + +Read more about InfluxDB here: + +[http://docs.grafana.org/datasources/influxdb/](http://docs.grafana.org/datasources/influxdb/) \ No newline at end of file diff --git a/public/app/plugins/datasource/mixed/README.md b/public/app/plugins/datasource/mixed/README.md new file mode 100644 index 00000000000..b69bb626c15 --- /dev/null +++ b/public/app/plugins/datasource/mixed/README.md @@ -0,0 +1,3 @@ +# Mixed Datasource - Native Plugin + +This is a **built in** datasource that allows you to mix different datasource on the same graph! You can enable this by selecting the built in -- Mixed -- data source. When selected this will allow you to specify data source on a per query basis. This will, for example, allow you to plot metrics from different Graphite servers on the same Graph or plot data from Elasticsearch alongside data from Prometheus. Mixing different data sources on the same graph works for any data source, even custom ones. \ No newline at end of file diff --git a/public/app/plugins/datasource/opentsdb/README.md b/public/app/plugins/datasource/opentsdb/README.md new file mode 100644 index 00000000000..697ecd5c4dc --- /dev/null +++ b/public/app/plugins/datasource/opentsdb/README.md @@ -0,0 +1,7 @@ +# OpenTSDB Datasource - Native Plugin + +Grafana ships with **built in** support for OpenTSDB, a scalable, distributed time series database. + +Read more about it here: + +[http://docs.grafana.org/datasources/opentsdb/](http://docs.grafana.org/datasources/opentsdb/) \ No newline at end of file diff --git a/public/app/plugins/datasource/prometheus/README.md b/public/app/plugins/datasource/prometheus/README.md new file mode 100644 index 00000000000..ee714043157 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/README.md @@ -0,0 +1,7 @@ +# CloudWatch Datasource - Native Plugin + +Grafana ships with **built in** support for Prometheus, the open-source service monitoring system and time series database. + +Read more about it here: + +[http://docs.grafana.org/datasources/prometheus/](http://docs.grafana.org/datasources/prometheus/) \ No newline at end of file diff --git a/public/app/plugins/panel/dashlist/README.md b/public/app/plugins/panel/dashlist/README.md new file mode 100644 index 00000000000..55996b5aff4 --- /dev/null +++ b/public/app/plugins/panel/dashlist/README.md @@ -0,0 +1,9 @@ +# Dashlist Panel - Native Plugin + +This Dashlist panel is **included** with Grafana. + +The dashboard list panel allows you to display dynamic links to other dashboards. The list can be configured to use starred dashboards, a search query and/or dashboard tags. + +Read more about it here: + +[http://docs.grafana.org/reference/dashlist/](http://docs.grafana.org/reference/dashlist/) \ No newline at end of file diff --git a/public/app/plugins/panel/graph/README.md b/public/app/plugins/panel/graph/README.md new file mode 100644 index 00000000000..2dc8682f0e3 --- /dev/null +++ b/public/app/plugins/panel/graph/README.md @@ -0,0 +1,7 @@ +# Graph Panel - Native Plugin + +The Graph is the main graph panel and is **included** with Grafana. It provides a very rich set of graphing options. + +Read more about it here: + +[http://docs.grafana.org/reference/graph/](http://docs.grafana.org/reference/graph/) \ No newline at end of file diff --git a/public/app/plugins/panel/singlestat/README.md b/public/app/plugins/panel/singlestat/README.md new file mode 100644 index 00000000000..42d72825c27 --- /dev/null +++ b/public/app/plugins/panel/singlestat/README.md @@ -0,0 +1,9 @@ +# Singlestat Panel - Native Plugin + +The Singlestat Panel is **included** with Grafana. + +The Singlestat Panel allows you to show the one main summary stat of a SINGLE series. It reduces the series into a single number (by looking at the max, min, average, or sum of values in the series). Singlestat also provides thresholds to color the stat or the Panel background. It can also translate the single number into a text value, and show a sparkline summary of the series. + +Read more about it here: + +[http://docs.grafana.org/reference/singlestat/](http://docs.grafana.org/reference/singlestat/) \ No newline at end of file diff --git a/public/app/plugins/panel/table/README.md b/public/app/plugins/panel/table/README.md new file mode 100644 index 00000000000..48c4fac641b --- /dev/null +++ b/public/app/plugins/panel/table/README.md @@ -0,0 +1,9 @@ +# Table Panel - Native Plugin + +The Table Panel is **included** with Grafana. + +The table panel is very flexible, supporting both multiple modes for time series as well as for table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options. + +Check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase) or read more about it here: + +[http://docs.grafana.org/reference/table_panel/](http://docs.grafana.org/reference/table_panel/) \ No newline at end of file diff --git a/public/app/plugins/panel/text/README.md b/public/app/plugins/panel/text/README.md new file mode 100644 index 00000000000..14751842990 --- /dev/null +++ b/public/app/plugins/panel/text/README.md @@ -0,0 +1,5 @@ +# Text Panel - Native Plugin + +The Text Panel is **included** with Grafana. + +The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more. \ No newline at end of file From b85b5e00d4c3388407649917ee7548db96abbc13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 30 Mar 2016 21:26:54 -0700 Subject: [PATCH 069/142] fix(): fixed panel resize and fullscreen panel height issue --- public/app/features/panel/panel_ctrl.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 2821164bb10..ff2e7fde1bd 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -45,7 +45,7 @@ export class PanelCtrl { } $scope.$on("refresh", () => this.refresh()); - $scope.$on("render", () => this.calculatePanelHeight()); + $scope.$on("render", () => this.render()); } init() { @@ -149,6 +149,7 @@ export class PanelCtrl { return; } + this.calculatePanelHeight(); this.events.emit('render', payload); } From 2bf305b177803117045ac36dfcca2429a92f00a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 30 Mar 2016 22:30:41 -0700 Subject: [PATCH 070/142] build(): updated build version --- package.json | 2 +- packaging/publish/publish.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f2f7af468b5..ad4840144dc 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "3.0.0-pre", + "version": "3.0.0-beta1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" diff --git a/packaging/publish/publish.sh b/packaging/publish/publish.sh index bc644b71042..d918f9a89d8 100755 --- a/packaging/publish/publish.sh +++ b/packaging/publish/publish.sh @@ -1,11 +1,11 @@ #! /usr/bin/env bash -deb_ver=3.0.0-pre1459365183 -rpm_ver=3.0.0-pre1459365183 +deb_ver=3.0.0-pre1459399258 +rpm_ver=3.0.0-pre1459399258 #rpm_ver=3.0.0-1 -# wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb +#wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb # package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb # package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb From eb850ddd365257de75cc53c8b156b7bee7f6a917 Mon Sep 17 00:00:00 2001 From: Pascal Borreli Date: Thu, 31 Mar 2016 11:43:04 +0100 Subject: [PATCH 071/142] doc(plugins): typo --- public/app/plugins/datasource/prometheus/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/README.md b/public/app/plugins/datasource/prometheus/README.md index ee714043157..5b188c3393c 100644 --- a/public/app/plugins/datasource/prometheus/README.md +++ b/public/app/plugins/datasource/prometheus/README.md @@ -1,7 +1,7 @@ -# CloudWatch Datasource - Native Plugin +# Prometheus Datasource - Native Plugin Grafana ships with **built in** support for Prometheus, the open-source service monitoring system and time series database. Read more about it here: -[http://docs.grafana.org/datasources/prometheus/](http://docs.grafana.org/datasources/prometheus/) \ No newline at end of file +[http://docs.grafana.org/datasources/prometheus/](http://docs.grafana.org/datasources/prometheus/) From 601e90f5fe6c631405550b1b24ea59febe2df6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 31 Mar 2016 06:23:52 -0700 Subject: [PATCH 072/142] docs(): updated install instructions --- docs/sources/installation/debian.md | 11 +++++++++-- docs/sources/installation/rpm.md | 23 +++++++++++++++++++++-- docs/sources/installation/windows.md | 2 +- packaging/publish/publish.sh | 6 +++--- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index b5c72f3182c..2d24e7a9c3f 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -10,14 +10,21 @@ page_keywords: grafana, installation, debian, ubuntu, guide Description | Download ------------ | ------------- -.deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb) +Stable .deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb) +Beta .deb for Debian-based Linux | [grafana_3.0.0-beta11459429091_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta11459429091_amd64.deb) -## Install +## Install Stable $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb $ sudo apt-get install -y adduser libfontconfig $ sudo dpkg -i grafana_2.6.0_amd64.deb +## Install 3.0 Beta + + $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta11459429091_amd64.deb + $ sudo apt-get install -y adduser libfontconfig + $ sudo dpkg -i grafana_3.0.0-beta11459429091_amd64.deb + ## APT Repository Add the following line to your `/etc/apt/sources.list` file. diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index ec4f648e44b..5e8e746d0f7 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -10,9 +10,10 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide Description | Download ------------ | ------------- -.RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm) +Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm) +Beta .RPM for CentOS / Fedor / OpenSuse / Redhat Linux | [grafana-3.0.0-beta11459429091.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta11459429091.x86_64.rpm) -## Install from package file +## Install Stable Release from package file You can install Grafana using Yum directly. @@ -29,6 +30,24 @@ Or install manually using `rpm`. $ sudo rpm -i --nodeps grafana-2.6.0-1.x86_64.rpm +## Install Beta Release from package file + +You can install Grafana using Yum directly. + + $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta11459429091.x86_64.rpm + +Or install manually using `rpm`. + +#### On CentOS / Fedora / Redhat: + + $ sudo yum install initscripts fontconfig + $ sudo rpm -Uvh grafana-3.0.0-beta11459429091.x86_64.rpm + +#### On OpenSuse: + + $ sudo rpm -i --nodeps grafana-3.0.0-beta11459429091.x86_64.rpm + + ## Install via YUM Repository Add the following to a new file at `/etc/yum.repos.d/grafana.repo` diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 1bc2b5a2b92..9a1ccbd0a19 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide Description | Download ------------ | ------------- -Zip package for Windows | [grafana.2.5.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.5.0.windows-x64.zip) +Stable Zip package for Windows | [grafana.2.5.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.5.0.windows-x64.zip) ## Configure diff --git a/packaging/publish/publish.sh b/packaging/publish/publish.sh index d918f9a89d8..58021bc6ffa 100755 --- a/packaging/publish/publish.sh +++ b/packaging/publish/publish.sh @@ -1,11 +1,11 @@ #! /usr/bin/env bash -deb_ver=3.0.0-pre1459399258 -rpm_ver=3.0.0-pre1459399258 +deb_ver=3.0.0-beta11459429091 +rpm_ver=3.0.0-beta11459429091 #rpm_ver=3.0.0-1 -#wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb +wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb # package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb # package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb From 5e6c3f314c4678aad3700be36fe6283437640307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 31 Mar 2016 06:47:50 -0700 Subject: [PATCH 073/142] build(): trying to fix windows build on appveyor --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 83506312912..7d84bafc148 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ os: Windows Server 2012 R2 clone_folder: c:\gopath\src\github.com\grafana\grafana environment: - nodejs_version: "4" + nodejs_version: "5" GOPATH: c:\gopath install: From 171f6422b1144960000197ab1d9abca946d8acde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 31 Mar 2016 07:05:44 -0700 Subject: [PATCH 074/142] docs(): corrected version --- docs/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/VERSION b/docs/VERSION index fcdb2e109f6..4a36342fcab 100644 --- a/docs/VERSION +++ b/docs/VERSION @@ -1 +1 @@ -4.0.0 +3.0.0 From 2f2029a5c1d96ea958f9cd1b5a26d31f4ca564c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 Apr 2016 10:56:56 -0700 Subject: [PATCH 075/142] fix(dashboard): fixed dashboard layout for mobile devices, fixes #4529 --- CHANGELOG.md | 7 ++++++- package.json | 2 +- public/sass/_old_responsive.scss | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9959767c087..7e810a7801f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# 3.0.0 (unrelased master branch) +# 3.0.0-beta2 (unreleased) + +### Bug fixes +* **Dashboard**: Fixed dashboard panel layout for mobile devices, fixes [#4529](https://github.com/grafana/grafana/issues/4529) + +# 3.0.0-beta1 (2016-03-31) ### New Features * **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/issues/3655) diff --git a/package.json b/package.json index ad4840144dc..4b526f5a4f3 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "3.0.0-beta1", + "version": "3.0.0-beta2", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" diff --git a/public/sass/_old_responsive.scss b/public/sass/_old_responsive.scss index a7b2917277a..899a785c19b 100644 --- a/public/sass/_old_responsive.scss +++ b/public/sass/_old_responsive.scss @@ -7,8 +7,8 @@ // --------------------- @include media-breakpoint-down(sm) { div.panel { - width: 100%; - padding: 0px; + width: 100% !important; + padding: 0px !important; } .panel-margin { margin-right: 0; From 38a10f8be44a4f84018629925b95d4d9780b9067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 Apr 2016 17:34:30 -0700 Subject: [PATCH 076/142] progress --- pkg/api/api.go | 2 + pkg/api/dashboard.go | 1 - pkg/api/dtos/prefs.go | 11 ++- pkg/api/preferences.go | 35 ++++++++ pkg/services/sqlstore/preferences.go | 3 +- .../app/core/components/dashboard_selector.ts | 18 +++- public/app/core/core.ts | 2 + public/app/core/routes/routes.ts | 1 + public/app/features/all.js | 2 +- .../app/features/org/partials/orgDetails.html | 1 - .../features/profile/partials/profile.html | 40 +++++++-- public/app/features/profile/profileCtrl.js | 51 ----------- public/app/features/profile/profile_ctrl.ts | 87 +++++++++++++++++++ public/vendor/angular/angular.js | 2 +- 14 files changed, 189 insertions(+), 67 deletions(-) delete mode 100644 public/app/features/profile/profileCtrl.js create mode 100644 public/app/features/profile/profile_ctrl.ts diff --git a/pkg/api/api.go b/pkg/api/api.go index 277e5f4373a..b47c632a918 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -100,6 +100,8 @@ func Register(r *macaron.Macaron) { r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard)) r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword)) r.Get("/quotas", wrap(GetUserQuotas)) + r.Get("/preferences", wrap(GetUserPreferences)) + r.Put("/preferences", bind(dtos.UpdateUserPrefsCmd{}), wrap(UpdateUserPreferences)) }) // users (admin permission required) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 1a45ffcd020..fc4aa59fbb0 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -159,7 +159,6 @@ func canEditDashboard(role m.RoleType) bool { } func GetHomeDashboard(c *middleware.Context) { - // Checking if there is any preference set for home dashboard query := m.GetPreferencesQuery{UserId: c.UserId, OrgId: c.OrgId} diff --git a/pkg/api/dtos/prefs.go b/pkg/api/dtos/prefs.go index f66068edccb..77ef0a1da14 100644 --- a/pkg/api/dtos/prefs.go +++ b/pkg/api/dtos/prefs.go @@ -1,6 +1,15 @@ package dtos -type Preferences struct { +type UserPrefs struct { + Theme string `json:"theme"` + ThemeDefault string `json:"themeDefault"` + HomeDashboardId int64 `json:"homeDashboardId"` + HomeDashboardIdDefault int64 `json:"homeDashboardIdDefault"` + Timezone string `json:"timezone"` + TimezoneDefault string `json:"timezoneDefault"` +} + +type UpdateUserPrefsCmd struct { Theme string `json:"theme"` HomeDashboardId int64 `json:"homeDashboardId"` Timezone string `json:"timezone"` diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index 490da451afc..ed51ddaa9ae 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -1,6 +1,7 @@ package api import ( + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -18,3 +19,37 @@ func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Respo return ApiSuccess("Home dashboard set") } + +// GET /api/user/preferences +func GetUserPreferences(c *middleware.Context) Response { + userPrefs := m.GetPreferencesQuery{UserId: c.UserId, OrgId: c.OrgId} + + if err := bus.Dispatch(&userPrefs); err != nil { + c.JsonApiErr(500, "Failed to get preferences", err) + } + + dto := dtos.UserPrefs{ + Theme: userPrefs.Result.Theme, + HomeDashboardId: userPrefs.Result.HomeDashboardId, + Timezone: userPrefs.Result.Timezone, + } + + return Json(200, &dto) +} + +// PUT /api/user/preferences +func UpdateUserPreferences(c *middleware.Context, dtoCmd dtos.UpdateUserPrefsCmd) Response { + saveCmd := m.SavePreferencesCommand{ + UserId: c.UserId, + OrgId: c.OrgId, + Theme: dtoCmd.Theme, + Timezone: dtoCmd.Timezone, + HomeDashboardId: dtoCmd.HomeDashboardId, + } + + if err := bus.Dispatch(&saveCmd); err != nil { + c.JsonApiErr(500, "Failed to save preferences", err) + } + + return ApiSuccess("User preferences updated") +} diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index 8882495329f..1b5fa79d879 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -1,9 +1,10 @@ package sqlstore import ( + "time" + "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "time" ) func init() { diff --git a/public/app/core/components/dashboard_selector.ts b/public/app/core/components/dashboard_selector.ts index ada4866f98e..913a43911fa 100644 --- a/public/app/core/components/dashboard_selector.ts +++ b/public/app/core/components/dashboard_selector.ts @@ -6,12 +6,25 @@ import $ from 'jquery'; import coreModule from 'app/core/core_module'; var template = ` + `; export class DashboardSelectorCtrl { + model: any; + options: any; /** @ngInject */ - constructor(private $scope, private $rootScope) { + constructor(private backendSrv) { + } + + $onInit() { + this.options = [{value: 0, text: 'Default'}]; + + return this.backendSrv.search({starred: true}).then(res => { + res.forEach(dash => { + this.options.push({value: dash.id, text: dash.title}); + }); + }); } } @@ -22,6 +35,9 @@ export function dashboardSelector() { bindToController: true, controllerAs: 'ctrl', template: template, + scope: { + model: '=' + } }; } diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 279f54d4e45..abebb5ce560 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -32,6 +32,7 @@ import {liveSrv} from './live/live_srv'; import {Emitter} from './utils/emitter'; import {layoutSelector} from './components/layout_selector/layout_selector'; import {switchDirective} from './components/switch'; +import {dashboardSelector} from './components/dashboard_selector'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -54,4 +55,5 @@ export { infoPopover, Emitter, appEvents, + dashboardSelector, }; diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 92cf32448a1..ddc0ac06250 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -90,6 +90,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) { .when('/profile', { templateUrl: 'public/app/features/profile/partials/profile.html', controller : 'ProfileCtrl', + controllerAs: 'ctrl', }) .when('/profile/password', { templateUrl: 'public/app/features/profile/partials/password.html', diff --git a/public/app/features/all.js b/public/app/features/all.js index c110bcff7cd..19ca8d7e2c9 100644 --- a/public/app/features/all.js +++ b/public/app/features/all.js @@ -7,7 +7,7 @@ define([ './playlist/all', './snapshot/all', './panel/all', - './profile/profileCtrl', + './profile/profile_ctrl', './profile/changePasswordCtrl', './profile/selectOrgCtrl', './styleguide/styleguide', diff --git a/public/app/features/org/partials/orgDetails.html b/public/app/features/org/partials/orgDetails.html index fe988570e50..b3ba0f73d83 100644 --- a/public/app/features/org/partials/orgDetails.html +++ b/public/app/features/org/partials/orgDetails.html @@ -19,7 +19,6 @@
    -

    Address

    diff --git a/public/app/features/profile/partials/profile.html b/public/app/features/profile/partials/profile.html index 0d6e5fb2951..e899b017ea6 100644 --- a/public/app/features/profile/partials/profile.html +++ b/public/app/features/profile/partials/profile.html @@ -6,35 +6,57 @@

    Profile

    - -

    Preferences

    + +

    Information

    Name - +
    Email - +
    Username - +
    +
    + +
    + + +
    +

    Preferences

    +
    UI Theme - +
    + +
    +
    Home Dashboard - + + +
    + +
    + +
    + +
    - +
    +

    Password

    Change Password @@ -51,7 +73,7 @@ - + {{org.name}} {{org.role}} diff --git a/public/app/features/profile/profileCtrl.js b/public/app/features/profile/profileCtrl.js deleted file mode 100644 index bae85a3d0c4..00000000000 --- a/public/app/features/profile/profileCtrl.js +++ /dev/null @@ -1,51 +0,0 @@ -define([ - 'angular', - 'app/core/config', -], -function (angular, config) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('ProfileCtrl', function($scope, backendSrv, contextSrv, $location) { - - $scope.init = function() { - $scope.getUser(); - $scope.getUserOrgs(); - }; - - $scope.getUser = function() { - backendSrv.get('/api/user').then(function(user) { - $scope.user = user; - $scope.user.theme = user.theme || 'dark'; - $scope.old_theme = $scope.user.theme; - }); - }; - - $scope.getUserOrgs = function() { - backendSrv.get('/api/user/orgs').then(function(orgs) { - $scope.orgs = orgs; - }); - }; - - $scope.setUsingOrg = function(org) { - backendSrv.post('/api/user/using/' + org.orgId).then(function() { - window.location.href = config.appSubUrl + '/profile'; - }); - }; - - $scope.update = function() { - if (!$scope.userForm.$valid) { return; } - - backendSrv.put('/api/user/', $scope.user).then(function() { - contextSrv.user.name = $scope.user.name || $scope.user.login; - if ($scope.old_theme !== $scope.user.theme) { - window.location.href = config.appSubUrl + $location.path(); - } - }); - }; - - $scope.init(); - - }); -}); diff --git a/public/app/features/profile/profile_ctrl.ts b/public/app/features/profile/profile_ctrl.ts new file mode 100644 index 00000000000..7b37f5484be --- /dev/null +++ b/public/app/features/profile/profile_ctrl.ts @@ -0,0 +1,87 @@ +/// + +import config from 'app/core/config'; +import {coreModule} from 'app/core/core'; +import _ from 'lodash'; + +export class ProfileCtrl { + user: any; + old_theme: any; + orgs: any; + prefs: any; + userForm: any; + prefsForm: any; + + timezones: any = [ + {value: '', text: 'Default'}, + {value: 'browser', text: 'Local browser time'}, + {value: 'utc', text: 'UTC'}, + ]; + themes: any = [ + {value: '', text: 'Default'}, + {value: 'dark', text: 'Dark'}, + {value: 'light', text: 'Light'}, + ]; + + /** @ngInject **/ + constructor(private $scope, private backendSrv, private contextSrv, private $location) { + this.getUser(); + this.getUserOrgs(); + this.getUserPrefs(); + } + + getUser() { + this.backendSrv.get('/api/user').then(user => { + this.user = user; + this.user.theme = user.theme || 'dark'; + }); + } + + getUserPrefs() { + this.backendSrv.get('/api/user/preferences').then(prefs => { + this.prefs = prefs; + this.old_theme = prefs.theme; + }); + } + + 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 = config.appSubUrl + '/profile'; + }); + } + + update() { + if (!this.userForm.$valid) { return; } + + this.backendSrv.put('/api/user/', this.user).then(() => { + this.contextSrv.user.name = this.user.name || this.user.login; + if (this.old_theme !== this.user.theme) { + window.location.href = config.appSubUrl + this.$location.path(); + } + }); + } + + updatePrefs() { + if (!this.prefsForm.$valid) { return; } + + var cmd = { + theme: this.prefs.theme, + timezone: this.prefs.timezone, + homeDashboardId: this.prefs.homeDashboardId + }; + + this.backendSrv.put('/api/user/preferences', cmd).then(() => { + if (this.old_theme !== cmd.theme) { + window.location.href = config.appSubUrl + this.$location.path(); + } + }); + } +} + +coreModule.controller('ProfileCtrl', ProfileCtrl); diff --git a/public/vendor/angular/angular.js b/public/vendor/angular/angular.js index 5e6fb2c363d..9ef1f59c0b7 100644 --- a/public/vendor/angular/angular.js +++ b/public/vendor/angular/angular.js @@ -30711,4 +30711,4 @@ $provide.value("$locale", { })(window, document); -!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend(''); \ No newline at end of file +!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend(''); From ab1048b7ee7fd13c3e596f0b49b38d17b03c211f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 Apr 2016 13:54:06 -0700 Subject: [PATCH 077/142] feat(preferences): theme and home dashbord settings now work work on profile and org settings page --- pkg/api/api.go | 8 +- pkg/api/dashboard.go | 14 ++- pkg/api/dtos/models.go | 1 + pkg/api/dtos/prefs.go | 13 +-- pkg/api/index.go | 10 +- pkg/api/preferences.go | 42 +++++--- pkg/models/preferences.go | 8 ++ pkg/models/user.go | 1 - pkg/services/sqlstore/preferences.go | 37 ++++++- pkg/services/sqlstore/user.go | 1 - public/app/core/routes/routes.ts | 9 +- public/app/features/all.js | 3 - public/app/features/org/all.js | 4 + .../change_password_ctrl.js} | 0 .../partials/change_password.html} | 0 .../app/features/org/partials/orgDetails.html | 2 + .../{profile => org}/partials/profile.html | 31 +----- .../{profile => org}/partials/select_org.html | 0 public/app/features/org/prefs_control.ts | 100 ++++++++++++++++++ .../features/{profile => org}/profile_ctrl.ts | 38 +------ .../select_org_ctrl.js} | 0 21 files changed, 216 insertions(+), 106 deletions(-) rename public/app/features/{profile/changePasswordCtrl.js => org/change_password_ctrl.js} (100%) rename public/app/features/{profile/partials/password.html => org/partials/change_password.html} (100%) rename public/app/features/{profile => org}/partials/profile.html (62%) rename public/app/features/{profile => org}/partials/select_org.html (100%) create mode 100644 public/app/features/org/prefs_control.ts rename public/app/features/{profile => org}/profile_ctrl.ts (53%) rename public/app/features/{profile/selectOrgCtrl.js => org/select_org_ctrl.js} (100%) diff --git a/pkg/api/api.go b/pkg/api/api.go index b47c632a918..5ae51b58904 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -96,12 +96,15 @@ func Register(r *macaron.Macaron) { r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser)) r.Post("/using/:id", wrap(UserSetUsingOrg)) r.Get("/orgs", wrap(GetSignedInUserOrgList)) + r.Post("/stars/dashboard/:id", wrap(StarDashboard)) r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard)) + r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword)) r.Get("/quotas", wrap(GetUserQuotas)) + r.Get("/preferences", wrap(GetUserPreferences)) - r.Put("/preferences", bind(dtos.UpdateUserPrefsCmd{}), wrap(UpdateUserPreferences)) + r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences)) }) // users (admin permission required) @@ -132,6 +135,9 @@ func Register(r *macaron.Macaron) { r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite)) r.Patch("/invites/:code/revoke", wrap(RevokeInvite)) + // prefs + r.Get("/preferences", wrap(GetOrgPreferences)) + r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences)) }, reqOrgAdmin) // create new org diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index fc4aa59fbb0..b55a1377bd8 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -159,22 +159,20 @@ func canEditDashboard(role m.RoleType) bool { } func GetHomeDashboard(c *middleware.Context) { - // Checking if there is any preference set for home dashboard - query := m.GetPreferencesQuery{UserId: c.UserId, OrgId: c.OrgId} - - if err := bus.Dispatch(&query); err != nil { + prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId} + if err := bus.Dispatch(&prefsQuery); err != nil { c.JsonApiErr(500, "Failed to get preferences", err) } - if query.Result.HomeDashboardId != 0 { - query := m.GetDashboardSlugByIdQuery{Id: query.Result.HomeDashboardId} - err := bus.Dispatch(&query) + if prefsQuery.Result.HomeDashboardId != 0 { + slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId} + err := bus.Dispatch(&slugQuery) if err != nil { c.JsonApiErr(500, "Failed to get slug from database", err) return } - dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + query.Result} + dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result} c.JSON(200, &dashRedirect) return } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index a95bd464f35..8db36be2140 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -33,6 +33,7 @@ type CurrentUser struct { OrgRole m.RoleType `json:"orgRole"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` GravatarUrl string `json:"gravatarUrl"` + Timezone string `json:"timezone"` } type DashboardMeta struct { diff --git a/pkg/api/dtos/prefs.go b/pkg/api/dtos/prefs.go index 77ef0a1da14..97e20bb4011 100644 --- a/pkg/api/dtos/prefs.go +++ b/pkg/api/dtos/prefs.go @@ -1,15 +1,12 @@ package dtos -type UserPrefs struct { - Theme string `json:"theme"` - ThemeDefault string `json:"themeDefault"` - HomeDashboardId int64 `json:"homeDashboardId"` - HomeDashboardIdDefault int64 `json:"homeDashboardIdDefault"` - Timezone string `json:"timezone"` - TimezoneDefault string `json:"timezoneDefault"` +type Prefs struct { + Theme string `json:"theme"` + HomeDashboardId int64 `json:"homeDashboardId"` + Timezone string `json:"timezone"` } -type UpdateUserPrefsCmd struct { +type UpdatePrefsCmd struct { Theme string `json:"theme"` HomeDashboardId int64 `json:"homeDashboardId"` Timezone string `json:"timezone"` diff --git a/pkg/api/index.go b/pkg/api/index.go index a376f9504a4..575ea35cfaf 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" @@ -14,6 +15,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { return nil, err } + prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId} + if err := bus.Dispatch(&prefsQuery); err != nil { + return nil, err + } + prefs := prefsQuery.Result + var data = dtos.IndexViewData{ User: &dtos.CurrentUser{ Id: c.UserId, @@ -21,12 +28,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Login: c.Login, Email: c.Email, Name: c.Name, - LightTheme: c.Theme == "light", OrgId: c.OrgId, OrgName: c.OrgName, OrgRole: c.OrgRole, GravatarUrl: dtos.GetGravatarUrl(c.Email), IsGrafanaAdmin: c.IsGrafanaAdmin, + LightTheme: prefs.Theme == "light", + Timezone: prefs.Timezone, }, Settings: settings, AppUrl: setting.AppUrl, diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index ed51ddaa9ae..795b8994470 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -22,34 +22,52 @@ func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Respo // GET /api/user/preferences func GetUserPreferences(c *middleware.Context) Response { - userPrefs := m.GetPreferencesQuery{UserId: c.UserId, OrgId: c.OrgId} + return getPreferencesFor(c.OrgId, c.UserId) +} - if err := bus.Dispatch(&userPrefs); err != nil { - c.JsonApiErr(500, "Failed to get preferences", err) +func getPreferencesFor(orgId int64, userId int64) Response { + prefsQuery := m.GetPreferencesQuery{UserId: userId, OrgId: orgId} + + if err := bus.Dispatch(&prefsQuery); err != nil { + return ApiError(500, "Failed to get preferences", err) } - dto := dtos.UserPrefs{ - Theme: userPrefs.Result.Theme, - HomeDashboardId: userPrefs.Result.HomeDashboardId, - Timezone: userPrefs.Result.Timezone, + dto := dtos.Prefs{ + Theme: prefsQuery.Result.Theme, + HomeDashboardId: prefsQuery.Result.HomeDashboardId, + Timezone: prefsQuery.Result.Timezone, } return Json(200, &dto) } // PUT /api/user/preferences -func UpdateUserPreferences(c *middleware.Context, dtoCmd dtos.UpdateUserPrefsCmd) Response { +func UpdateUserPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response { + return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd) +} + +func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd) Response { saveCmd := m.SavePreferencesCommand{ - UserId: c.UserId, - OrgId: c.OrgId, + UserId: userId, + OrgId: orgId, Theme: dtoCmd.Theme, Timezone: dtoCmd.Timezone, HomeDashboardId: dtoCmd.HomeDashboardId, } if err := bus.Dispatch(&saveCmd); err != nil { - c.JsonApiErr(500, "Failed to save preferences", err) + return ApiError(500, "Failed to save preferences", err) } - return ApiSuccess("User preferences updated") + return ApiSuccess("Preferences updated") +} + +// GET /api/org/preferences +func GetOrgPreferences(c *middleware.Context) Response { + return getPreferencesFor(c.OrgId, 0) +} + +// PUT /api/org/preferences +func UpdateOrgPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response { + return updatePreferencesFor(c.OrgId, 0, &dtoCmd) } diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go index 523a3bfc83f..4c77bc96d4d 100644 --- a/pkg/models/preferences.go +++ b/pkg/models/preferences.go @@ -33,6 +33,14 @@ type GetPreferencesQuery struct { Result *Preferences } +type GetPreferencesWithDefaultsQuery struct { + Id int64 + OrgId int64 + UserId int64 + + Result *Preferences +} + // --------------------- // COMMANDS type SavePreferencesCommand struct { diff --git a/pkg/models/user.go b/pkg/models/user.go index 2842bad490d..a231156b7b0 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -136,7 +136,6 @@ type SignedInUser struct { Login string Name string Email string - Theme string ApiKeyId int64 IsGrafanaAdmin bool } diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index 1b5fa79d879..d120c485ed3 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -9,9 +9,44 @@ import ( func init() { bus.AddHandler("sql", GetPreferences) + bus.AddHandler("sql", GetPreferencesWithDefaults) bus.AddHandler("sql", SavePreferences) } +func GetPreferencesWithDefaults(query *m.GetPreferencesWithDefaultsQuery) error { + + prefs := make([]*m.Preferences, 0) + filter := "(org_id=? AND user_id=?) OR (org_id=? AND user_id=0)" + err := x.Where(filter, query.OrgId, query.UserId, query.OrgId). + OrderBy("user_id ASC"). + Find(&prefs) + + if err != nil { + return err + } + + res := &m.Preferences{ + Theme: "dark", + Timezone: "browser", + HomeDashboardId: 0, + } + + for _, p := range prefs { + if p.Theme != "" { + res.Theme = p.Theme + } + if p.Timezone != "" { + res.Timezone = p.Timezone + } + if p.HomeDashboardId != 0 { + res.HomeDashboardId = p.HomeDashboardId + } + } + + query.Result = res + return nil +} + func GetPreferences(query *m.GetPreferencesQuery) error { var prefs m.Preferences @@ -54,7 +89,7 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { prefs.Theme = cmd.Theme prefs.Updated = time.Now() prefs.Version += 1 - _, err = sess.Id(prefs.Id).Update(&prefs) + _, err := sess.Id(prefs.Id).AllCols().Update(&prefs) return err } }) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 96b8c24b8fc..623e85ec472 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -273,7 +273,6 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { u.email as email, u.login as login, u.name as name, - u.theme as theme, org.name as org_name, org_user.role as org_role, org.id as org_id diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index ddc0ac06250..c258fb1a611 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -88,17 +88,20 @@ function setupAngularRoutes($routeProvider, $locationProvider) { resolve: loadOrgBundle, }) .when('/profile', { - templateUrl: 'public/app/features/profile/partials/profile.html', + templateUrl: 'public/app/features/org/partials/profile.html', controller : 'ProfileCtrl', controllerAs: 'ctrl', + resolve: loadOrgBundle, }) .when('/profile/password', { - templateUrl: 'public/app/features/profile/partials/password.html', + templateUrl: 'public/app/features/org/partials/password.html', controller : 'ChangePasswordCtrl', + resolve: loadOrgBundle, }) .when('/profile/select-org', { - templateUrl: 'public/app/features/profile/partials/select_org.html', + templateUrl: 'public/app/features/org/partials/select_org.html', controller : 'SelectOrgCtrl', + resolve: loadOrgBundle, }) // ADMIN .when('/admin', { diff --git a/public/app/features/all.js b/public/app/features/all.js index 19ca8d7e2c9..43d9f33f190 100644 --- a/public/app/features/all.js +++ b/public/app/features/all.js @@ -7,8 +7,5 @@ define([ './playlist/all', './snapshot/all', './panel/all', - './profile/profile_ctrl', - './profile/changePasswordCtrl', - './profile/selectOrgCtrl', './styleguide/styleguide', ], function () {}); diff --git a/public/app/features/org/all.js b/public/app/features/org/all.js index e04634d709a..b610a20e686 100644 --- a/public/app/features/org/all.js +++ b/public/app/features/org/all.js @@ -1,7 +1,11 @@ define([ './org_users_ctrl', + './profile_ctrl', + './org_users_ctrl', + './select_org_ctrl', './newOrgCtrl', './userInviteCtrl', './orgApiKeysCtrl', './orgDetailsCtrl', + './prefs_control', ], function () {}); diff --git a/public/app/features/profile/changePasswordCtrl.js b/public/app/features/org/change_password_ctrl.js similarity index 100% rename from public/app/features/profile/changePasswordCtrl.js rename to public/app/features/org/change_password_ctrl.js diff --git a/public/app/features/profile/partials/password.html b/public/app/features/org/partials/change_password.html similarity index 100% rename from public/app/features/profile/partials/password.html rename to public/app/features/org/partials/change_password.html diff --git a/public/app/features/org/partials/orgDetails.html b/public/app/features/org/partials/orgDetails.html index b3ba0f73d83..5f264517b0d 100644 --- a/public/app/features/org/partials/orgDetails.html +++ b/public/app/features/org/partials/orgDetails.html @@ -19,6 +19,8 @@
    + +

    Address

    diff --git a/public/app/features/profile/partials/profile.html b/public/app/features/org/partials/profile.html similarity index 62% rename from public/app/features/profile/partials/profile.html rename to public/app/features/org/partials/profile.html index e899b017ea6..811891ae317 100644 --- a/public/app/features/profile/partials/profile.html +++ b/public/app/features/org/partials/profile.html @@ -26,36 +26,7 @@
    -
    -

    Preferences

    - -
    - UI Theme -
    - -
    -
    - -
    - Home Dashboard - - -
    - -
    - -
    - -
    -
    - -
    - -
    -
    - +

    Password

    diff --git a/public/app/features/profile/partials/select_org.html b/public/app/features/org/partials/select_org.html similarity index 100% rename from public/app/features/profile/partials/select_org.html rename to public/app/features/org/partials/select_org.html diff --git a/public/app/features/org/prefs_control.ts b/public/app/features/org/prefs_control.ts new file mode 100644 index 00000000000..bd1cee06bac --- /dev/null +++ b/public/app/features/org/prefs_control.ts @@ -0,0 +1,100 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import coreModule from '../../core/core_module'; + +export class PrefsControlCtrl { + prefs: any; + oldTheme: any; + prefsForm: any; + mode: string; + + timezones: any = [ + {value: '', text: 'Default'}, + {value: 'browser', text: 'Local browser time'}, + {value: 'utc', text: 'UTC'}, + ]; + themes: any = [ + {value: '', text: 'Default'}, + {value: 'dark', text: 'Dark'}, + {value: 'light', text: 'Light'}, + ]; + + /** @ngInject **/ + constructor(private backendSrv, private $location) { + } + + $onInit() { + return this.backendSrv.get(`/api/${this.mode}/preferences`).then(prefs => { + this.prefs = prefs; + this.oldTheme = prefs.theme; + }); + } + + updatePrefs() { + if (!this.prefsForm.$valid) { return; } + + var cmd = { + theme: this.prefs.theme, + timezone: this.prefs.timezone, + homeDashboardId: this.prefs.homeDashboardId + }; + + this.backendSrv.put(`/api/${this.mode}/preferences`, cmd).then(() => { + if (this.oldTheme !== cmd.theme) { + window.location.href = config.appSubUrl + this.$location.path(); + } + }); + } + +} + +var template = ` +
    +

    Preferences

    + +
    + UI Theme +
    + +
    +
    + +
    + Home Dashboard + + +
    + +
    + +
    + +
    +
    + +
    + +
    +
    +`; + +export function prefsControlDirective() { + return { + restrict: 'E', + controller: PrefsControlCtrl, + bindToController: true, + controllerAs: 'ctrl', + template: template, + scope: { + mode: "@" + } + }; +} + +coreModule.directive('prefsControl', prefsControlDirective); + + diff --git a/public/app/features/profile/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts similarity index 53% rename from public/app/features/profile/profile_ctrl.ts rename to public/app/features/org/profile_ctrl.ts index 7b37f5484be..65bff1f6c12 100644 --- a/public/app/features/profile/profile_ctrl.ts +++ b/public/app/features/org/profile_ctrl.ts @@ -8,26 +8,12 @@ export class ProfileCtrl { user: any; old_theme: any; orgs: any; - prefs: any; userForm: any; - prefsForm: any; - - timezones: any = [ - {value: '', text: 'Default'}, - {value: 'browser', text: 'Local browser time'}, - {value: 'utc', text: 'UTC'}, - ]; - themes: any = [ - {value: '', text: 'Default'}, - {value: 'dark', text: 'Dark'}, - {value: 'light', text: 'Light'}, - ]; /** @ngInject **/ - constructor(private $scope, private backendSrv, private contextSrv, private $location) { + constructor(private backendSrv, private contextSrv, private $location) { this.getUser(); this.getUserOrgs(); - this.getUserPrefs(); } getUser() { @@ -37,13 +23,6 @@ export class ProfileCtrl { }); } - getUserPrefs() { - this.backendSrv.get('/api/user/preferences').then(prefs => { - this.prefs = prefs; - this.old_theme = prefs.theme; - }); - } - getUserOrgs() { this.backendSrv.get('/api/user/orgs').then(orgs => { this.orgs = orgs; @@ -67,21 +46,6 @@ export class ProfileCtrl { }); } - updatePrefs() { - if (!this.prefsForm.$valid) { return; } - - var cmd = { - theme: this.prefs.theme, - timezone: this.prefs.timezone, - homeDashboardId: this.prefs.homeDashboardId - }; - - this.backendSrv.put('/api/user/preferences', cmd).then(() => { - if (this.old_theme !== cmd.theme) { - window.location.href = config.appSubUrl + this.$location.path(); - } - }); - } } coreModule.controller('ProfileCtrl', ProfileCtrl); diff --git a/public/app/features/profile/selectOrgCtrl.js b/public/app/features/org/select_org_ctrl.js similarity index 100% rename from public/app/features/profile/selectOrgCtrl.js rename to public/app/features/org/select_org_ctrl.js From 55d95f90092d499afdcd8e14a36de60a3112bd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 05:27:35 -0700 Subject: [PATCH 078/142] feat(prefs): moved context srv to typescript --- public/app/core/services/context_srv.js | 47 ----------------- public/app/core/services/context_srv.ts | 67 +++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 47 deletions(-) delete mode 100644 public/app/core/services/context_srv.js create mode 100644 public/app/core/services/context_srv.ts diff --git a/public/app/core/services/context_srv.js b/public/app/core/services/context_srv.js deleted file mode 100644 index b4a133afea3..00000000000 --- a/public/app/core/services/context_srv.js +++ /dev/null @@ -1,47 +0,0 @@ -define([ - 'angular', - 'lodash', - '../core_module', - 'app/core/store', - 'app/core/config', -], -function (angular, _, coreModule, store, config) { - 'use strict'; - - coreModule.default.service('contextSrv', function() { - - function User() { - if (config.bootData.user) { - _.extend(this, config.bootData.user); - } - } - - this.hasRole = function(role) { - return this.user.orgRole === role; - }; - - this.setPinnedState = function(val) { - this.pinned = val; - store.set('grafana.sidemenu.pinned', val); - }; - - this.toggleSideMenu = function() { - this.sidemenu = !this.sidemenu; - if (!this.sidemenu) { - this.setPinnedState(false); - } - }; - - this.pinned = store.getBool('grafana.sidemenu.pinned', false); - if (this.pinned) { - this.sidemenu = true; - } - - this.version = config.buildInfo.version; - this.lightTheme = false; - this.user = new User(); - this.isSignedIn = this.user.isSignedIn; - this.isGrafanaAdmin = this.user.isGrafanaAdmin; - this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); - }); -}); diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts new file mode 100644 index 00000000000..0ce00c5f444 --- /dev/null +++ b/public/app/core/services/context_srv.ts @@ -0,0 +1,67 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; +import store from 'app/core/store'; + +export class User { + isGrafanaAdmin: any; + isSignedIn: any; + orgRole: any; + + constructor() { + if (config.bootData.user) { + _.extend(this, config.bootData.user); + } + } +} + +export class ContextSrv { + pinned: any; + version: any; + user: User; + isSignedIn: any; + isGrafanaAdmin: any; + isEditor: any; + sidemenu: any; + lightTheme: any; + + constructor() { + this.pinned = store.getBool('grafana.sidemenu.pinned', false); + if (this.pinned) { + this.sidemenu = true; + } + + this.version = config.buildInfo.version; + this.lightTheme = false; + this.user = new User(); + this.isSignedIn = this.user.isSignedIn; + this.isGrafanaAdmin = this.user.isGrafanaAdmin; + this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); + } + + hasRole(role) { + return this.user.orgRole === role; + } + + setPinnedState(val) { + this.pinned = val; + store.set('grafana.sidemenu.pinned', val); + } + + toggleSideMenu() { + this.sidemenu = !this.sidemenu; + if (!this.sidemenu) { + this.setPinnedState(false); + } + } +} + +var contextSrv = new ContextSrv(); +export {contextSrv}; + +coreModule.factory('contextSrv', function() { + return contextSrv; +}); From a2c6469d416a03c84accd7cceba0d94802b517f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 05:32:15 -0700 Subject: [PATCH 079/142] fix(): A Datasource with null jsonData would make Grafana fail to load page, fixes #4536 --- CHANGELOG.md | 1 + pkg/api/frontendsettings.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e810a7801f..586cf96f56e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Bug fixes * **Dashboard**: Fixed dashboard panel layout for mobile devices, fixes [#4529](https://github.com/grafana/grafana/issues/4529) +* **Page Load Crash**: A Datasource with null jsonData would make Grafana fail to load page, fixes [#4536](https://github.com/grafana/grafana/issues/4536) # 3.0.0-beta1 (2016-03-31) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index c84d7faccff..f23f416b47a 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -59,7 +59,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro defaultDatasource = ds.Name } - if len(ds.JsonData.MustMap()) > 0 { + if ds.JsonData != nil { dsMap["jsonData"] = ds.JsonData } From b30b78e44264e89a729bd280ae67407ded610a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 06:05:43 -0700 Subject: [PATCH 080/142] feat(preferences): got timezone option to work on org and profile level, as well as dashboard --- public/app/features/dashboard/dashboardCtrl.js | 4 ++++ public/app/features/dashboard/dashboardSrv.js | 12 ++++++++++-- public/app/features/dashboard/partials/settings.html | 2 +- .../app/features/dashboard/timepicker/timepicker.ts | 4 ++-- public/app/features/org/prefs_control.ts | 6 ++---- public/app/plugins/panel/graph/graph.js | 2 +- public/app/plugins/panel/table/module.ts | 2 +- public/app/plugins/panel/table/renderer.ts | 4 ++-- 8 files changed, 23 insertions(+), 13 deletions(-) diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js index ea0d91019b8..b6702631155 100644 --- a/public/app/features/dashboard/dashboardCtrl.js +++ b/public/app/features/dashboard/dashboardCtrl.js @@ -134,6 +134,10 @@ function (angular, $, config, moment) { }); }; + $scope.timezoneChanged = function() { + $rootScope.$broadcast("refresh"); + }; + $scope.formatDate = function(date) { return moment(date).format('MMM Do YYYY, h:mm:ss a'); }; diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index b3194868d97..ed119f6e23f 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -9,7 +9,7 @@ function (angular, $, _, moment) { var module = angular.module('grafana.services'); - module.factory('dashboardSrv', function() { + module.factory('dashboardSrv', function(contextSrv) { function DashboardModel (data, meta) { if (!data) { @@ -25,7 +25,7 @@ function (angular, $, _, moment) { this.originalTitle = this.title; this.tags = data.tags || []; this.style = data.style || "dark"; - this.timezone = data.timezone || 'browser'; + this.timezone = data.timezone || ''; this.editable = data.editable !== false; this.hideControls = data.hideControls || false; this.sharedCrosshair = data.sharedCrosshair || false; @@ -208,6 +208,14 @@ function (angular, $, _, moment) { }); }; + p.isTimezoneUtc = function() { + return this.getTimezone() === 'utc'; + }; + + p.getTimezone = function() { + return this.timezone ? this.timezone : contextSrv.user.timezone; + }; + p._updateSchema = function(old) { var i, j, k; var oldVersion = this.schemaVersion; diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index ca591c0c481..2a2287613ae 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -34,7 +34,7 @@
    - +
    diff --git a/public/app/features/dashboard/timepicker/timepicker.ts b/public/app/features/dashboard/timepicker/timepicker.ts index 453eaaf3204..2f757d36357 100644 --- a/public/app/features/dashboard/timepicker/timepicker.ts +++ b/public/app/features/dashboard/timepicker/timepicker.ts @@ -44,7 +44,7 @@ export class TimePickerCtrl { var time = angular.copy(this.timeSrv.timeRange()); var timeRaw = angular.copy(this.timeSrv.timeRange(false)); - if (this.dashboard.timezone === 'browser') { + if (!this.dashboard.isTimezoneUtc()) { time.from.local(); time.to.local(); if (moment.isMoment(timeRaw.from)) { @@ -125,7 +125,7 @@ export class TimePickerCtrl { } getAbsoluteMomentForTimezone(jsDate) { - return this.dashboard.timezone === 'browser' ? moment(jsDate) : moment(jsDate).utc(); + return this.dashboard.isTimezoneUtc() ? moment(jsDate).utc() : moment(jsDate); } setRelativeFilter(timespan) { diff --git a/public/app/features/org/prefs_control.ts b/public/app/features/org/prefs_control.ts index bd1cee06bac..53704d5e390 100644 --- a/public/app/features/org/prefs_control.ts +++ b/public/app/features/org/prefs_control.ts @@ -2,7 +2,7 @@ import config from 'app/core/config'; import _ from 'lodash'; -import coreModule from '../../core/core_module'; +import coreModule from 'app/core/core_module'; export class PrefsControlCtrl { prefs: any; @@ -42,9 +42,7 @@ export class PrefsControlCtrl { }; this.backendSrv.put(`/api/${this.mode}/preferences`, cmd).then(() => { - if (this.oldTheme !== cmd.theme) { - window.location.href = config.appSubUrl + this.$location.path(); - } + window.location.href = config.appSubUrl + this.$location.path(); }); } diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 0443a294eed..4e948bf8e5b 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -279,7 +279,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf(); options.xaxis = { - timezone: dashboard.timezone, + timezone: dashboard.getTimezone(), show: panel['x-axis'], mode: "time", min: min, diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index d5ac802a62b..2d39d349d9e 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -155,7 +155,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { } function appendTableRows(tbodyElem) { - var renderer = new TableRenderer(panel, data, ctrl.dashboard.timezone); + var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc()); tbodyElem.empty(); tbodyElem.html(renderer.render(ctrl.pageIndex)); } diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 0a306b103a4..c00656071be 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -8,7 +8,7 @@ export class TableRenderer { formaters: any[]; colorState: any; - constructor(private panel, private table, private timezone) { + constructor(private panel, private table, private isUtc) { this.formaters = []; this.colorState = {}; } @@ -45,7 +45,7 @@ export class TableRenderer { return v => { if (_.isArray(v)) { v = v[0]; } var date = moment(v); - if (this.timezone === 'utc') { + if (this.isUtc) { date = date.utc(); } return date.format(style.dateFormat); From 6e6ebc59477503561c8f2da2a4f68559d1bb47fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 09:43:33 -0400 Subject: [PATCH 081/142] trying to fix karma issue --- karma.conf.js | 1 - package.json | 4 ++-- public/app/features/dashboard/dashnav/dashnav.ts | 9 +-------- public/vendor/angular/.bower.json | 2 +- public/vendor/angular/angular.js | 2 +- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index cbb0abff7c6..dc082924b11 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,7 +9,6 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ 'vendor/npm/es6-shim/es6-shim.js', - 'vendor/npm/es6-promise/dist/es6-promise.js', 'vendor/npm/systemjs/dist/system.src.js', 'test/test-main.js', diff --git a/package.json b/package.json index 4b526f5a4f3..d741bab574e 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "grunt-contrib-watch": "^0.6.1", "grunt-filerev": "^0.2.1", "grunt-git-describe": "~2.3.2", - "grunt-karma": "~0.12.1", + "grunt-karma": "~0.12.2", "grunt-ng-annotate": "^1.0.1", "grunt-notify": "^0.4.3", "grunt-postcss": "^0.8.0", @@ -43,7 +43,7 @@ "grunt-typescript": "^0.8.0", "grunt-usemin": "3.0.0", "jshint-stylish": "~2.1.0", - "karma": "~0.13.15", + "karma": "0.13.22", "karma-chrome-launcher": "~0.2.2", "karma-coverage": "0.5.3", "karma-coveralls": "1.1.2", diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 14dfa8cc67a..2d5b66cb13c 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -7,7 +7,7 @@ import angular from 'angular'; export class DashNavCtrl { /** @ngInject */ - constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, contextSrv, $timeout) { + constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout) { $scope.init = function() { $scope.onAppEvent('save-dashboard', $scope.saveDashboard); @@ -103,13 +103,6 @@ export class DashNavCtrl { }, $scope.handleSaveDashError); }; - $scope.saveDashboardAsHome = function() { - // TODO: this backend method needs to be implemented - backendSrv.post('/api/preferences/set-home-dash', { - homeDashboardId: $scope.dashboard.id - }); - }; - $scope.handleSaveDashError = function(err) { if (err.data && err.data.status === "version-mismatch") { err.isHandled = true; diff --git a/public/vendor/angular/.bower.json b/public/vendor/angular/.bower.json index b7b5e0bb59f..4221ce7cb09 100644 --- a/public/vendor/angular/.bower.json +++ b/public/vendor/angular/.bower.json @@ -13,6 +13,6 @@ "commit": "5a07c5107b4d24f41744a02b07717d55bad88e70" }, "_source": "git://github.com/angular/bower-angular.git", - "_target": "1.5.3", + "_target": "~1.5.3", "_originalSource": "angular" } \ No newline at end of file diff --git a/public/vendor/angular/angular.js b/public/vendor/angular/angular.js index 9ef1f59c0b7..5e6fb2c363d 100644 --- a/public/vendor/angular/angular.js +++ b/public/vendor/angular/angular.js @@ -30711,4 +30711,4 @@ $provide.value("$locale", { })(window, document); -!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend(''); +!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend(''); \ No newline at end of file From 431a610f0015171984f0d2f166ca63161ab6b7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 10:12:43 -0400 Subject: [PATCH 082/142] fix(): fixed failing unit test --- public/app/core/services/context_srv.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 0ce00c5f444..ad670f68a32 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -2,7 +2,6 @@ import config from 'app/core/config'; import _ from 'lodash'; -import $ from 'jquery'; import coreModule from 'app/core/core_module'; import store from 'app/core/store'; @@ -34,6 +33,13 @@ export class ContextSrv { this.sidemenu = true; } + if (!config.buildInfo) { + config.buildInfo = {}; + } + if (!config.bootData) { + config.bootData = {user: {}, settings: {}}; + } + this.version = config.buildInfo.version; this.lightTheme = false; this.user = new User(); From a12de0953364aa67adb62764627f325c601011d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 10:21:11 -0400 Subject: [PATCH 083/142] changelog(): updated with preferences features --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 586cf96f56e..b14626c3126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 3.0.0-beta2 (unreleased) +### New Features (introduces since 3.0-beta1) +* **Preferences**: Set home dashboard on user and org level, closes [#1678](https://github.com/grafana/grafana/issues/1678) +* **Preferences**: Set timezone on user and org level, closes [#3214](https://github.com/grafana/grafana/issues/3214), [#1200](https://github.com/grafana/grafana/issues/1200) +* **Preferences**: Set theme on user and org level, closes [#3214](https://github.com/grafana/grafana/issues/3214), [#1917](https://github.com/grafana/grafana/issues/1917) + ### Bug fixes * **Dashboard**: Fixed dashboard panel layout for mobile devices, fixes [#4529](https://github.com/grafana/grafana/issues/4529) * **Page Load Crash**: A Datasource with null jsonData would make Grafana fail to load page, fixes [#4536](https://github.com/grafana/grafana/issues/4536) From 105a678d642417197844b0c85c2a4636e013b200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 10:28:31 -0400 Subject: [PATCH 084/142] fix(): another test fix --- .../app/plugins/panel/graph/specs/graph_specs.ts | 4 +++- public/test/specs/dashboardSrv-specs.js | 4 ++++ public/test/specs/dynamicDashboardSrv-specs.js | 15 ++++++++++----- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 1a708696680..b9c9362e5de 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -53,7 +53,9 @@ describe('grafanaGraph', function() { }, renderingCompleted: sinon.spy(), hiddenSeries: {}, - dashboard: {timezone: 'browser'}, + dashboard: { + getTimezone: sinon.stub().returns('browser') + }, range: { from: moment([2015, 1, 1, 10]), to: moment([2015, 1, 1, 22]), diff --git a/public/test/specs/dashboardSrv-specs.js b/public/test/specs/dashboardSrv-specs.js index e02495c1236..6fc53f09190 100644 --- a/public/test/specs/dashboardSrv-specs.js +++ b/public/test/specs/dashboardSrv-specs.js @@ -7,6 +7,10 @@ define([ var _dashboardSrv; beforeEach(module('grafana.services')); + beforeEach(module(function($provide) { + $provide.value('contextSrv', { + }); + })); beforeEach(inject(function(dashboardSrv) { _dashboardSrv = dashboardSrv; diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js index 0104081b768..b988203009a 100644 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ b/public/test/specs/dynamicDashboardSrv-specs.js @@ -12,6 +12,11 @@ define([ ctx.setup = function (setupFunc) { beforeEach(module('grafana.services')); + beforeEach(module(function($provide) { + $provide.value('contextSrv', { + user: { timezone: 'utc'} + }); + })); beforeEach(inject(function(dynamicDashboardSrv, dashboardSrv) { ctx.dynamicDashboardSrv = dynamicDashboardSrv; @@ -45,10 +50,10 @@ define([ value: ['se1', 'se2', 'se3'] }, options: [ - {text: 'se1', value: 'se1', selected: true}, - {text: 'se2', value: 'se2', selected: true}, - {text: 'se3', value: 'se3', selected: true}, - {text: 'se4', value: 'se4', selected: false} + {text: 'se1', value: 'se1', selected: true}, + {text: 'se2', value: 'se2', selected: true}, + {text: 'se3', value: 'se3', selected: true}, + {text: 'se4', value: 'se4', selected: false} ] }); }); @@ -93,7 +98,7 @@ define([ describe('After a second iteration and selected values reduced', function() { beforeEach(function() { ctx.dash.templating.list[0].options[1].selected = false; - + ctx.dynamicDashboardSrv.update(ctx.dash); }); From d8499e6941c8c758feb98a2b4aad571c7ae833e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 15:51:54 -0400 Subject: [PATCH 085/142] fix(influxdb): added regex start string char to templating suggestion, fixes #4405 --- public/app/plugins/datasource/influxdb/query_ctrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index d59bbbbccd5..041e3c8a882 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -193,7 +193,7 @@ export class InfluxQueryCtrl extends QueryCtrl { if (addTemplateVars) { for (let variable of this.templateSrv.variables) { - segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/$' + variable.name + '$/', expandable: true })); + segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/^$' + variable.name + '$/', expandable: true })); } } From 1c97f10d8a4757bdaa100cc63efe0503bfc0a86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 16:01:48 -0400 Subject: [PATCH 086/142] fix(table panel): fixed column sort issue with table panel, #4532 --- CHANGELOG.md | 1 + public/app/plugins/panel/table/module.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b14626c3126..e20da1142d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Bug fixes * **Dashboard**: Fixed dashboard panel layout for mobile devices, fixes [#4529](https://github.com/grafana/grafana/issues/4529) +* **Table Panel**: Fixed issue with table panel sort, fixes [#4532](https://github.com/grafana/grafana/issues/4532) * **Page Load Crash**: A Datasource with null jsonData would make Grafana fail to load page, fixes [#4536](https://github.com/grafana/grafana/issues/4536) # 3.0.0-beta1 (2016-03-31) diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 2d39d349d9e..91fe1a5fb8b 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -131,7 +131,9 @@ class TablePanelCtrl extends MetricsPanelCtrl { this.panel.sort.desc = true; } - this.render(); + this.table = transformDataToTable(this.dataRaw, this.panel); + this.table.sort(this.panel.sort); + this.render(this.table); } exportCsv() { From c764e7fd974dd1695459efd97e7ec96cea93ab01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 16:11:42 -0400 Subject: [PATCH 087/142] fix(elasticsearch): add a better default terms size limit of 10, fixes #4013 --- public/app/plugins/datasource/elasticsearch/bucket_agg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.js b/public/app/plugins/datasource/elasticsearch/bucket_agg.js index 47d88a6ea3c..18e2f3cf3b1 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.js +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.js @@ -77,7 +77,7 @@ function (angular, _, queryDef) { switch($scope.agg.type) { case 'terms': { settings.order = settings.order || "asc"; - settings.size = settings.size || "0"; + settings.size = settings.size || "10"; settings.orderBy = settings.orderBy || "_term"; if (settings.size !== '0') { From f35eb0f148dc7035d5e33858caad2af14e1ebc63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Apr 2016 16:19:14 -0400 Subject: [PATCH 088/142] fix(metrics tab): Fix for missing datasource name in datasource selector, fixes #4540 --- CHANGELOG.md | 1 + public/app/features/panel/metrics_panel_ctrl.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e20da1142d4..fc75b3fa734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * **Dashboard**: Fixed dashboard panel layout for mobile devices, fixes [#4529](https://github.com/grafana/grafana/issues/4529) * **Table Panel**: Fixed issue with table panel sort, fixes [#4532](https://github.com/grafana/grafana/issues/4532) * **Page Load Crash**: A Datasource with null jsonData would make Grafana fail to load page, fixes [#4536](https://github.com/grafana/grafana/issues/4536) +* **Metrics tab**: Fix for missing datasource name in datasource selector, fixes [#4540](https://github.com/grafana/grafana/issues/4540) # 3.0.0-beta1 (2016-03-31) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 52e9c64981c..c1f408e9374 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -165,6 +165,7 @@ class MetricsPanelCtrl extends PanelCtrl { issueQueries(datasource) { this.updateTimeRange(); + this.datasource = datasource; if (!this.panel.targets || this.panel.targets.length === 0) { return this.$q.when([]); @@ -251,7 +252,6 @@ class MetricsPanelCtrl extends PanelCtrl { addDataQuery(datasource) { var target = { - datasource: datasource ? datasource.name : undefined }; this.panel.targets.push(target); } From 542e080f0a7777860355223d714aab985e6cb014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Apr 2016 13:13:25 -0400 Subject: [PATCH 089/142] fix(graph): fixed issue with graph legend when in table mode and series on righ-y axis, also fixed so that you can see what series are on right-y axis when legend is in table mode, fixes #4551, fixes #1145 --- CHANGELOG.md | 1 + public/app/plugins/panel/graph/legend.js | 2 +- public/sass/base/_font_awesome.scss | 6 ------ public/sass/components/_panel_graph.scss | 16 ++++++++++++++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc75b3fa734..4e92dd13f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * **Table Panel**: Fixed issue with table panel sort, fixes [#4532](https://github.com/grafana/grafana/issues/4532) * **Page Load Crash**: A Datasource with null jsonData would make Grafana fail to load page, fixes [#4536](https://github.com/grafana/grafana/issues/4536) * **Metrics tab**: Fix for missing datasource name in datasource selector, fixes [#4540](https://github.com/grafana/grafana/issues/4540) +* **Graph**: Fix legend in table mode with series on right-y axis, fixes [#4551](https://github.com/grafana/grafana/issues/4551), [#1145](https://github.com/grafana/grafana/issues/1145) # 3.0.0-beta1 (2016-03-31) diff --git a/public/app/plugins/panel/graph/legend.js b/public/app/plugins/panel/graph/legend.js index 22bbb756b59..4cad3c16844 100644 --- a/public/app/plugins/panel/graph/legend.js +++ b/public/app/plugins/panel/graph/legend.js @@ -157,7 +157,7 @@ function (angular, _, $) { } var html = '
    '; html += '
    '; diff --git a/public/sass/base/_font_awesome.scss b/public/sass/base/_font_awesome.scss index 438185f3198..51d9bac987d 100644 --- a/public/sass/base/_font_awesome.scss +++ b/public/sass/base/_font_awesome.scss @@ -66,12 +66,6 @@ border: solid 0.08em #eeeeee; border-radius: .1em; } -.pull-right { - float: right; -} -.pull-left { - float: left; -} .fa.pull-left { margin-right: .3em; } diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index ccecbc6c9a7..9837f33a543 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -74,6 +74,10 @@ float: left; white-space: nowrap; padding-left: 10px; + + &--right-y { + float: right; + } } .graph-legend-value { @@ -83,14 +87,22 @@ .graph-legend-table { overflow-y: scroll; - .graph-legend-series { display: table-row; + .graph-legend-series { + display: table-row; float: none; padding-left: 0; - &.pull-right { + &--right-y { float: none; + + .graph-legend-alias:after { + content: '(right-y)'; + padding: 0 5px; + color: $text-color-weak; + } } } + td, .graph-legend-alias, .graph-legend-icon, .graph-legend-value { float: none; display: table-cell; From 892e79a9827e74e784117005929c162062c0d448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Apr 2016 13:43:13 -0400 Subject: [PATCH 090/142] fix(table): another fix for table panel sorting, fixes #4544 --- public/app/plugins/panel/table/module.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 91fe1a5fb8b..295c7e0ee33 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -114,9 +114,13 @@ class TablePanelCtrl extends MetricsPanelCtrl { } } + this.render(); + } + + render() { this.table = transformDataToTable(this.dataRaw, this.panel); this.table.sort(this.panel.sort); - this.render(this.table); + return super.render(this.table); } toggleColumnSort(col, colIndex) { @@ -130,10 +134,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { this.panel.sort.col = colIndex; this.panel.sort.desc = true; } - - this.table = transformDataToTable(this.dataRaw, this.panel); - this.table.sort(this.panel.sort); - this.render(this.table); + this.render(); } exportCsv() { From 43ebc172cb57907869fb4d261ad361286eaccfdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Apr 2016 13:52:41 -0400 Subject: [PATCH 091/142] fix(profile): fixed change password link and page, fixes #4542 --- CHANGELOG.md | 1 + pkg/api/api.go | 1 + public/app/core/routes/routes.ts | 2 +- public/app/features/org/all.js | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e92dd13f52..cb4ab3ad1e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * **Page Load Crash**: A Datasource with null jsonData would make Grafana fail to load page, fixes [#4536](https://github.com/grafana/grafana/issues/4536) * **Metrics tab**: Fix for missing datasource name in datasource selector, fixes [#4540](https://github.com/grafana/grafana/issues/4540) * **Graph**: Fix legend in table mode with series on right-y axis, fixes [#4551](https://github.com/grafana/grafana/issues/4551), [#1145](https://github.com/grafana/grafana/issues/1145) +* **Password**: Password reset link/page did not work, fixes [#4542](https://github.com/grafana/grafana/issues/4542) # 3.0.0-beta1 (2016-03-31) diff --git a/pkg/api/api.go b/pkg/api/api.go index 5ae51b58904..85f1e2474c3 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -29,6 +29,7 @@ func Register(r *macaron.Macaron) { // authed views r.Get("/profile/", reqSignedIn, Index) + r.Get("/profile/password", reqSignedIn, Index) r.Get("/org/", reqSignedIn, Index) r.Get("/org/new", reqSignedIn, Index) r.Get("/datasources/", reqSignedIn, Index) diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index c258fb1a611..1608a772e87 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -94,7 +94,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) { resolve: loadOrgBundle, }) .when('/profile/password', { - templateUrl: 'public/app/features/org/partials/password.html', + templateUrl: 'public/app/features/org/partials/change_password.html', controller : 'ChangePasswordCtrl', resolve: loadOrgBundle, }) diff --git a/public/app/features/org/all.js b/public/app/features/org/all.js index b610a20e686..e206583a8c7 100644 --- a/public/app/features/org/all.js +++ b/public/app/features/org/all.js @@ -3,6 +3,7 @@ define([ './profile_ctrl', './org_users_ctrl', './select_org_ctrl', + './change_password_ctrl', './newOrgCtrl', './userInviteCtrl', './orgApiKeysCtrl', From e0277702135c9c2e8eeee2a1cd75ee5c7b71c391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Apr 2016 17:49:33 -0400 Subject: [PATCH 092/142] docs(): updated links to beta2 --- docs/sources/installation/debian.md | 6 +++--- docs/sources/installation/rpm.md | 8 ++++---- packaging/publish/publish.sh | 18 +++++++++--------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 2d24e7a9c3f..84beccfa513 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -11,7 +11,7 @@ page_keywords: grafana, installation, debian, ubuntu, guide Description | Download ------------ | ------------- Stable .deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb) -Beta .deb for Debian-based Linux | [grafana_3.0.0-beta11459429091_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta11459429091_amd64.deb) +Beta .deb for Debian-based Linux | [grafana_3.0.0-beta21459801392_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta21459801392_amd64.deb) ## Install Stable @@ -21,9 +21,9 @@ Beta .deb for Debian-based Linux | [grafana_3.0.0-beta11459429091_amd64.deb](h ## Install 3.0 Beta - $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta11459429091_amd64.deb + $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta21459801392_amd64.deb $ sudo apt-get install -y adduser libfontconfig - $ sudo dpkg -i grafana_3.0.0-beta11459429091_amd64.deb + $ sudo dpkg -i grafana_3.0.0-beta21459801392_amd64.deb ## APT Repository diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 5e8e746d0f7..498fcb2d580 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -11,7 +11,7 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide Description | Download ------------ | ------------- Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm) -Beta .RPM for CentOS / Fedor / OpenSuse / Redhat Linux | [grafana-3.0.0-beta11459429091.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta11459429091.x86_64.rpm) +Beta .RPM for CentOS / Fedor / OpenSuse / Redhat Linux | [grafana-3.0.0-beta21459801392.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta21459801392.x86_64.rpm) ## Install Stable Release from package file @@ -34,18 +34,18 @@ Or install manually using `rpm`. You can install Grafana using Yum directly. - $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta11459429091.x86_64.rpm + $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta21459801392.x86_64.rpm Or install manually using `rpm`. #### On CentOS / Fedora / Redhat: $ sudo yum install initscripts fontconfig - $ sudo rpm -Uvh grafana-3.0.0-beta11459429091.x86_64.rpm + $ sudo rpm -Uvh grafana-3.0.0-beta21459801392.x86_64.rpm #### On OpenSuse: - $ sudo rpm -i --nodeps grafana-3.0.0-beta11459429091.x86_64.rpm + $ sudo rpm -i --nodeps grafana-3.0.0-beta21459801392.x86_64.rpm ## Install via YUM Repository diff --git a/packaging/publish/publish.sh b/packaging/publish/publish.sh index 58021bc6ffa..38f4a6b4167 100755 --- a/packaging/publish/publish.sh +++ b/packaging/publish/publish.sh @@ -1,22 +1,22 @@ #! /usr/bin/env bash -deb_ver=3.0.0-beta11459429091 -rpm_ver=3.0.0-beta11459429091 +deb_ver=3.0.0-beta21459801392 +rpm_ver=3.0.0-beta21459801392 #rpm_ver=3.0.0-1 -wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb +#wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb -# package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb -# package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb +#package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb +#package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb -package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb -package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb +#package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb +#package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb -wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm +#wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm -package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm +¤package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm # package_cloud push grafana/stable/el/7 grafana-${version}-1.x86_64.rpm # package_cloud push grafana/stable/el/6 grafana-${version}-1.x86_64.rpm From 860e771493ecb0bda7f82414fd71edc5e87c3491 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 5 Apr 2016 12:01:13 +0200 Subject: [PATCH 093/142] fix(plugins): Fix broken link in plugins readme Can't find the DEVELOPMENT.md file so linked to http://docs.grafana.org/project/building_from_source/ instead. --- docs/sources/plugins/development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/plugins/development.md b/docs/sources/plugins/development.md index e8b5c620732..46f0f1764af 100644 --- a/docs/sources/plugins/development.md +++ b/docs/sources/plugins/development.md @@ -10,7 +10,7 @@ From grafana 3.0 it's very easy to develop your own plugins and share them with ## Short version -1. [Setup grafana](https://github.com/grafana/grafana/blob/master/DEVELOPMENT.md) +1. [Setup grafana](http://docs.grafana.org/project/building_from_source/) 2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if your running development version from source dir) 3. Code away! From b39aa02b52df1efc51f4a7b278cf08134a0fc88d Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 5 Apr 2016 12:05:14 +0200 Subject: [PATCH 094/142] fix(plugins): Fix broken link in plugins readme Another link to setup instructions that it is broken. --- docs/sources/plugins/development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/plugins/development.md b/docs/sources/plugins/development.md index 46f0f1764af..29dcd0568e3 100644 --- a/docs/sources/plugins/development.md +++ b/docs/sources/plugins/development.md @@ -34,7 +34,7 @@ and [apps](./apps.md) plugins in the documentation. ## Start developing your plugin There are two ways that you can start developing a Grafana plugin. -1. Setup a Grafana development environment. [(described here)](https://github.com/grafana/grafana/blob/master/DEVELOPMENT.md) and place your plugin in the ```data/plugins``` folder. +1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder. 2. Install Grafana and place your plugin in the plugins directory which is set in your [config file](../installation/configuration.md). By default this is `/var/lib/grafana/plugins` on Linux systems. 3. Place your plugin directory anywhere you like and specify it grafana.ini. From 3c1e9e9a25732a1aefe2f44a30307d0478849493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Apr 2016 09:05:34 -0400 Subject: [PATCH 095/142] fix(table): fixed issue with table panel and annotations, fixes #4563 --- public/app/plugins/panel/table/module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 295c7e0ee33..a6d348064eb 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -77,9 +77,9 @@ class TablePanelCtrl extends MetricsPanelCtrl { this.pageIndex = 0; if (this.panel.transform === 'annotations') { + this.setTimeQueryStart(); return this.annotationsSrv.getAnnotations(this.dashboard).then(annotations => { - this.dataRaw = annotations; - this.render(); + return {data: annotations}; }); } From 676e950fa30547f4b835776c82fa57cf40714eea Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Apr 2016 15:34:24 +0200 Subject: [PATCH 096/142] fix(cli): improve error message for not using sudo closes #4562 --- .../grafana-cli/commands/install_command.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 0f783d5d3ff..81e7a15233b 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -120,6 +120,7 @@ func RemoveGitBuildFromName(pluginName, filename string) string { } var retryCount = 0 +var permissionsDeniedMessage = "Could not create %s. Permission denied. Make sure you have write access to plugindir" func downloadFile(pluginName, filePath, url string) (err error) { defer func() { @@ -153,16 +154,16 @@ func downloadFile(pluginName, filePath, url string) (err error) { newFile := path.Join(filePath, RemoveGitBuildFromName(pluginName, zf.Name)) if zf.FileInfo().IsDir() { - os.Mkdir(newFile, 0777) + err := os.Mkdir(newFile, 0777) + if PermissionsError(err) { + return fmt.Errorf(permissionsDeniedMessage, newFile) + } } else { dst, err := os.Create(newFile) - if err != nil { - if strings.Contains(err.Error(), "permission denied") { - return fmt.Errorf( - "Could not create file %s. permission deined. Make sure you have write access to plugindir", - newFile) - } + if PermissionsError(err) { + return fmt.Errorf(permissionsDeniedMessage, newFile) } + defer dst.Close() src, err := zf.Open() if err != nil { @@ -176,3 +177,7 @@ func downloadFile(pluginName, filePath, url string) (err error) { return nil } + +func PermissionsError(err error) bool { + return err != nil && strings.Contains(err.Error(), "permission denied") +} From 9b30634217817edf34e533d4a09fdb9a58c613cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Apr 2016 11:24:08 -0400 Subject: [PATCH 097/142] fix(postgres): fixed issue with postgres caused by wrong prefrences table types, dropped table and created it with correct column types, fixes #4558 --- CHANGELOG.md | 7 ++++++- packaging/publish/publish.sh | 2 +- pkg/api/avatar/avatar.go | 1 + .../sqlstore/migrations/preferences_mig.go | 14 ++++++++------ 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4ab3ad1e8..930ba87956b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# 3.0.0-beta2 (unreleased) +# 3.0.0-beta3 (unreleased) + +### Bug fixes +* **Postgres**: Fixed page render crash when using postgres, fixes [#4558](https://github.com/grafana/grafana/issues/4558) + +# 3.0.0-beta2 (2016-04-04) ### New Features (introduces since 3.0-beta1) * **Preferences**: Set home dashboard on user and org level, closes [#1678](https://github.com/grafana/grafana/issues/1678) diff --git a/packaging/publish/publish.sh b/packaging/publish/publish.sh index 38f4a6b4167..89a59ea5be1 100755 --- a/packaging/publish/publish.sh +++ b/packaging/publish/publish.sh @@ -16,7 +16,7 @@ rpm_ver=3.0.0-beta21459801392 #wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm -¤package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm +#package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm # package_cloud push grafana/stable/el/7 grafana-${version}-1.x86_64.rpm # package_cloud push grafana/stable/el/6 grafana-${version}-1.x86_64.rpm diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 79aebece349..41ce857db4f 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -116,6 +116,7 @@ func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { if avatar.Expired() { if err := avatar.Update(); err != nil { log.Trace("avatar update error: %v", err) + avatar = this.notFound } } diff --git a/pkg/services/sqlstore/migrations/preferences_mig.go b/pkg/services/sqlstore/migrations/preferences_mig.go index 6425c3735bb..67a8169a7a8 100644 --- a/pkg/services/sqlstore/migrations/preferences_mig.go +++ b/pkg/services/sqlstore/migrations/preferences_mig.go @@ -10,12 +10,12 @@ func addPreferencesMigrations(mg *Migrator) { Name: "preferences", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "org_id", Type: DB_Int, Nullable: true}, - {Name: "user_id", Type: DB_NVarchar, Length: 255, Nullable: true}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "user_id", Type: DB_BigInt, Nullable: false}, {Name: "version", Type: DB_Int, Nullable: false}, - {Name: "home_dashboard_id", Type: DB_BigInt, Nullable: true}, - {Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: true}, - {Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: true}, + {Name: "home_dashboard_id", Type: DB_BigInt, Nullable: false}, + {Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: false}, + {Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, }, @@ -25,6 +25,8 @@ func addPreferencesMigrations(mg *Migrator) { }, } + mg.AddMigration("drop preferences table v3", NewDropTableMigration("preferences")) + // create table - mg.AddMigration("create preferences table v2", NewAddTableMigration(preferencesV2)) + mg.AddMigration("create preferences table v3", NewAddTableMigration(preferencesV2)) } From 43896dc416cfadce50668976b61e662c2a417d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Apr 2016 11:29:12 -0400 Subject: [PATCH 098/142] fix(): updated changelog and version to beta3 --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 930ba87956b..589e4e07551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Bug fixes * **Postgres**: Fixed page render crash when using postgres, fixes [#4558](https://github.com/grafana/grafana/issues/4558) +* **Table panel**: Fixed table panel bug when trying to show annotations in table panel, fixes [#4563](https://github.com/grafana/grafana/issues/4563) # 3.0.0-beta2 (2016-04-04) diff --git a/package.json b/package.json index d741bab574e..b245bf540da 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "3.0.0-beta2", + "version": "3.0.0-beta3", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" From 616b8f82ee25940aa810e3ea3e527a2976007955 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 5 Apr 2016 13:24:19 -0400 Subject: [PATCH 099/142] document godep build dependency --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a3336e0373b..434dd19152b 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ the latest master builds [here](http://grafana.org/download/builds) - Go 1.5 - NodeJS +- [Godep](https://github.com/tools/godep) ### Get Code From ed1fe9364838044eaaa038458b6c475e99ed9e68 Mon Sep 17 00:00:00 2001 From: Vitor Boschi Date: Wed, 6 Apr 2016 08:53:20 -0300 Subject: [PATCH 100/142] Fix percentage checkbox naming. Fix #4584 --- public/app/plugins/panel/graph/tab_display.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index 5c021dae8e1..067651540e2 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -69,7 +69,7 @@ + checked="ctrl.panel.percentage" on-change="ctrl.render()">
    From dc7f8014a27faa43098227c1e8b647e389b9cf9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 6 Apr 2016 08:43:06 -0400 Subject: [PATCH 101/142] fix(app config): fixed issue with app config tab, fixes #4575 --- CHANGELOG.md | 1 + public/app/core/directives/plugin_component.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 589e4e07551..936be63f7c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Bug fixes * **Postgres**: Fixed page render crash when using postgres, fixes [#4558](https://github.com/grafana/grafana/issues/4558) * **Table panel**: Fixed table panel bug when trying to show annotations in table panel, fixes [#4563](https://github.com/grafana/grafana/issues/4563) +* **App Config**: Fixed app config issue showing content of other app config, fixes [#4575](https://github.com/grafana/grafana/issues/4575) # 3.0.0-beta2 (2016-04-04) diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 4f87a94573e..3e6383cc5c6 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -169,7 +169,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ return System.import(model.module).then(function(appModule) { return { baseUrl: model.baseUrl, - name: 'app-config-' + model.pluginId, + name: 'app-config-' + model.id, bindings: {appModel: "=", appEditCtrl: "="}, attrs: {"app-model": "ctrl.model", "app-edit-ctrl": "ctrl"}, Component: appModule.ConfigCtrl, From a27d72b437a1528b03ae658c8b47643ac59b126f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 6 Apr 2016 08:49:47 -0400 Subject: [PATCH 102/142] fix(): fixed issue with scripted dashboard example, fixes #4579 --- public/app/features/dashboard/dashboardSrv.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index ed119f6e23f..89c09ac551a 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -428,6 +428,8 @@ function (angular, $, _, moment) { // update graph yaxes changes panelUpgrades.push(function(panel) { if (panel.type !== 'graph') { return; } + if (!panel.grid) { return; } + if (!panel.yaxes) { panel.yaxes = [ { From 7f8f5f34989d5842f80177ddefecbffdbb1e91c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 6 Apr 2016 09:08:49 -0400 Subject: [PATCH 103/142] fix(): minor fix for plugin page, fixes #4578 --- public/app/features/plugins/partials/plugin_edit.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/plugins/partials/plugin_edit.html b/public/app/features/plugins/partials/plugin_edit.html index 6cb0ae5bf78..d5f69df2366 100644 --- a/public/app/features/plugins/partials/plugin_edit.html +++ b/public/app/features/plugins/partials/plugin_edit.html @@ -4,8 +4,8 @@