From 104982401448afe4b30acd92dfb64dc95eae0a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 3 Mar 2016 22:31:07 +0100 Subject: [PATCH 001/268] ux(): minor tweak to validation state --- public/sass/base/_forms.scss | 15 --------------- public/sass/utils/_validation.scss | 5 +++-- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/public/sass/base/_forms.scss b/public/sass/base/_forms.scss index 25a95fafde8..4f1d0778e0b 100644 --- a/public/sass/base/_forms.scss +++ b/public/sass/base/_forms.scss @@ -143,21 +143,6 @@ input[type="checkbox"][readonly] { background-color: transparent; } - -// HTML5 invalid states -// Shares styles with the .control-group.error above -input:focus:invalid, -textarea:focus:invalid, -select:focus:invalid { - color: #b94a48; - border-color: #ee5f5b; - &:focus { - border-color: darken(#ee5f5b, 10%); - $shadow: 0 0 6px lighten(#ee5f5b, 20%); - @include box-shadow($shadow); - } -} - input[type=text].input-fluid { width: 100%; box-sizing: border-box; diff --git a/public/sass/utils/_validation.scss b/public/sass/utils/_validation.scss index b59b65f6a54..1145212707e 100644 --- a/public/sass/utils/_validation.scss +++ b/public/sass/utils/_validation.scss @@ -1,7 +1,8 @@ input[type=text].ng-dirty.ng-invalid { - box-shadow: inset 0 0px 7px $red; } input.ng-dirty.ng-invalid { - color: $errorText; + box-shadow: inset 0 0px 5px $red; } + + From fc877ae0f4f3550aadb35fd1daadaf05a6e7a8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 3 Mar 2016 23:05:08 +0100 Subject: [PATCH 002/268] poc(websockets): websocket poc --- pkg/api/api.go | 7 +- pkg/api/search.go | 3 + pkg/live/conn.go | 105 +++++++++++++++++++++++++++++ pkg/live/hub.go | 52 ++++++++++++++ public/app/features/admin/admin.ts | 11 ++- 5 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 pkg/live/conn.go create mode 100644 pkg/live/hub.go diff --git a/pkg/api/api.go b/pkg/api/api.go index ed029a5171a..7242db9713a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -4,6 +4,7 @@ import ( "github.com/go-macaron/binding" "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/live" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "gopkg.in/macaron.v1" @@ -35,6 +36,7 @@ func Register(r *macaron.Macaron) { r.Get("/org/users/", reqSignedIn, Index) r.Get("/org/apikeys/", reqSignedIn, Index) r.Get("/dashboard/import/", reqSignedIn, Index) + r.Get("/admin", reqGrafanaAdmin, Index) r.Get("/admin/settings", reqGrafanaAdmin, Index) r.Get("/admin/users", reqGrafanaAdmin, Index) r.Get("/admin/users/create", reqGrafanaAdmin, Index) @@ -230,7 +232,10 @@ func Register(r *macaron.Macaron) { avt := avatar.CacheServer() r.Get("/avatar/:hash", avt.ServeHTTP) + // Websocket + liveConn := live.New() + r.Any("/ws", liveConn.Serve) + InitAppPluginRoutes(r) - r.NotFound(NotFoundHandler) } diff --git a/pkg/api/search.go b/pkg/api/search.go index 035e59fa3f4..8f190dd6bf1 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/live" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/services/search" ) @@ -32,4 +33,6 @@ func Search(c *middleware.Context) { } c.JSON(200, searchQuery.Result) + + live.SendMessage(query) } diff --git a/pkg/live/conn.go b/pkg/live/conn.go new file mode 100644 index 00000000000..90ce0af9cae --- /dev/null +++ b/pkg/live/conn.go @@ -0,0 +1,105 @@ +package live + +import ( + "net/http" + "time" + + "github.com/gorilla/websocket" + "github.com/grafana/grafana/pkg/log" +) + +const ( + // Time allowed to write a message to the peer. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Maximum message size allowed from peer. + maxMessageSize = 512 +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +type connection struct { + ws *websocket.Conn + send chan []byte +} + +func (c *connection) readPump() { + defer func() { + h.unregister <- c + c.ws.Close() + }() + c.ws.SetReadLimit(maxMessageSize) + c.ws.SetReadDeadline(time.Now().Add(pongWait)) + c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + for { + _, message, err := c.ws.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) { + log.Info("error: %v", err) + } + break + } + h.broadcast <- message + } +} + +func (c *connection) write(mt int, payload []byte) error { + c.ws.SetWriteDeadline(time.Now().Add(writeWait)) + return c.ws.WriteMessage(mt, payload) +} + +// writePump pumps messages from the hub to the websocket connection. +func (c *connection) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.ws.Close() + }() + for { + select { + case message, ok := <-c.send: + if !ok { + c.write(websocket.CloseMessage, []byte{}) + return + } + if err := c.write(websocket.TextMessage, message); err != nil { + return + } + case <-ticker.C: + if err := c.write(websocket.PingMessage, []byte{}); err != nil { + return + } + } + } +} + +type LiveConn struct { +} + +func New() *LiveConn { + go h.run() + return &LiveConn{} +} + +func (lc *LiveConn) Serve(w http.ResponseWriter, r *http.Request) { + log.Info("Live: Upgrading to WebSocket") + + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Error(3, "Live: Failed to upgrade connection to WebSocket", err) + return + } + c := &connection{send: make(chan []byte, 256), ws: ws} + h.register <- c + go c.writePump() + c.readPump() +} diff --git a/pkg/live/hub.go b/pkg/live/hub.go new file mode 100644 index 00000000000..9e276eb46d1 --- /dev/null +++ b/pkg/live/hub.go @@ -0,0 +1,52 @@ +package live + +import "github.com/grafana/grafana/pkg/log" + +type hub struct { + // Registered connections. + connections map[*connection]bool + + // Inbound messages from the connections. + broadcast chan []byte + + // Register requests from the connections. + register chan *connection + + // Unregister requests from connections. + unregister chan *connection +} + +var h = hub{ + broadcast: make(chan []byte), + register: make(chan *connection), + unregister: make(chan *connection), + connections: make(map[*connection]bool), +} + +func (h *hub) run() { + for { + select { + case c := <-h.register: + h.connections[c] = true + case c := <-h.unregister: + if _, ok := h.connections[c]; ok { + delete(h.connections, c) + close(c.send) + } + case m := <-h.broadcast: + log.Info("Live: broadcasting") + for c := range h.connections { + select { + case c.send <- m: + default: + close(c.send) + delete(h.connections, c) + } + } + } + } +} + +func SendMessage(message string) { + h.broadcast <- []byte(message) +} diff --git a/public/app/features/admin/admin.ts b/public/app/features/admin/admin.ts index 47757520afe..a76ed6ea6b1 100644 --- a/public/app/features/admin/admin.ts +++ b/public/app/features/admin/admin.ts @@ -19,7 +19,16 @@ class AdminSettingsCtrl { class AdminHomeCtrl { /** @ngInject **/ - constructor() {} + constructor() { + + var conn = new WebSocket("ws://localhost:3000/ws"); + conn.onclose = function(evt) { + console.log("Connection closed"); + }; + conn.onmessage = function(evt) { + console.log("message", evt.data); + }; + } } export class AdminStatsCtrl { From cf926134ef03d4a1ccbb44178660181ff40b8fd5 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Sat, 5 Mar 2016 13:15:49 -0800 Subject: [PATCH 003/268] 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 004/268] 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 005/268] 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 006/268] 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 007/268] 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 008/268] 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 009/268] 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 010/268] 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 fbd94fc6ceed57f9f719e613314e217ec8c8e33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Mar 2016 11:59:51 +0100 Subject: [PATCH 011/268] feat(websockets): inital work on websockets, #4355 --- pkg/api/api.go | 2 +- pkg/{ => api}/live/conn.go | 0 pkg/{ => api}/live/hub.go | 0 pkg/api/search.go | 6 ++-- public/app/core/core.ts | 2 ++ public/app/core/live/live_srv.ts | 31 +++++++++++++++++++ public/app/features/admin/admin.ts | 8 ----- .../plugins/datasource/stream/datasource.ts | 21 +++++++++++++ .../app/plugins/datasource/stream/module.ts | 15 +++++++++ .../stream/partials/query.editor.html | 8 +++++ .../app/plugins/datasource/stream/plugin.json | 8 +++++ 11 files changed, 88 insertions(+), 13 deletions(-) rename pkg/{ => api}/live/conn.go (100%) rename pkg/{ => api}/live/hub.go (100%) create mode 100644 public/app/core/live/live_srv.ts create mode 100644 public/app/plugins/datasource/stream/datasource.ts create mode 100644 public/app/plugins/datasource/stream/module.ts create mode 100644 public/app/plugins/datasource/stream/partials/query.editor.html create mode 100644 public/app/plugins/datasource/stream/plugin.json diff --git a/pkg/api/api.go b/pkg/api/api.go index b6cd4cd72bb..32c929a42e2 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -4,7 +4,7 @@ import ( "github.com/go-macaron/binding" "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/live" + "github.com/grafana/grafana/pkg/api/live" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "gopkg.in/macaron.v1" diff --git a/pkg/live/conn.go b/pkg/api/live/conn.go similarity index 100% rename from pkg/live/conn.go rename to pkg/api/live/conn.go diff --git a/pkg/live/hub.go b/pkg/api/live/hub.go similarity index 100% rename from pkg/live/hub.go rename to pkg/api/live/hub.go diff --git a/pkg/api/search.go b/pkg/api/search.go index 6123db5778c..5ec95971033 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -1,11 +1,11 @@ package api import ( + "strconv" + "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/live" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/services/search" - "strconv" ) func Search(c *middleware.Context) { @@ -43,6 +43,4 @@ func Search(c *middleware.Context) { } c.JSON(200, searchQuery.Result) - - live.SendMessage(query) } diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 32ddee02dc7..fac703ba5c2 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -28,6 +28,7 @@ import {infoPopover} from './components/info_popover'; import {colorPicker} from './components/colorpicker'; import {navbarDirective} from './components/navbar/navbar'; import {arrayJoin} from './directives/array_join'; +import {liveSrv} from './live/live_srv'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -42,5 +43,6 @@ export { navbarDirective, searchDirective, colorPicker, + liveSrv, infoPopover }; diff --git a/public/app/core/live/live_srv.ts b/public/app/core/live/live_srv.ts new file mode 100644 index 00000000000..d417597e1ea --- /dev/null +++ b/public/app/core/live/live_srv.ts @@ -0,0 +1,31 @@ +/// + +import config from 'app/core/config'; +import coreModule from 'app/core/core_module'; + +export class LiveSrv { + conn: any; + + init() { + this.conn = new WebSocket("ws://localhost:3000/ws"); + this.conn.onclose = function(evt) { + console.log("WebSocket closed"); + }; + this.conn.onmessage = function(evt) { + console.log("WebSocket message", evt.data); + }; + this.conn.onopen = function(evt) { + console.log("Connection opened"); + }; + } + + subscribe(name) { + if (!this.conn) { + this.init(); + } + } + +} + +var instance = new LiveSrv(); +export {instance as liveSrv}; diff --git a/public/app/features/admin/admin.ts b/public/app/features/admin/admin.ts index a76ed6ea6b1..b93fd07a059 100644 --- a/public/app/features/admin/admin.ts +++ b/public/app/features/admin/admin.ts @@ -20,14 +20,6 @@ class AdminSettingsCtrl { class AdminHomeCtrl { /** @ngInject **/ constructor() { - - var conn = new WebSocket("ws://localhost:3000/ws"); - conn.onclose = function(evt) { - console.log("Connection closed"); - }; - conn.onmessage = function(evt) { - console.log("message", evt.data); - }; } } diff --git a/public/app/plugins/datasource/stream/datasource.ts b/public/app/plugins/datasource/stream/datasource.ts new file mode 100644 index 00000000000..ae2bb87dbef --- /dev/null +++ b/public/app/plugins/datasource/stream/datasource.ts @@ -0,0 +1,21 @@ +/// + +import {liveSrv} from 'app/core/core'; + +export class GrafanaStreamDS { + + /** @ngInject */ + constructor(private $q) { + + } + + query(options) { + if (options.targets.length === 0) { + return Promise.resolve({data: []}); + } + + var target = options.targets[0]; + liveSrv.subscribe(target); + } +} + diff --git a/public/app/plugins/datasource/stream/module.ts b/public/app/plugins/datasource/stream/module.ts new file mode 100644 index 00000000000..b17abd02feb --- /dev/null +++ b/public/app/plugins/datasource/stream/module.ts @@ -0,0 +1,15 @@ +/// + +import angular from 'angular'; +import {GrafanaStreamDS} from './datasource'; +import {QueryCtrl} from 'app/plugins/sdk'; + +class GrafanaQueryCtrl extends QueryCtrl { + static templateUrl = 'partials/query.editor.html'; +} + +export { + GrafanaStreamDS as Datasource, + GrafanaQueryCtrl as QueryCtrl, +}; + diff --git a/public/app/plugins/datasource/stream/partials/query.editor.html b/public/app/plugins/datasource/stream/partials/query.editor.html new file mode 100644 index 00000000000..6a8554d017d --- /dev/null +++ b/public/app/plugins/datasource/stream/partials/query.editor.html @@ -0,0 +1,8 @@ + +
  • + Stream Expression +
  • +
  • + +
  • +
    diff --git a/public/app/plugins/datasource/stream/plugin.json b/public/app/plugins/datasource/stream/plugin.json new file mode 100644 index 00000000000..df5fee586ed --- /dev/null +++ b/public/app/plugins/datasource/stream/plugin.json @@ -0,0 +1,8 @@ +{ + "type": "datasource", + "name": "Grafana Stream DS", + "id": "grafana-stream-ds", + + "builtIn": true, + "metrics": true +} From 2adc4d12be3f87d27400291a972185cb05797f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Mar 2016 13:20:55 +0100 Subject: [PATCH 012/268] feat(live): work on websocket data source, #3455 --- pkg/api/live/conn.go | 42 +++++++++++++-- pkg/api/live/hub.go | 1 + .../app/core/directives/plugin_component.ts | 6 ++- public/app/core/live/live_srv.ts | 53 ++++++++++++++----- .../{stream => grafana-live}/datasource.ts | 4 +- .../{stream => grafana-live}/module.ts | 0 .../partials/query.editor.html | 4 +- .../datasource/grafana-live/plugin.json | 7 +++ .../app/plugins/datasource/stream/plugin.json | 8 --- 9 files changed, 96 insertions(+), 29 deletions(-) rename public/app/plugins/datasource/{stream => grafana-live}/datasource.ts (80%) rename public/app/plugins/datasource/{stream => grafana-live}/module.ts (100%) rename public/app/plugins/datasource/{stream => grafana-live}/partials/query.editor.html (78%) create mode 100644 public/app/plugins/datasource/grafana-live/plugin.json delete mode 100644 public/app/plugins/datasource/stream/plugin.json diff --git a/pkg/api/live/conn.go b/pkg/api/live/conn.go index 90ce0af9cae..4f5df016dcd 100644 --- a/pkg/api/live/conn.go +++ b/pkg/api/live/conn.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" ) @@ -25,11 +26,27 @@ const ( var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +type subscription struct { + name string } type connection struct { - ws *websocket.Conn - send chan []byte + ws *websocket.Conn + streams []*subscription + send chan []byte +} + +func newConnection(ws *websocket.Conn) *connection { + return &connection{ + send: make(chan []byte, 256), + streams: make([]*subscription, 0), + ws: ws, + } } func (c *connection) readPump() { @@ -48,7 +65,24 @@ func (c *connection) readPump() { } break } - h.broadcast <- message + + c.handleMessage(message) + } +} + +func (c *connection) handleMessage(message []byte) { + json, err := simplejson.NewJson(message) + if err != nil { + log.Error(3, "Unreadable message on websocket channel:", err) + } + + msgType := json.Get("action").MustString() + streamName := json.Get("stream").MustString() + + switch msgType { + case "subscribe": + c.streams = append(c.streams, &subscription{name: streamName}) + log.Info("Live: subscribing to stream %v", streamName) } } @@ -98,7 +132,7 @@ func (lc *LiveConn) Serve(w http.ResponseWriter, r *http.Request) { log.Error(3, "Live: Failed to upgrade connection to WebSocket", err) return } - c := &connection{send: make(chan []byte, 256), ws: ws} + c := newConnection(ws) h.register <- c go c.writePump() c.readPump() diff --git a/pkg/api/live/hub.go b/pkg/api/live/hub.go index 9e276eb46d1..c776cea7f34 100644 --- a/pkg/api/live/hub.go +++ b/pkg/api/live/hub.go @@ -28,6 +28,7 @@ func (h *hub) run() { select { case c := <-h.register: h.connections[c] = true + log.Info("Live: New connection (Total count: %v)", len(h.connections)) case c := <-h.unregister: if _, ok := h.connections[c]; ok { delete(h.connections, c) diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 69535327cc8..9dc0f86897f 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -149,7 +149,11 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ // ConfigCtrl case 'datasource-config-ctrl': { var dsMeta = scope.ctrl.datasourceMeta; - return System.import(dsMeta.module).then(function(dsModule) { + return System.import(dsMeta.module).then(function(dsModule): any { + if (!dsMeta.ConfigCtrl) { + return {notFound: true}; + } + return { baseUrl: dsMeta.baseUrl, name: 'ds-config-' + dsMeta.id, diff --git a/public/app/core/live/live_srv.ts b/public/app/core/live/live_srv.ts index d417597e1ea..df4fb220932 100644 --- a/public/app/core/live/live_srv.ts +++ b/public/app/core/live/live_srv.ts @@ -5,24 +5,51 @@ import coreModule from 'app/core/core_module'; export class LiveSrv { conn: any; + initPromise: any; + + getWebSocketUrl() { + var l = window.location; + return ((l.protocol === "https:") ? "wss://" : "ws://") + l.host + config.appSubUrl + '/ws'; + } init() { - this.conn = new WebSocket("ws://localhost:3000/ws"); - this.conn.onclose = function(evt) { - console.log("WebSocket closed"); - }; - this.conn.onmessage = function(evt) { - console.log("WebSocket message", evt.data); - }; - this.conn.onopen = function(evt) { - console.log("Connection opened"); - }; + if (this.initPromise) { + return this.initPromise; + } + + if (this.conn && this.conn.readyState === 1) { + return Promise.resolve(); + } + + this.initPromise = new Promise((resolve, reject) => { + console.log('Live: connecting...'); + this.conn = new WebSocket(this.getWebSocketUrl()); + + this.conn.onclose = function(evt) { + reject({message: 'Connection closed'}); + }; + + this.conn.onmessage = function(evt) { + console.log("Live: message received:", evt.data); + }; + + this.conn.onopen = function(evt) { + console.log('Live: connection open'); + resolve(); + }; + }); + + return this.initPromise; + } + + send(data) { + this.conn.send(JSON.stringify(data)); } subscribe(name) { - if (!this.conn) { - this.init(); - } + return this.init().then(() => { + this.send({action: 'subscribe', stream: name}); + }); } } diff --git a/public/app/plugins/datasource/stream/datasource.ts b/public/app/plugins/datasource/grafana-live/datasource.ts similarity index 80% rename from public/app/plugins/datasource/stream/datasource.ts rename to public/app/plugins/datasource/grafana-live/datasource.ts index ae2bb87dbef..b62e64b6793 100644 --- a/public/app/plugins/datasource/stream/datasource.ts +++ b/public/app/plugins/datasource/grafana-live/datasource.ts @@ -15,7 +15,9 @@ export class GrafanaStreamDS { } var target = options.targets[0]; - liveSrv.subscribe(target); + liveSrv.subscribe(target.stream); + + return Promise.resolve({data: []}); } } diff --git a/public/app/plugins/datasource/stream/module.ts b/public/app/plugins/datasource/grafana-live/module.ts similarity index 100% rename from public/app/plugins/datasource/stream/module.ts rename to public/app/plugins/datasource/grafana-live/module.ts diff --git a/public/app/plugins/datasource/stream/partials/query.editor.html b/public/app/plugins/datasource/grafana-live/partials/query.editor.html similarity index 78% rename from public/app/plugins/datasource/stream/partials/query.editor.html rename to public/app/plugins/datasource/grafana-live/partials/query.editor.html index 6a8554d017d..912b28a6247 100644 --- a/public/app/plugins/datasource/stream/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana-live/partials/query.editor.html @@ -1,8 +1,8 @@
  • - Stream Expression + Stream
  • - +
  • diff --git a/public/app/plugins/datasource/grafana-live/plugin.json b/public/app/plugins/datasource/grafana-live/plugin.json new file mode 100644 index 00000000000..1f2ec204949 --- /dev/null +++ b/public/app/plugins/datasource/grafana-live/plugin.json @@ -0,0 +1,7 @@ +{ + "type": "datasource", + "name": "Grafana Live", + "id": "grafana-live", + + "metrics": true +} diff --git a/public/app/plugins/datasource/stream/plugin.json b/public/app/plugins/datasource/stream/plugin.json deleted file mode 100644 index df5fee586ed..00000000000 --- a/public/app/plugins/datasource/stream/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "datasource", - "name": "Grafana Stream DS", - "id": "grafana-stream-ds", - - "builtIn": true, - "metrics": true -} From 195be2742cc3288fd0101e88213be98eb71e1064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Mar 2016 16:18:07 +0100 Subject: [PATCH 013/268] feat(live): wip work --- pkg/api/api.go | 3 +++ pkg/api/dtos/stream.go | 9 +++++++++ pkg/api/live/conn.go | 22 ---------------------- pkg/api/live/hub.go | 7 ++++++- pkg/api/live/live.go | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 pkg/api/dtos/stream.go create mode 100644 pkg/api/live/live.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 32c929a42e2..d85ce6b569a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -244,6 +244,9 @@ func Register(r *macaron.Macaron) { liveConn := live.New() r.Any("/ws", liveConn.Serve) + // streams + r.Post("/streams/push", reqSignedIn, bind(dtos.StreamMessage{}), liveConn.PushToStream) + InitAppPluginRoutes(r) } diff --git a/pkg/api/dtos/stream.go b/pkg/api/dtos/stream.go new file mode 100644 index 00000000000..032588247b0 --- /dev/null +++ b/pkg/api/dtos/stream.go @@ -0,0 +1,9 @@ +package dtos + +import "encoding/json" + +type StreamMessage struct { + Stream string `json:"stream"` + Metric string `json:"name"` + Datapoints [][]json.Number `json:"username"` +} diff --git a/pkg/api/live/conn.go b/pkg/api/live/conn.go index 4f5df016dcd..b9a39f92fc6 100644 --- a/pkg/api/live/conn.go +++ b/pkg/api/live/conn.go @@ -115,25 +115,3 @@ func (c *connection) writePump() { } } } - -type LiveConn struct { -} - -func New() *LiveConn { - go h.run() - return &LiveConn{} -} - -func (lc *LiveConn) Serve(w http.ResponseWriter, r *http.Request) { - log.Info("Live: Upgrading to WebSocket") - - ws, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Error(3, "Live: Failed to upgrade connection to WebSocket", err) - return - } - c := newConnection(ws) - h.register <- c - go c.writePump() - c.readPump() -} diff --git a/pkg/api/live/hub.go b/pkg/api/live/hub.go index c776cea7f34..cd90eb90dc7 100644 --- a/pkg/api/live/hub.go +++ b/pkg/api/live/hub.go @@ -1,6 +1,9 @@ package live -import "github.com/grafana/grafana/pkg/log" +import ( + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/log" +) type hub struct { // Registered connections. @@ -14,6 +17,8 @@ type hub struct { // Unregister requests from connections. unregister chan *connection + + streamPipe chan *dtos.StreamMessage } var h = hub{ diff --git a/pkg/api/live/live.go b/pkg/api/live/live.go new file mode 100644 index 00000000000..e38b4df38ad --- /dev/null +++ b/pkg/api/live/live.go @@ -0,0 +1,35 @@ +package live + +import ( + "net/http" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/middleware" +) + +type LiveConn struct { +} + +func New() *LiveConn { + go h.run() + return &LiveConn{} +} + +func (lc *LiveConn) Serve(w http.ResponseWriter, r *http.Request) { + log.Info("Live: Upgrading to WebSocket") + + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Error(3, "Live: Failed to upgrade connection to WebSocket", err) + return + } + c := newConnection(ws) + h.register <- c + go c.writePump() + c.readPump() +} + +func (lc *LiveConn) PushToStream(c *middleware.Context, message dtos.StreamMessage) { + +} From 3d5251d9a51c29846edbac8671d76434e9f66038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Mar 2016 17:32:48 +0100 Subject: [PATCH 014/268] feat(websocket): more work websocket ds, # 4355 --- pkg/api/api.go | 2 +- pkg/api/dtos/stream.go | 4 +-- pkg/api/live/conn.go | 22 +++++++------- pkg/api/live/hub.go | 68 +++++++++++++++++++++++++++--------------- pkg/api/live/live.go | 3 +- 5 files changed, 59 insertions(+), 40 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index d85ce6b569a..761f9244ea7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -245,7 +245,7 @@ func Register(r *macaron.Macaron) { r.Any("/ws", liveConn.Serve) // streams - r.Post("/streams/push", reqSignedIn, bind(dtos.StreamMessage{}), liveConn.PushToStream) + r.Post("/api/streams/push", reqSignedIn, bind(dtos.StreamMessage{}), liveConn.PushToStream) InitAppPluginRoutes(r) diff --git a/pkg/api/dtos/stream.go b/pkg/api/dtos/stream.go index 032588247b0..9381c936a77 100644 --- a/pkg/api/dtos/stream.go +++ b/pkg/api/dtos/stream.go @@ -4,6 +4,6 @@ import "encoding/json" type StreamMessage struct { Stream string `json:"stream"` - Metric string `json:"name"` - Datapoints [][]json.Number `json:"username"` + Metric string `json:"metric"` + Datapoints [][]json.Number `json:"Datapoints"` } diff --git a/pkg/api/live/conn.go b/pkg/api/live/conn.go index b9a39f92fc6..dcb3cc2b465 100644 --- a/pkg/api/live/conn.go +++ b/pkg/api/live/conn.go @@ -31,21 +31,15 @@ var upgrader = websocket.Upgrader{ }, } -type subscription struct { - name string -} - type connection struct { - ws *websocket.Conn - streams []*subscription - send chan []byte + ws *websocket.Conn + send chan []byte } func newConnection(ws *websocket.Conn) *connection { return &connection{ - send: make(chan []byte, 256), - streams: make([]*subscription, 0), - ws: ws, + send: make(chan []byte, 256), + ws: ws, } } @@ -79,10 +73,14 @@ func (c *connection) handleMessage(message []byte) { msgType := json.Get("action").MustString() streamName := json.Get("stream").MustString() + if len(streamName) == 0 { + log.Error(3, "Not allowed to subscribe to empty stream name") + return + } + switch msgType { case "subscribe": - c.streams = append(c.streams, &subscription{name: streamName}) - log.Info("Live: subscribing to stream %v", streamName) + h.subChannel <- &streamSubscription{name: streamName, conn: c} } } diff --git a/pkg/api/live/hub.go b/pkg/api/live/hub.go index cd90eb90dc7..c266d69fb33 100644 --- a/pkg/api/live/hub.go +++ b/pkg/api/live/hub.go @@ -2,30 +2,36 @@ package live import ( "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" ) type hub struct { - // Registered connections. connections map[*connection]bool + streams map[string]map[*connection]bool - // Inbound messages from the connections. - broadcast chan []byte + register chan *connection + unregister chan *connection + streamChannel chan *dtos.StreamMessage + subChannel chan *streamSubscription +} - // Register requests from the connections. - register chan *connection - - // Unregister requests from connections. - unregister chan *connection - - streamPipe chan *dtos.StreamMessage +type streamSubscription struct { + conn *connection + name string } var h = hub{ - broadcast: make(chan []byte), - register: make(chan *connection), - unregister: make(chan *connection), - connections: make(map[*connection]bool), + connections: make(map[*connection]bool), + streams: make(map[string]map[*connection]bool), + register: make(chan *connection), + unregister: make(chan *connection), + streamChannel: make(chan *dtos.StreamMessage), + subChannel: make(chan *streamSubscription), +} + +func (h *hub) removeConnection() { + } func (h *hub) run() { @@ -39,20 +45,34 @@ func (h *hub) run() { delete(h.connections, c) close(c.send) } - case m := <-h.broadcast: - log.Info("Live: broadcasting") - for c := range h.connections { + // hand stream subscriptions + case sub := <-h.subChannel: + log.Info("Live: Connection subscribing to: %v", sub.name) + subscribers, exists := h.streams[sub.name] + if !exists { + subscribers = make(map[*connection]bool) + h.streams[sub.name] = subscribers + } + subscribers[sub.conn] = true + + // handle stream messages + case message := <-h.streamChannel: + subscribers, exists := h.streams[message.Stream] + if !exists { + log.Info("Live: Message to stream without subscribers: %v", message.Stream) + continue + } + + messageBytes, _ := simplejson.NewFromAny(message).Encode() + for sub := range subscribers { select { - case c.send <- m: + case sub.send <- messageBytes: default: - close(c.send) - delete(h.connections, c) + close(sub.send) + delete(h.connections, sub) + delete(subscribers, sub) } } } } } - -func SendMessage(message string) { - h.broadcast <- []byte(message) -} diff --git a/pkg/api/live/live.go b/pkg/api/live/live.go index e38b4df38ad..4a309740590 100644 --- a/pkg/api/live/live.go +++ b/pkg/api/live/live.go @@ -31,5 +31,6 @@ func (lc *LiveConn) Serve(w http.ResponseWriter, r *http.Request) { } func (lc *LiveConn) PushToStream(c *middleware.Context, message dtos.StreamMessage) { - + h.streamChannel <- &message + c.JsonOK("Message recevived") } From 92f20b9b7d374b9bd0dd3fc2a5aabace5ef4f40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Mar 2016 19:21:32 +0100 Subject: [PATCH 015/268] feat(websocket): reconnection and resubscription handling, #4355 --- package.json | 2 +- public/app/core/live/live_srv.ts | 80 ++++++++++++++++--- .../datasource/grafana-live/datasource.ts | 6 +- 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 1c8af93eb5f..1dea1e79445 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "mocha": "2.3.4", "phantomjs": "^1.9.19", "reflect-metadata": "0.1.2", - "rxjs": "5.0.0-beta.0", + "rxjs": "5.0.0-beta.2", "sass-lint": "^1.5.0", "systemjs": "0.19.20", "zone.js": "0.5.10" diff --git a/public/app/core/live/live_srv.ts b/public/app/core/live/live_srv.ts index df4fb220932..b6954bdcffc 100644 --- a/public/app/core/live/live_srv.ts +++ b/public/app/core/live/live_srv.ts @@ -1,57 +1,117 @@ /// +import _ from 'lodash'; import config from 'app/core/config'; import coreModule from 'app/core/core_module'; +import {Observable} from 'vendor/npm/rxjs/Observable'; + export class LiveSrv { conn: any; + observers: any; initPromise: any; + constructor() { + this.observers = {}; + } + getWebSocketUrl() { var l = window.location; return ((l.protocol === "https:") ? "wss://" : "ws://") + l.host + config.appSubUrl + '/ws'; } - init() { + getConnection() { if (this.initPromise) { return this.initPromise; } if (this.conn && this.conn.readyState === 1) { - return Promise.resolve(); + return Promise.resolve(this.conn); } this.initPromise = new Promise((resolve, reject) => { console.log('Live: connecting...'); this.conn = new WebSocket(this.getWebSocketUrl()); - this.conn.onclose = function(evt) { + this.conn.onclose = (evt) => { + console.log("Live: websocket onclose", evt); reject({message: 'Connection closed'}); + + this.initPromise = null; + setTimeout(this.reconnect.bind(this), 2000); }; - this.conn.onmessage = function(evt) { + this.conn.onmessage = (evt) => { console.log("Live: message received:", evt.data); }; - this.conn.onopen = function(evt) { - console.log('Live: connection open'); - resolve(); + this.conn.onerror = (evt) => { + this.initPromise = null; + reject({message: 'Connection error'}); + console.log("Live: websocket error", evt); + }; + + this.conn.onopen = (evt) => { + console.log('opened'); + this.initPromise = null; + resolve(this.conn); }; }); return this.initPromise; } + reconnect() { + // no need to reconnect if no one cares + if (_.keys(this.observers).length === 0) { + return; + } + + console.log('LiveSrv: Reconnecting'); + + this.getConnection().then(conn => { + _.each(this.observers, (value, key) => { + this.send({action: 'subscribe', stream: key}); + }); + }); + } + send(data) { this.conn.send(JSON.stringify(data)); } - subscribe(name) { - return this.init().then(() => { - this.send({action: 'subscribe', stream: name}); + addObserver(stream, observer) { + this.observers[stream] = observer; + + this.getConnection().then(conn => { + this.send({action: 'subscribe', stream: stream}); }); } + removeObserver(stream, observer) { + delete this.observers[stream]; + + this.getConnection().then(conn => { + this.send({action: 'unsubscribe', stream: stream}); + }); + } + + subscribe(streamName) { + console.log('LiveSrv.subscribe: ' + streamName); + + return Observable.create(observer => { + this.addObserver(streamName, observer); + + return () => { + this.removeObserver(streamName, observer); + }; + }); + + // return this.init().then(() => { + // this.send({action: 'subscribe', stream: name}); + // }); + } + } var instance = new LiveSrv(); diff --git a/public/app/plugins/datasource/grafana-live/datasource.ts b/public/app/plugins/datasource/grafana-live/datasource.ts index b62e64b6793..13ee30d7ddb 100644 --- a/public/app/plugins/datasource/grafana-live/datasource.ts +++ b/public/app/plugins/datasource/grafana-live/datasource.ts @@ -3,6 +3,7 @@ import {liveSrv} from 'app/core/core'; export class GrafanaStreamDS { + subscription: any; /** @ngInject */ constructor(private $q) { @@ -15,7 +16,10 @@ export class GrafanaStreamDS { } var target = options.targets[0]; - liveSrv.subscribe(target.stream); + var observable = liveSrv.subscribe(target.stream); + this.subscription = observable.subscribe(data => { + console.log("grafana stream ds data!", data); + }); return Promise.resolve({data: []}); } From 2e1f26096f9e713326e5ea69e7376927a8d4a84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Mar 2016 22:26:43 +0100 Subject: [PATCH 016/268] feat(grafana_live): more work pushing data to websocket, #4355 --- pkg/api/dtos/stream.go | 10 +++++--- pkg/api/live/conn.go | 3 +++ pkg/api/live/hub.go | 24 +++++++++++++++---- public/app/core/live/live_srv.ts | 20 +++++++++++++++- .../datasource/grafana-live/datasource.ts | 16 ++++++++++++- 5 files changed, 64 insertions(+), 9 deletions(-) diff --git a/pkg/api/dtos/stream.go b/pkg/api/dtos/stream.go index 9381c936a77..026ff79c1e8 100644 --- a/pkg/api/dtos/stream.go +++ b/pkg/api/dtos/stream.go @@ -3,7 +3,11 @@ package dtos import "encoding/json" type StreamMessage struct { - Stream string `json:"stream"` - Metric string `json:"metric"` - Datapoints [][]json.Number `json:"Datapoints"` + Stream string `json:"stream"` + Series []StreamMessageSeries `json:"series"` +} + +type StreamMessageSeries struct { + Name string `json:"name"` + Datapoints [][]json.Number `json:"datapoints"` } diff --git a/pkg/api/live/conn.go b/pkg/api/live/conn.go index dcb3cc2b465..d474fc48a1f 100644 --- a/pkg/api/live/conn.go +++ b/pkg/api/live/conn.go @@ -81,7 +81,10 @@ func (c *connection) handleMessage(message []byte) { switch msgType { case "subscribe": h.subChannel <- &streamSubscription{name: streamName, conn: c} + case "unsubscribe": + h.subChannel <- &streamSubscription{name: streamName, conn: c, remove: true} } + } func (c *connection) write(mt int, payload []byte) error { diff --git a/pkg/api/live/hub.go b/pkg/api/live/hub.go index c266d69fb33..736f848db2c 100644 --- a/pkg/api/live/hub.go +++ b/pkg/api/live/hub.go @@ -17,8 +17,9 @@ type hub struct { } type streamSubscription struct { - conn *connection - name string + conn *connection + name string + remove bool } var h = hub{ @@ -40,15 +41,24 @@ func (h *hub) run() { case c := <-h.register: h.connections[c] = true log.Info("Live: New connection (Total count: %v)", len(h.connections)) + case c := <-h.unregister: if _, ok := h.connections[c]; ok { + log.Info("Live: Closing Connection (Total count: %v)", len(h.connections)) delete(h.connections, c) close(c.send) } // hand stream subscriptions case sub := <-h.subChannel: - log.Info("Live: Connection subscribing to: %v", sub.name) + log.Info("Live: Subscribing to: %v, remove: %v", sub.name, sub.remove) subscribers, exists := h.streams[sub.name] + + // handle unsubscribe + if exists && sub.remove { + delete(subscribers, sub.conn) + continue + } + if !exists { subscribers = make(map[*connection]bool) h.streams[sub.name] = subscribers @@ -58,13 +68,19 @@ func (h *hub) run() { // handle stream messages case message := <-h.streamChannel: subscribers, exists := h.streams[message.Stream] - if !exists { + if !exists || len(subscribers) == 0 { log.Info("Live: Message to stream without subscribers: %v", message.Stream) continue } messageBytes, _ := simplejson.NewFromAny(message).Encode() for sub := range subscribers { + // check if channel is open + if _, ok := h.connections[sub]; !ok { + delete(subscribers, sub) + continue + } + select { case sub.send <- messageBytes: default: diff --git a/public/app/core/live/live_srv.ts b/public/app/core/live/live_srv.ts index b6954bdcffc..12ea8ff4266 100644 --- a/public/app/core/live/live_srv.ts +++ b/public/app/core/live/live_srv.ts @@ -42,7 +42,7 @@ export class LiveSrv { }; this.conn.onmessage = (evt) => { - console.log("Live: message received:", evt.data); + this.handleMessage(evt.data); }; this.conn.onerror = (evt) => { @@ -61,6 +61,23 @@ export class LiveSrv { return this.initPromise; } + handleMessage(message) { + message = JSON.parse(message); + + if (!message.stream) { + console.log("Error: stream message without stream!", message); + return; + } + + var observer = this.observers[message.stream]; + if (!observer) { + this.removeObserver(message.stream, null); + return; + } + + observer.next(message); + } + reconnect() { // no need to reconnect if no one cares if (_.keys(this.observers).length === 0) { @@ -89,6 +106,7 @@ export class LiveSrv { } removeObserver(stream, observer) { + console.log('unsubscribe', stream); delete this.observers[stream]; this.getConnection().then(conn => { diff --git a/public/app/plugins/datasource/grafana-live/datasource.ts b/public/app/plugins/datasource/grafana-live/datasource.ts index 13ee30d7ddb..0864057a762 100644 --- a/public/app/plugins/datasource/grafana-live/datasource.ts +++ b/public/app/plugins/datasource/grafana-live/datasource.ts @@ -2,11 +2,13 @@ import {liveSrv} from 'app/core/core'; +import {Observable} from 'vendor/npm/rxjs/Observable'; + export class GrafanaStreamDS { subscription: any; /** @ngInject */ - constructor(private $q) { + constructor() { } @@ -16,11 +18,23 @@ export class GrafanaStreamDS { } var target = options.targets[0]; + + if (this.subscription) { + if (this.subscription.stream !== target.stream) { + this.subscription.unsubscribe(); + } else { + return Promise.resolve({data: []}); + } + } + var observable = liveSrv.subscribe(target.stream); + this.subscription = observable.subscribe(data => { console.log("grafana stream ds data!", data); }); + this.subscription.stream = target.stream; + return Promise.resolve({data: []}); } } 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 017/268] 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 a738945a888fc6b8e3fc9c263aa7a4580039727a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 16 Mar 2016 14:19:30 +0100 Subject: [PATCH 018/268] feat(data source observerable): started work on handling data source observerable --- .../app/features/panel/metrics_panel_ctrl.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index e1ad782fa74..51380653b7b 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -26,6 +26,7 @@ class MetricsPanelCtrl extends PanelCtrl { timeInfo: any; skipDataOnInit: boolean; datasources: any[]; + dataSubscription: any; constructor($scope, $injector) { super($scope, $injector); @@ -182,6 +183,12 @@ class MetricsPanelCtrl extends PanelCtrl { return datasource.query(metricsQuery).then(results => { this.setTimeQueryEnd(); + // check for if data source returns observable + if (results && results.subscribe) { + this.handleObservable(results); + return {data: []}; + } + if (this.dashboard.snapshot) { this.panel.snapshotData = results; } @@ -193,6 +200,20 @@ class MetricsPanelCtrl extends PanelCtrl { } } + handleObservable(observable) { + this.dataSubscription = observable.subscribe({ + next: (data) => { + console.log('panel: observer got data'); + }, + error: (error) => { + console.log('panel: observer got error'); + }, + complete: () => { + console.log('panel: observer got complete'); + } + }); + } + setDatasource(datasource) { // switching to mixed if (datasource.meta.mixed) { From 79a803ea8ff40e982f99ace1f9eeeb6a45648b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 16 Mar 2016 15:59:21 +0100 Subject: [PATCH 019/268] feat(ds observable): minor progress on handling data source observable --- public/app/features/panel/metrics_panel_ctrl.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 51380653b7b..227e91753d7 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -27,6 +27,7 @@ class MetricsPanelCtrl extends PanelCtrl { skipDataOnInit: boolean; datasources: any[]; dataSubscription: any; + dataHandler: any; constructor($scope, $injector) { super($scope, $injector); @@ -203,7 +204,10 @@ class MetricsPanelCtrl extends PanelCtrl { handleObservable(observable) { this.dataSubscription = observable.subscribe({ next: (data) => { - console.log('panel: observer got data'); + if (data.range) { + this.range = data.range; + } + this.dataHandler(data); }, error: (error) => { console.log('panel: observer got error'); From 749fd618a949ff5a49b1fe293710fffcf3213f03 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Wed, 16 Mar 2016 23:22:27 -0700 Subject: [PATCH 020/268] 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 021/268] 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 022/268] 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 023/268] 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 024/268] 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 6016033b8b11a17f48a8beab49c3adba7d0c77d5 Mon Sep 17 00:00:00 2001 From: Matt Toback Date: Thu, 17 Mar 2016 15:14:26 -0400 Subject: [PATCH 025/268] Finishing touches --- public/app/features/org/partials/newOrg.html | 28 +++++++++---------- .../app/features/org/partials/orgDetails.html | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/public/app/features/org/partials/newOrg.html b/public/app/features/org/partials/newOrg.html index 916c73177b7..88c348db78f 100644 --- a/public/app/features/org/partials/newOrg.html +++ b/public/app/features/org/partials/newOrg.html @@ -1,21 +1,21 @@ -
    -
    +
    + -

    Add Organization

    +

    Each organization contains their own dashboards, data sources and configuration, and cannot be shared between orgs. While users may belong to more than one, mutiple organization are most frequently used in multi-tenant deployments.

    - -
    - Org. name - -
    -
    -
    - -
    - +
    +
    + Org. name + +
    +
    +
    + +
    - diff --git a/public/app/features/org/partials/orgDetails.html b/public/app/features/org/partials/orgDetails.html index d0f99e23eb7..fe988570e50 100644 --- a/public/app/features/org/partials/orgDetails.html +++ b/public/app/features/org/partials/orgDetails.html @@ -3,7 +3,7 @@

    General

    From 1ccde201be827438cdde36ec0735bbb5672d1a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Mar 2016 13:52:26 +0100 Subject: [PATCH 026/268] feat(live datasouces): moved from Observable to Subject --- .../app/features/panel/metrics_panel_ctrl.ts | 29 ++++++++++++++----- public/app/plugins/panel/graph/module.ts | 4 +-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 227e91753d7..a8f1079868c 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -26,8 +26,7 @@ class MetricsPanelCtrl extends PanelCtrl { timeInfo: any; skipDataOnInit: boolean; datasources: any[]; - dataSubscription: any; - dataHandler: any; + dataSubject: any; constructor($scope, $injector) { super($scope, $injector); @@ -168,7 +167,12 @@ class MetricsPanelCtrl extends PanelCtrl { return this.$q.when([]); } + if (this.dataSubject) { + return this.$q.when([]); + } + var metricsQuery = { + panelId: this.panel.id, range: this.range, rangeRaw: this.rangeRaw, interval: this.interval, @@ -184,9 +188,9 @@ class MetricsPanelCtrl extends PanelCtrl { return datasource.query(metricsQuery).then(results => { this.setTimeQueryEnd(); - // check for if data source returns observable + // check for if data source returns subject if (results && results.subscribe) { - this.handleObservable(results); + this.handleDataSubject(results); return {data: []}; } @@ -194,16 +198,27 @@ class MetricsPanelCtrl extends PanelCtrl { this.panel.snapshotData = results; } - return results; + return this.dataHandler(results); }); } catch (err) { return this.$q.reject(err); } } - handleObservable(observable) { - this.dataSubscription = observable.subscribe({ + dataHandler(data) { + return data; + } + + handleDataSubject(subject) { + // if we already have a connection + if (this.dataSubject) { + return; + } + + this.dataSubject = subject; + this.dataSubject.subscribe({ next: (data) => { + console.log('dataSubject next!'); if (data.range) { this.range = data.range; } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 201d3f3e8c8..3f4fa833460 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -140,9 +140,7 @@ class GraphCtrl extends MetricsPanelCtrl { refreshData(datasource) { this.annotationsPromise = this.annotationsSrv.getAnnotations(this.dashboard); - return this.issueQueries(datasource) - .then(res => this.dataHandler(res)) - .catch(err => { + return this.issueQueries(datasource).catch(err => { this.seriesList = []; this.render([]); throw err; From e63ff1c7626fb06d42f216ef7a9edf751bcd0dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 19 Mar 2016 11:09:26 +0100 Subject: [PATCH 027/268] feat(dataproxy): set flush interval, need a setting for this --- pkg/api/dataproxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 6e6b4394e2d..eb88045cd51 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -61,7 +61,7 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *ht req.Header.Del("Set-Cookie") } - return &httputil.ReverseProxy{Director: director} + return &httputil.ReverseProxy{Director: director, FlushInterval: time.Millisecond * 200} } func getDatasource(id int64, orgId int64) (*m.DataSource, error) { 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 028/268] 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 029/268] 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 7f8643efdec070d34d4fce40ab0ac31e6e0566d4 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 Mar 2016 10:01:07 +0100 Subject: [PATCH 030/268] feat(cli): make all plugin commands subcommands --- docs/sources/plugins/installation.md | 12 ++++++------ pkg/cmd/grafana-cli/commands/commands.go | 10 +++++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/sources/plugins/installation.md b/docs/sources/plugins/installation.md index d3a8013ce2b..7a4ed1889f9 100644 --- a/docs/sources/plugins/installation.md +++ b/docs/sources/plugins/installation.md @@ -17,30 +17,30 @@ On Linux systems the grafana-cli will assume that the grafana plugin directory i List available plugins ``` -grafana-cli list-remote +grafana-cli plugins list-remote ``` Install a plugin type ``` -grafana-cli install +grafana-cli plugins install ``` List installed plugins ``` -grafana-cli ls +grafana-cli plugins ls ``` Upgrade all installed plugins ``` -grafana-cli upgrade-all +grafana-cli plugins upgrade-all ``` Upgrade one plugin ``` -grafana-cli upgrade +grafana-cli plugins upgrade ``` Remove one plugin ``` -grafana-cli remove +grafana-cli plugins remove ``` diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index f1b36c90ef2..55a0e2660ec 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -22,7 +22,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C } } -var Commands = []cli.Command{ +var pluginCommands = []cli.Command{ { Name: "install", Usage: "install ", @@ -49,3 +49,11 @@ var Commands = []cli.Command{ Action: runCommand(removeCommand), }, } + +var Commands = []cli.Command{ + { + Name: "plugins", + Usage: "Manage plugins for grafana", + Subcommands: pluginCommands, + }, +} From 14df3c62494a3d3c9e2eb28acd6199b7c14e596f Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 Mar 2016 10:05:23 +0100 Subject: [PATCH 031/268] feat(cli): use built in envvar support --- pkg/cmd/grafana-cli/main.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index b277714fe9b..2b3b94f1751 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -12,10 +12,6 @@ import ( var version = "master" func getGrafanaPluginPath() string { - if os.Getenv("GF_PLUGIN_DIR") != "" { - return os.Getenv("GF_PLUGIN_DIR") - } - os := runtime.GOOS if os == "windows" { return "C:\\opt\\grafana\\plugins" @@ -34,14 +30,16 @@ func main() { app.Version = version app.Flags = []cli.Flag{ cli.StringFlag{ - Name: "path", - Usage: "path to the grafana installation", - Value: getGrafanaPluginPath(), + Name: "path", + Usage: "path to the grafana installation", + Value: getGrafanaPluginPath(), + EnvVar: "GF_PLUGIN_DIR", }, cli.StringFlag{ - Name: "repo", - Usage: "url to the plugin repository", - Value: "https://grafana-net.raintank.io/api/plugins", + Name: "repo", + Usage: "url to the plugin repository", + Value: "https://grafana-net.raintank.io/api/plugins", + EnvVar: "GF_PLUGIN_REPO", }, cli.BoolFlag{ Name: "debug, d", From 3ea441d1a023b4dbc86043b28a59dc64b4a13891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 21 Mar 2016 10:42:10 +0100 Subject: [PATCH 032/268] feat(live): tricky --- .../datasource/grafana-live/datasource.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/public/app/plugins/datasource/grafana-live/datasource.ts b/public/app/plugins/datasource/grafana-live/datasource.ts index 0864057a762..d7e519355c1 100644 --- a/public/app/plugins/datasource/grafana-live/datasource.ts +++ b/public/app/plugins/datasource/grafana-live/datasource.ts @@ -4,6 +4,21 @@ import {liveSrv} from 'app/core/core'; import {Observable} from 'vendor/npm/rxjs/Observable'; +class DataObservable { + target: any; + + constructor(target) { + this.target = target; + } + + subscribe(options) { + var observable = liveSrv.subscribe(this.target.stream); + return observable.subscribe(data => { + console.log("grafana stream ds data!", data); + }); + } +} + export class GrafanaStreamDS { subscription: any; @@ -12,30 +27,15 @@ export class GrafanaStreamDS { } - query(options) { + query(options): any { if (options.targets.length === 0) { return Promise.resolve({data: []}); } var target = options.targets[0]; + var observable = new DataObservable(target); - if (this.subscription) { - if (this.subscription.stream !== target.stream) { - this.subscription.unsubscribe(); - } else { - return Promise.resolve({data: []}); - } - } - - var observable = liveSrv.subscribe(target.stream); - - this.subscription = observable.subscribe(data => { - console.log("grafana stream ds data!", data); - }); - - this.subscription.stream = target.stream; - - return Promise.resolve({data: []}); + return Promise.resolve(observable); } } 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 033/268] 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 034/268] 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 86a274a7eed37acb45f43c5e695e2bd6dce1b7d8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 Mar 2016 12:30:13 +0100 Subject: [PATCH 035/268] feat(backendsrv): improves error response handling datasourceRequests that could not reach the destination threw invalid errors due to missing property. This fixes gives the user a better error message. closes #4428 --- public/app/core/services/backend_srv.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/app/core/services/backend_srv.js b/public/app/core/services/backend_srv.js index 6d0d112ba26..c5638af167e 100644 --- a/public/app/core/services/backend_srv.js +++ b/public/app/core/services/backend_srv.js @@ -105,6 +105,13 @@ function (angular, _, coreModule, config) { }); } + //populate error obj on Internal Error + if (_.isString(err.data) && err.status === 500 && !err.data) { + err.data = { + error: err.statusText + }; + } + // for Prometheus if (!err.data.message && _.isString(err.data.error)) { err.data.message = err.data.error; From 90c6b04361edf9f08adb3ed955f48dca767b94ae Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 Mar 2016 12:43:30 +0100 Subject: [PATCH 036/268] fix(backendsrv): remove invalid check --- public/app/core/services/backend_srv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/services/backend_srv.js b/public/app/core/services/backend_srv.js index c5638af167e..ff3784ab45e 100644 --- a/public/app/core/services/backend_srv.js +++ b/public/app/core/services/backend_srv.js @@ -106,7 +106,7 @@ function (angular, _, coreModule, config) { } //populate error obj on Internal Error - if (_.isString(err.data) && err.status === 500 && !err.data) { + if (_.isString(err.data) && err.status === 500) { err.data = { error: err.statusText }; 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 037/268] 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 038/268] 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 65c0937741c769c511776fcf7c3092707052ef82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 21 Mar 2016 19:07:08 +0100 Subject: [PATCH 039/268] ux(): app navigation improvements, changes to plugin.json for apps, merged pages with includes section, #4434 --- pkg/api/dtos/plugins.go | 1 - pkg/api/index.go | 39 ++++++++++++++----- pkg/api/plugins.go | 4 -- pkg/plugins/app_plugin.go | 11 +----- pkg/plugins/models.go | 15 +++++-- public/app/core/components/navbar/navbar.html | 6 +-- .../core/components/sidemenu/sidemenu.html | 5 ++- public/app/features/panel/panel_menu.js | 1 + .../plugins/partials/plugin_edit.html | 4 -- .../plugins/partials/plugin_page.html | 2 +- .../app/features/plugins/plugin_page_ctrl.ts | 5 ++- 11 files changed, 53 insertions(+), 40 deletions(-) diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 88683155006..d03a55861fc 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -11,7 +11,6 @@ type PluginSetting struct { Module string `json:"module"` BaseUrl string `json:"baseUrl"` Info *plugins.PluginInfo `json:"info"` - Pages []*plugins.AppPluginPage `json:"pages"` Includes []*plugins.PluginInclude `json:"includes"` Dependencies *plugins.PluginDependencies `json:"dependencies"` JsonData map[string]interface{} `json:"jsonData"` diff --git a/pkg/api/index.go b/pkg/api/index.go index 691c50f04f4..b26fb22ea52 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -56,8 +56,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true}) - dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Url: setting.AppSubUrl + "/dashboard/new"}) - dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Url: setting.AppSubUrl + "/import/dashboard"}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/import/dashboard"}) } data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ @@ -88,22 +88,41 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { for _, plugin := range enabledPlugins.Apps { if plugin.Pinned { - pageLink := &dtos.NavLink{ + appLink := &dtos.NavLink{ Text: plugin.Name, Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit", Img: plugin.Info.Logos.Small, } - for _, page := range plugin.Pages { - if !page.SuppressNav { - pageLink.Children = append(pageLink.Children, &dtos.NavLink{ - Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + page.Slug, - Text: page.Name, - }) + for _, include := range plugin.Includes { + if include.Type == "page" && include.AddToNav { + link := &dtos.NavLink{ + Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + include.Slug, + Text: include.Name, + } + if include.DefaultNav { + appLink.Url = link.Url + } + appLink.Children = append(appLink.Children, link) + } + if include.Type == "dashboard" && include.AddToNav { + link := &dtos.NavLink{ + Url: setting.AppSubUrl + "/dashboard/db/" + include.Slug, + Text: include.Name, + } + if include.DefaultNav { + appLink.Url = link.Url + } + appLink.Children = append(appLink.Children, link) } } - data.MainNavLinks = append(data.MainNavLinks, pageLink) + if c.OrgRole == m.ROLE_ADMIN { + appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true}) + appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) + } + + data.MainNavLinks = append(data.MainNavLinks, appLink) } } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 56d0d99296c..793cf33e3c6 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -82,10 +82,6 @@ func GetPluginSettingById(c *middleware.Context) Response { Module: def.Module, } - if app, exists := plugins.Apps[pluginId]; exists { - dto.Pages = app.Pages - } - query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { if err != m.ErrPluginSettingNotFound { diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go index 7fc170784f3..ecf59a15e15 100644 --- a/pkg/plugins/app_plugin.go +++ b/pkg/plugins/app_plugin.go @@ -8,14 +8,6 @@ import ( "github.com/grafana/grafana/pkg/models" ) -type AppPluginPage struct { - Name string `json:"name"` - Slug string `json:"slug"` - Component string `json:"component"` - Role models.RoleType `json:"role"` - SuppressNav bool `json:"suppressNav"` -} - type AppPluginCss struct { Light string `json:"light"` Dark string `json:"dark"` @@ -23,7 +15,6 @@ type AppPluginCss struct { type AppPlugin struct { FrontendPluginBase - Pages []*AppPluginPage `json:"pages"` Routes []*AppPluginRoute `json:"routes"` FoundChildPlugins []*PluginInclude `json:"-"` @@ -85,7 +76,7 @@ func (app *AppPlugin) initApp() { } // slugify pages - for _, page := range app.Pages { + for _, page := range app.Includes { if page.Slug == "" { page.Slug = slug.Make(page.Name) } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 8443e91931d..28e092dcd4a 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -74,10 +75,16 @@ type PluginDependencies struct { } type PluginInclude struct { - Name string `json:"name"` - Path string `json:"path"` - Type string `json:"type"` - Id string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` + Component string `json:"component"` + Role models.RoleType `json:"role"` + AddToNav bool `json:"AddToNav"` + DefaultNav bool `json:"defaultNav"` + Slug string `json:"slug"` + + Id string `json:"-"` } type PluginDependencyItem struct { diff --git a/public/app/core/components/navbar/navbar.html b/public/app/core/components/navbar/navbar.html index cbc0472c373..8c050174201 100644 --- a/public/app/core/components/navbar/navbar.html +++ b/public/app/core/components/navbar/navbar.html @@ -8,9 +8,9 @@ - - - {{::ctrl.title}} + + + {{ctrl.title}}
    diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index 7fa5cc56647..4ff30c92f47 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -40,7 +40,10 @@ diff --git a/public/app/features/panel/panel_menu.js b/public/app/features/panel/panel_menu.js index bdd4a9ce754..f75aacb32f1 100644 --- a/public/app/features/panel/panel_menu.js +++ b/public/app/features/panel/panel_menu.js @@ -56,6 +56,7 @@ function (angular, $, _, Tether) { template += ''; }); diff --git a/public/app/features/plugins/partials/plugin_edit.html b/public/app/features/plugins/partials/plugin_edit.html index 6ae4ac5f95c..20b129b94e9 100644 --- a/public/app/features/plugins/partials/plugin_edit.html +++ b/public/app/features/plugins/partials/plugin_edit.html @@ -62,10 +62,6 @@ {{plug.name}} -
  • - - {{page.name}} -
  • diff --git a/public/app/features/plugins/partials/plugin_page.html b/public/app/features/plugins/partials/plugin_page.html index 1fc172a39cd..1105cff3b46 100644 --- a/public/app/features/plugins/partials/plugin_page.html +++ b/public/app/features/plugins/partials/plugin_page.html @@ -1,7 +1,7 @@ -
    +
    diff --git a/public/app/features/plugins/plugin_page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts index 6a840717a80..b12960a7306 100644 --- a/public/app/features/plugins/plugin_page_ctrl.ts +++ b/public/app/features/plugins/plugin_page_ctrl.ts @@ -11,11 +11,12 @@ export class AppPageCtrl { /** @ngInject */ constructor(private backendSrv, private $routeParams: any, private $rootScope) { this.pluginId = $routeParams.pluginId; + this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(app => { this.appModel = app; - this.page = _.findWhere(app.pages, {slug: this.$routeParams.slug}); + this.page = _.findWhere(app.includes, {slug: this.$routeParams.slug}); if (!this.page) { - $rootScope.appEvent('alert-error', ['App Page Not Found', '']); + this.$rootScope.appEvent('alert-error', ['App Page Not Found', '']); } }); } 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 040/268] 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 041/268] 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 f6cbbe1e6406807dd569c6bb6b52280678fb5ff2 Mon Sep 17 00:00:00 2001 From: utkarshcmu Date: Mon, 21 Mar 2016 22:56:46 -0700 Subject: [PATCH 042/268] Redirect to home when switching orgs --- public/app/core/components/sidemenu/sidemenu.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index 4c0e100f85c..4b2b1fd1ab3 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -87,6 +87,7 @@ export class SideMenuCtrl { switchOrg(orgId) { this.backendSrv.post('/api/user/using/' + orgId).then(() => { window.location.href = window.location.href; + this.$location.path('/'); }); }; } From 4110c7ffb0c5529e25010d6b6d842a7ab35cab4f Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 22 Mar 2016 09:25:46 +0100 Subject: [PATCH 043/268] fix(switchorg): fixes broken redirect when switchin org --- public/app/core/components/sidemenu/sidemenu.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index 4b2b1fd1ab3..a47704f9d7d 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -86,8 +86,7 @@ export class SideMenuCtrl { switchOrg(orgId) { this.backendSrv.post('/api/user/using/' + orgId).then(() => { - window.location.href = window.location.href; - this.$location.path('/'); + window.location.href = `${config.appSubUrl}/`; }); }; } From 10df9dc8c3ed3c24edef075e9ee99bb2ce01f8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 22 Mar 2016 10:15:47 +0100 Subject: [PATCH 044/268] feat(plugins): finished app navigation enhancements, closes #4434 --- pkg/api/dtos/plugins.go | 23 ++++++++++--------- pkg/api/index.go | 18 +++++---------- pkg/api/plugins.go | 17 +++++++------- pkg/plugins/app_plugin.go | 15 +++++++++--- pkg/plugins/models.go | 3 ++- .../plugins/partials/plugin_page.html | 2 +- .../app/features/plugins/plugin_page_ctrl.ts | 1 + 7 files changed, 43 insertions(+), 36 deletions(-) diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index d03a55861fc..53c911c10fa 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -3,17 +3,18 @@ package dtos import "github.com/grafana/grafana/pkg/plugins" type PluginSetting struct { - Name string `json:"name"` - Type string `json:"type"` - Id string `json:"id"` - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - Module string `json:"module"` - BaseUrl string `json:"baseUrl"` - Info *plugins.PluginInfo `json:"info"` - Includes []*plugins.PluginInclude `json:"includes"` - Dependencies *plugins.PluginDependencies `json:"dependencies"` - JsonData map[string]interface{} `json:"jsonData"` + Name string `json:"name"` + Type string `json:"type"` + Id string `json:"id"` + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + Module string `json:"module"` + BaseUrl string `json:"baseUrl"` + Info *plugins.PluginInfo `json:"info"` + Includes []*plugins.PluginInclude `json:"includes"` + Dependencies *plugins.PluginDependencies `json:"dependencies"` + JsonData map[string]interface{} `json:"jsonData"` + DefaultNavUrl string `json:"defaultNavUrl"` } type PluginListItem struct { diff --git a/pkg/api/index.go b/pkg/api/index.go index b26fb22ea52..a376f9504a4 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -90,7 +90,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if plugin.Pinned { appLink := &dtos.NavLink{ Text: plugin.Name, - Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit", + Url: plugin.DefaultNavUrl, Img: plugin.Info.Logos.Small, } @@ -100,9 +100,6 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + include.Slug, Text: include.Name, } - if include.DefaultNav { - appLink.Url = link.Url - } appLink.Children = append(appLink.Children, link) } if include.Type == "dashboard" && include.AddToNav { @@ -110,16 +107,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Url: setting.AppSubUrl + "/dashboard/db/" + include.Slug, Text: include.Name, } - if include.DefaultNav { - appLink.Url = link.Url - } appLink.Children = append(appLink.Children, link) } } if c.OrgRole == m.ROLE_ADMIN { appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true}) - appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) + appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) } data.MainNavLinks = append(data.MainNavLinks, appLink) @@ -132,10 +126,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin", Children: []*dtos.NavLink{ - {Text: "Global Users", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/users"}, - {Text: "Global Orgs", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/orgs"}, - {Text: "Server Settings", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/settings"}, - {Text: "Server Stats", Icon: "fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/stats"}, + {Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"}, + {Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"}, + {Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"}, + {Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"}, }, }) } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 793cf33e3c6..0411c0746ef 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -72,14 +72,15 @@ func GetPluginSettingById(c *middleware.Context) Response { } else { dto := &dtos.PluginSetting{ - Type: def.Type, - Id: def.Id, - Name: def.Name, - Info: &def.Info, - Dependencies: &def.Dependencies, - Includes: def.Includes, - BaseUrl: def.BaseUrl, - Module: def.Module, + Type: def.Type, + Id: def.Id, + Name: def.Name, + Info: &def.Info, + Dependencies: &def.Dependencies, + Includes: def.Includes, + BaseUrl: def.BaseUrl, + Module: def.Module, + DefaultNavUrl: def.DefaultNavUrl, } query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId} diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go index ecf59a15e15..f565b0e2b22 100644 --- a/pkg/plugins/app_plugin.go +++ b/pkg/plugins/app_plugin.go @@ -6,6 +6,7 @@ import ( "github.com/gosimple/slug" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" ) type AppPluginCss struct { @@ -75,10 +76,18 @@ func (app *AppPlugin) initApp() { } } + app.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + app.Id + "/edit" + // slugify pages - for _, page := range app.Includes { - if page.Slug == "" { - page.Slug = slug.Make(page.Name) + for _, include := range app.Includes { + if include.Slug == "" { + include.Slug = slug.Make(include.Name) + } + if include.Type == "page" && include.DefaultNav { + app.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + app.Id + "/page/" + include.Slug + } + if include.Type == "dashboard" && include.DefaultNav { + app.DefaultNavUrl = setting.AppSubUrl + "/dashboard/db/" + include.Slug } } } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 28e092dcd4a..30f794285e1 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -42,6 +42,7 @@ type PluginBase struct { IncludedInAppId string `json:"-"` PluginDir string `json:"-"` + DefaultNavUrl string `json:"-"` // cache for readme file contents Readme []byte `json:"-"` @@ -80,7 +81,7 @@ type PluginInclude struct { Type string `json:"type"` Component string `json:"component"` Role models.RoleType `json:"role"` - AddToNav bool `json:"AddToNav"` + AddToNav bool `json:"addToNav"` DefaultNav bool `json:"defaultNav"` Slug string `json:"slug"` diff --git a/public/app/features/plugins/partials/plugin_page.html b/public/app/features/plugins/partials/plugin_page.html index 1105cff3b46..5aadf6db86b 100644 --- a/public/app/features/plugins/partials/plugin_page.html +++ b/public/app/features/plugins/partials/plugin_page.html @@ -1,4 +1,4 @@ - +
    diff --git a/public/app/features/plugins/plugin_page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts index b12960a7306..f9150702de6 100644 --- a/public/app/features/plugins/plugin_page_ctrl.ts +++ b/public/app/features/plugins/plugin_page_ctrl.ts @@ -15,6 +15,7 @@ export class AppPageCtrl { this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(app => { this.appModel = app; this.page = _.findWhere(app.includes, {slug: this.$routeParams.slug}); + if (!this.page) { this.$rootScope.appEvent('alert-error', ['App Page Not Found', '']); } From 03f91e8d854baa3db9b07df6343df95560a71980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 22 Mar 2016 10:42:29 +0100 Subject: [PATCH 045/268] fix(panel resize): fixed panel resize drag handling --- public/app/features/panel/panel_directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 755e56de71a..addbdf94d53 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -121,7 +121,7 @@ module.directive('panelResizer', function($rootScope) { } scope.$apply(function() { - scope.$broadcast('render'); + ctrl.broadcastRender(); }); } From b73d196c6b9b2012541adbeffb2c536c60deda56 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 22 Mar 2016 14:30:06 +0100 Subject: [PATCH 046/268] feat(influxdb): add support for 0.11.0 tags this change is backwards compatible. closes #4392 --- public/app/plugins/datasource/influxdb/datasource.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 1136c9709bd..f1dff01f95d 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -109,7 +109,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) return $q.reject(err); } - return this._seriesQuery(interpolated).then(function (results) { + return this._seriesQuery(interpolated).then((results) => { if (!results || results.results.length === 0) { return []; } var influxResults = results.results[0]; @@ -118,9 +118,9 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) } var series = influxResults.series[0]; - return _.map(series.values, function(value) { + return _.map(series.values, (value) => { if (_.isArray(value)) { - return { text: value[0] }; + return { text: this.getValueBasedOnInfluxVersion(value) }; } else { return { text: value }; } @@ -128,6 +128,12 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) }); }; + this.getValueBasedOnInfluxVersion = function(value) { + //influxdb 0.10.0 sends the value in first position + //influxdb 0.11.0 sends the value in second position + return value[1] || value[0]; + }; + this._seriesQuery = function(query) { return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}); }; From 4c5291f941a0217d46e6383cc30de78a2ae6badc Mon Sep 17 00:00:00 2001 From: Matt Toback Date: Tue, 22 Mar 2016 10:21:47 -0400 Subject: [PATCH 047/268] Updated card views to accommodate a 2 column layout at lg breakpoints --- public/sass/components/_cards.scss | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss index 0d4d1ae88c9..582e782ce9a 100644 --- a/public/sass/components/_cards.scss +++ b/public/sass/components/_cards.scss @@ -99,6 +99,10 @@ .card-list-layout-grid { + .card-item-header-action { + float: right; + } + .card-item-wrapper { width: 100%; padding: 0 1.5rem 1.5rem 0rem; @@ -128,6 +132,14 @@ width: 33.333333%; } } + + &.card-list-layout-grid--max-2-col { + @include media-breakpoint-up(lg) { + .card-item-wrapper { + width: 50%; + } + } + } } .card-list-layout-list { From 83b73973980bceaf55634745807a4b848d3a9504 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 22 Mar 2016 16:04:54 +0100 Subject: [PATCH 048/268] Revert "feat(influxdb): add support for 0.11.0 tags" This reverts commit b73d196c6b9b2012541adbeffb2c536c60deda56. --- public/app/plugins/datasource/influxdb/datasource.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index f1dff01f95d..1136c9709bd 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -109,7 +109,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) return $q.reject(err); } - return this._seriesQuery(interpolated).then((results) => { + return this._seriesQuery(interpolated).then(function (results) { if (!results || results.results.length === 0) { return []; } var influxResults = results.results[0]; @@ -118,9 +118,9 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) } var series = influxResults.series[0]; - return _.map(series.values, (value) => { + return _.map(series.values, function(value) { if (_.isArray(value)) { - return { text: this.getValueBasedOnInfluxVersion(value) }; + return { text: value[0] }; } else { return { text: value }; } @@ -128,12 +128,6 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) }); }; - this.getValueBasedOnInfluxVersion = function(value) { - //influxdb 0.10.0 sends the value in first position - //influxdb 0.11.0 sends the value in second position - return value[1] || value[0]; - }; - this._seriesQuery = function(query) { return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}); }; From bbc4c361bb0249d7d05594c0182a3219b53df4e7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 22 Mar 2016 17:23:15 +0100 Subject: [PATCH 049/268] fix(npm): updates phantomjs requirement --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 887c41650d2..0ef4d95b7fd 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "karma-phantomjs-launcher": "0.2.1", "load-grunt-tasks": "3.4.0", "mocha": "2.3.4", - "phantomjs": "~2.1.3", + "phantomjs": "^2.1.3", "reflect-metadata": "0.1.2", "rxjs": "5.0.0-beta.0", "sass-lint": "^1.5.0", From 8dee54bf5dfc057f21da1c6a4a4288d9ec180f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 22 Mar 2016 18:21:21 +0100 Subject: [PATCH 050/268] feat(live): progress on panel <-> data source communication patterns --- public/app/core/core.ts | 4 +- .../app/core/directives/plugin_component.ts | 4 +- public/app/core/utils/emitter.ts | 47 +++++++++++++++++++ .../app/features/panel/metrics_panel_ctrl.ts | 46 +++++++++--------- public/app/features/panel/panel_ctrl.ts | 10 ++-- .../datasource/grafana-live/datasource.ts | 1 - public/app/plugins/panel/graph/module.ts | 25 +++++----- public/app/plugins/panel/table/module.ts | 4 +- public/app/plugins/panel/text/module.ts | 6 +-- public/app/plugins/panel/unknown/module.ts | 1 + 10 files changed, 99 insertions(+), 49 deletions(-) create mode 100644 public/app/core/utils/emitter.ts diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 9fe873acdf1..e97c7179afe 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 {liveSrv} from './live/live_srv'; +import {Emitter} from './utils/emitter'; import {layoutSelector} from './components/layout_selector/layout_selector'; import 'app/core/controllers/all'; import 'app/core/services/all'; @@ -46,5 +47,6 @@ export { colorPicker, liveSrv, layoutSelector, - infoPopover + infoPopover, + Emitter }; diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 9dc0f86897f..4f87a94573e 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -146,11 +146,11 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ }; }); } - // ConfigCtrl + // Datasource ConfigCtrl case 'datasource-config-ctrl': { var dsMeta = scope.ctrl.datasourceMeta; return System.import(dsMeta.module).then(function(dsModule): any { - if (!dsMeta.ConfigCtrl) { + if (!dsModule.ConfigCtrl) { return {notFound: true}; } diff --git a/public/app/core/utils/emitter.ts b/public/app/core/utils/emitter.ts new file mode 100644 index 00000000000..1b7941dd094 --- /dev/null +++ b/public/app/core/utils/emitter.ts @@ -0,0 +1,47 @@ + +import {Subject} from 'vendor/npm/rxjs/Subject'; + +var hasOwnProp = {}.hasOwnProperty; + +function createName(name) { + return '$' + name; +} + +export class Emitter { + subjects: any; + + constructor() { + this.subjects = {}; + } + + emit(name, data) { + var fnName = createName(name); + this.subjects[fnName] || (this.subjects[fnName] = new Subject()); + this.subjects[fnName].next(data); + } + + on(name, handler) { + var fnName = createName(name); + this.subjects[fnName] || (this.subjects[fnName] = new Subject()); + this.subjects[fnName].subscribe(handler); + }; + + off(name, handler) { + var fnName = createName(name); + if (this.subjects[fnName]) { + this.subjects[fnName].dispose(); + delete this.subjects[fnName]; + } + } + + dispose() { + var subjects = this.subjects; + for (var prop in subjects) { + if (hasOwnProp.call(subjects, prop)) { + subjects[prop].dispose(); + } + } + + this.subjects = {}; + } +} diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index a8f1079868c..a2c80ff8b8e 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -9,6 +9,8 @@ import {PanelCtrl} from './panel_ctrl'; import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; +import {Subject} from 'vendor/npm/rxjs/Subject'; + class MetricsPanelCtrl extends PanelCtrl { error: boolean; loading: boolean; @@ -26,7 +28,8 @@ class MetricsPanelCtrl extends PanelCtrl { timeInfo: any; skipDataOnInit: boolean; datasources: any[]; - dataSubject: any; + dataStream: any; + dataSubscription: any; constructor($scope, $injector) { super($scope, $injector); @@ -50,11 +53,6 @@ class MetricsPanelCtrl extends PanelCtrl { this.datasources = this.datasourceSrv.getMetricSources(); } - refreshData(data) { - // null op - return this.$q.when(data); - } - loadSnapshot(data) { // null op return data; @@ -73,21 +71,27 @@ class MetricsPanelCtrl extends PanelCtrl { return; } + // // ignore if we have data stream + if (this.dataStream) { + return; + } + // clear loading/error state delete this.error; this.loading = true; // load datasource service - this.datasourceSrv.get(this.panel.datasource).then(datasource => { - this.datasource = datasource; - return this.refreshData(this.datasource); - }).then(() => { + this.datasourceSrv.get(this.panel.datasource) + .then(this.issueQueries.bind(this)) + .then(() => { this.loading = false; }).catch(err => { console.log('Panel data error:', err); this.loading = false; this.error = err.message || "Timeseries data request error"; this.inspector = {error: err}; + + this.events.emit('data-error', err); }); } @@ -167,10 +171,6 @@ class MetricsPanelCtrl extends PanelCtrl { return this.$q.when([]); } - if (this.dataSubject) { - return this.$q.when([]); - } - var metricsQuery = { panelId: this.panel.id, range: this.range, @@ -190,15 +190,15 @@ class MetricsPanelCtrl extends PanelCtrl { // check for if data source returns subject if (results && results.subscribe) { - this.handleDataSubject(results); - return {data: []}; + this.handleDataStream(results); + return; } if (this.dashboard.snapshot) { this.panel.snapshotData = results; } - return this.dataHandler(results); + return this.events.emit('data-received', results); }); } catch (err) { return this.$q.reject(err); @@ -209,22 +209,24 @@ class MetricsPanelCtrl extends PanelCtrl { return data; } - handleDataSubject(subject) { + handleDataStream(stream) { // if we already have a connection - if (this.dataSubject) { + if (this.dataStream) { + console.log('two stream observables!'); return; } - this.dataSubject = subject; - this.dataSubject.subscribe({ + this.dataStream = stream; + this.dataSubscription = stream.subscribe({ next: (data) => { console.log('dataSubject next!'); if (data.range) { this.range = data.range; } - this.dataHandler(data); + this.events.emit('data-received', data); }, error: (error) => { + this.events.emit('data-error', error); console.log('panel: observer got error'); }, complete: () => { diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 2385354fa91..824b1b87159 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -9,6 +9,8 @@ const TITLE_HEIGHT = 25; const EMPTY_TITLE_HEIGHT = 9; const PANEL_PADDING = 5; +import {Emitter} from 'app/core/core'; + export class PanelCtrl { panel: any; row: any; @@ -28,12 +30,14 @@ export class PanelCtrl { editMode: any; height: any; containerHeight: any; + events: Emitter; constructor($scope, $injector) { this.$injector = $injector; this.$scope = $scope; this.$timeout = $injector.get('$timeout'); this.editorTabIndex = 0; + this.events = new Emitter(); var plugin = config.panels[this.panel.type]; if (plugin) { @@ -56,7 +60,7 @@ export class PanelCtrl { } refresh() { - return; + this.render(); } publishAppEvent(evtName, evt) { @@ -138,7 +142,7 @@ export class PanelCtrl { this.height = this.containerHeight - (PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT)); } - broadcastRender(arg1?, arg2?) { + render(arg1?, arg2?) { this.$scope.$broadcast('render', arg1, arg2); } @@ -157,7 +161,7 @@ export class PanelCtrl { updateColumnSpan(span) { this.panel.span = Math.min(Math.max(Math.floor(this.panel.span + span), 1), 12); this.$timeout(() => { - this.broadcastRender(); + this.render(); }); } diff --git a/public/app/plugins/datasource/grafana-live/datasource.ts b/public/app/plugins/datasource/grafana-live/datasource.ts index d7e519355c1..36605e5b6bc 100644 --- a/public/app/plugins/datasource/grafana-live/datasource.ts +++ b/public/app/plugins/datasource/grafana-live/datasource.ts @@ -24,7 +24,6 @@ export class GrafanaStreamDS { /** @ngInject */ constructor() { - } query(options): any { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index a7f06e15db9..1974f743aa3 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -106,6 +106,9 @@ class GraphCtrl extends MetricsPanelCtrl { _.defaults(this.panel.legend, panelDefaults.legend); this.colors = $scope.$root.colors; + + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('data-error', this.onDataError.bind(this)); } initEditMode() { @@ -138,14 +141,9 @@ class GraphCtrl extends MetricsPanelCtrl { this.render(); } - refreshData(datasource) { + issueQueries(datasource) { this.annotationsPromise = this.annotationsSrv.getAnnotations(this.dashboard); - - return this.issueQueries(datasource).catch(err => { - this.seriesList = []; - this.render([]); - throw err; - }); + return super.issueQueries(datasource); } zoomOut(evt) { @@ -157,7 +155,12 @@ class GraphCtrl extends MetricsPanelCtrl { this.dataHandler(snapshotData); } - dataHandler(results) { + onDataError(err) { + this.seriesList = []; + this.render([]); + } + + onDataReceived(results) { // png renderer returns just a url if (_.isString(results)) { this.render(results); @@ -178,7 +181,7 @@ class GraphCtrl extends MetricsPanelCtrl { this.loading = false; this.render(this.seriesList); }); - }; + } seriesHandler(seriesData, index) { var datapoints = seriesData.datapoints; @@ -208,10 +211,6 @@ class GraphCtrl extends MetricsPanelCtrl { return series; } - render(data?: any) { - this.broadcastRender(data); - } - changeSeriesColor(series, color) { series.color = color; this.panel.aliasColors[series.alias] = series.color; diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index b821cf3642f..7887e210d7a 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -80,9 +80,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { }); } - return this.issueQueries(datasource) - .then(this.dataHandler.bind(this)) - .catch(err => { + return this.issueQueries(datasource).catch(err => { this.render(); throw err; }); diff --git a/public/app/plugins/panel/text/module.ts b/public/app/plugins/panel/text/module.ts index 601f5ffc813..f30b7c619bb 100644 --- a/public/app/plugins/panel/text/module.ts +++ b/public/app/plugins/panel/text/module.ts @@ -29,11 +29,9 @@ export class TextPanelCtrl extends PanelCtrl { this.editorTabIndex = 1; } - refresh() { - this.render(); - } - render() { + super.render(); + if (this.panel.mode === 'markdown') { this.renderMarkdown(this.panel.content); } else if (this.panel.mode === 'html') { diff --git a/public/app/plugins/panel/unknown/module.ts b/public/app/plugins/panel/unknown/module.ts index 0a0871d6b69..c4567599a38 100644 --- a/public/app/plugins/panel/unknown/module.ts +++ b/public/app/plugins/panel/unknown/module.ts @@ -9,6 +9,7 @@ export class UnknownPanelCtrl extends PanelCtrl { constructor($scope, $injector) { super($scope, $injector); } + } From 185438145607b2464e40f41f304e600f45f68bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 22 Mar 2016 18:32:08 +0100 Subject: [PATCH 051/268] tech(): updated es6-shim, removed es5-shim, no longer needed as we upraded phantomjs to 2.x, also removed es6-promise, dont think we need it --- package.json | 2 +- public/views/index.html | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index 0ef4d95b7fd..40cf48950d4 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "angular2": "2.0.0-beta.0", "autoprefixer": "^6.3.3", "es6-promise": "^3.0.2", - "es6-shim": "^0.33.3", + "es6-shim": "^0.35.0", "expect.js": "~0.2.0", "glob": "~3.2.7", "grunt": "~0.4.0", diff --git a/public/views/index.html b/public/views/index.html index 49bfe2c5bb9..c4bffae5019 100644 --- a/public/views/index.html +++ b/public/views/index.html @@ -51,9 +51,7 @@ - - From 2bf94d8db4d269e0f5af423ffb8c972162c8bbe0 Mon Sep 17 00:00:00 2001 From: David Warden Date: Thu, 17 Mar 2016 10:45:39 -0400 Subject: [PATCH 052/268] 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 053/268] 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 cb49e11eca0a303958a20d9ccbc6f173d9457d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 22 Mar 2016 21:27:53 +0100 Subject: [PATCH 054/268] feat(live): panel sdk/api refactorings --- public/app/features/dashboard/rowCtrl.js | 5 -- .../app/features/panel/metrics_panel_ctrl.ts | 56 +++++------- public/app/features/panel/panel_directive.ts | 2 +- .../plugins/datasource/graphite/datasource.ts | 56 ++++++------ public/app/plugins/panel/graph/module.ts | 8 +- .../panel/graph/specs/graph_ctrl_specs.ts | 85 ++++++------------- public/app/plugins/panel/singlestat/module.ts | 47 +++++----- .../singlestat/specs/singlestat-specs.ts | 9 +- public/app/plugins/panel/table/module.ts | 51 +++++------ 9 files changed, 126 insertions(+), 193 deletions(-) diff --git a/public/app/features/dashboard/rowCtrl.js b/public/app/features/dashboard/rowCtrl.js index ece5ebdfbf7..5607f1eecac 100644 --- a/public/app/features/dashboard/rowCtrl.js +++ b/public/app/features/dashboard/rowCtrl.js @@ -30,11 +30,6 @@ function (angular, _, config) { $scope.toggleRow = function(row) { row.collapse = row.collapse ? false : true; - if (!row.collapse) { - $timeout(function() { - $scope.$broadcast('render'); - }); - } }; $scope.addPanel = function(panel) { diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index a2c80ff8b8e..b36c389d9a8 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -53,21 +53,14 @@ class MetricsPanelCtrl extends PanelCtrl { this.datasources = this.datasourceSrv.getMetricSources(); } - loadSnapshot(data) { - // null op - return data; - } - refresh() { // ignore fetching data if another panel is in fullscreen if (this.otherPanelInFullscreenMode()) { return; } // if we have snapshot data use that if (this.panel.snapshotData) { - if (this.loadSnapshot) { - this.updateTimeRange(); - this.loadSnapshot(this.panel.snapshotData); - } + this.updateTimeRange(); + this.events.emit('load-snapshot', this.panel.snapshotData); return; } @@ -83,15 +76,13 @@ class MetricsPanelCtrl extends PanelCtrl { // load datasource service this.datasourceSrv.get(this.panel.datasource) .then(this.issueQueries.bind(this)) - .then(() => { - this.loading = false; - }).catch(err => { - console.log('Panel data error:', err); + .then(this.handleQueryResult.bind(this)) + .catch(err => { this.loading = false; this.error = err.message || "Timeseries data request error"; this.inspector = {error: err}; - this.events.emit('data-error', err); + console.log('Panel data error:', err); }); } @@ -184,29 +175,24 @@ class MetricsPanelCtrl extends PanelCtrl { }; this.setTimeQueryStart(); - try { - return datasource.query(metricsQuery).then(results => { - this.setTimeQueryEnd(); - - // check for if data source returns subject - if (results && results.subscribe) { - this.handleDataStream(results); - return; - } - - if (this.dashboard.snapshot) { - this.panel.snapshotData = results; - } - - return this.events.emit('data-received', results); - }); - } catch (err) { - return this.$q.reject(err); - } + return datasource.query(metricsQuery); } - dataHandler(data) { - return data; + handleQueryResult(result) { + this.setTimeQueryEnd(); + this.loading = false; + + // check for if data source returns subject + if (result && result.subscribe) { + this.handleDataStream(result); + return; + } + + if (this.dashboard.snapshot) { + this.panel.snapshotData = result; + } + + return this.events.emit('data-received', result.data); } handleDataStream(stream) { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index addbdf94d53..a9bb75760f5 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -121,7 +121,7 @@ module.directive('panelResizer', function($rootScope) { } scope.$apply(function() { - ctrl.broadcastRender(); + ctrl.render(); }); } diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index c5074b275fd..615f0e9eee6 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -16,38 +16,34 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv this.render_method = instanceSettings.render_method || 'POST'; this.query = function(options) { - try { - var graphOptions = { - from: this.translateTime(options.rangeRaw.from, false), - until: this.translateTime(options.rangeRaw.to, true), - targets: options.targets, - format: options.format, - cacheTimeout: options.cacheTimeout || this.cacheTimeout, - maxDataPoints: options.maxDataPoints, - }; + var graphOptions = { + from: this.translateTime(options.rangeRaw.from, false), + until: this.translateTime(options.rangeRaw.to, true), + targets: options.targets, + format: options.format, + cacheTimeout: options.cacheTimeout || this.cacheTimeout, + maxDataPoints: options.maxDataPoints, + }; - var params = this.buildGraphiteParams(graphOptions, options.scopedVars); - if (params.length === 0) { - return $q.when([]); - } - - if (options.format === 'png') { - return $q.when(this.url + '/render' + '?' + params.join('&')); - } - - var httpOptions: any = {method: this.render_method, url: '/render'}; - - if (httpOptions.method === 'GET') { - httpOptions.url = httpOptions.url + '?' + params.join('&'); - } else { - httpOptions.data = params.join('&'); - httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; - } - - return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs); - } catch (err) { - return $q.reject(err); + var params = this.buildGraphiteParams(graphOptions, options.scopedVars); + if (params.length === 0) { + return $q.when([]); } + + if (options.format === 'png') { + return $q.when(this.url + '/render' + '?' + params.join('&')); + } + + var httpOptions: any = {method: this.render_method, url: '/render'}; + + if (httpOptions.method === 'GET') { + httpOptions.url = httpOptions.url + '?' + params.join('&'); + } else { + httpOptions.data = params.join('&'); + httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; + } + + return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs); }; this.convertDataPointsToMs = function(result) { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 1974f743aa3..8ae79827900 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -160,17 +160,17 @@ class GraphCtrl extends MetricsPanelCtrl { this.render([]); } - onDataReceived(results) { + onDataReceived(dataList) { // png renderer returns just a url - if (_.isString(results)) { - this.render(results); + if (_.isString(dataList)) { + this.render(dataList); return; } this.datapointsWarning = false; this.datapointsCount = 0; this.datapointsOutside = false; - this.seriesList = _.map(results.data, (series, i) => this.seriesHandler(series, i)); + this.seriesList = dataList.map(this.seriesHandler.bind(this)); this.datapointsWarning = this.datapointsCount === 0 || this.datapointsOutside; this.annotationsPromise.then(annotations => { diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts index e46459e2249..dfce4031772 100644 --- a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts @@ -14,47 +14,19 @@ describe('GraphCtrl', function() { beforeEach(ctx.providePhase()); beforeEach(ctx.createPanelController(GraphCtrl)); - - describe('get_data with 2 series', function() { - beforeEach(function() { - ctx.annotationsSrv.getAnnotations = sinon.stub().returns(ctx.$q.when([])); - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ - { target: 'test.cpu1', datapoints: [[1, 10]]}, - { target: 'test.cpu2', datapoints: [[1, 10]]} - ] - })); - ctx.ctrl.render = sinon.spy(); - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); - }); - - it('should send time series to render', function() { - var data = ctx.ctrl.render.getCall(0).args[0]; - expect(data.length).to.be(2); - }); - - describe('get_data failure following success', function() { - beforeEach(function() { - ctx.datasource.query = sinon.stub().returns(ctx.$q.reject('Datasource Error')); - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); - }); - - }); + beforeEach(() => { + ctx.ctrl.annotationsPromise = Promise.resolve({}); + ctx.ctrl.updateTimeRange(); }); describe('msResolution with second resolution timestamps', function() { beforeEach(function() { - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ - { target: 'test.cpu1', datapoints: [[1234567890, 45], [1234567899, 60]]}, - { target: 'test.cpu2', datapoints: [[1236547890, 55], [1234456709, 90]]} - ] - })); + var data = [ + { target: 'test.cpu1', datapoints: [[1234567890, 45], [1234567899, 60]]}, + { target: 'test.cpu2', datapoints: [[1236547890, 55], [1234456709, 90]]} + ]; ctx.ctrl.panel.tooltip.msResolution = false; - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); + ctx.ctrl.onDataReceived(data); }); it('should not show millisecond resolution tooltip', function() { @@ -64,15 +36,12 @@ describe('GraphCtrl', function() { describe('msResolution with millisecond resolution timestamps', function() { beforeEach(function() { - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ - { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, - { target: 'test.cpu2', datapoints: [[1236547890001, 55], [1234456709000, 90]]} - ] - })); + var data = [ + { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, + { target: 'test.cpu2', datapoints: [[1236547890001, 55], [1234456709000, 90]]} + ]; ctx.ctrl.panel.tooltip.msResolution = false; - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); + ctx.ctrl.onDataReceived(data); }); it('should show millisecond resolution tooltip', function() { @@ -82,15 +51,12 @@ describe('GraphCtrl', function() { describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() { beforeEach(function() { - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ - { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, - { target: 'test.cpu2', datapoints: [[1236547890000, 55], [1234456709000, 90]]} - ] - })); + var data = [ + { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, + { target: 'test.cpu2', datapoints: [[1236547890000, 55], [1234456709000, 90]]} + ]; ctx.ctrl.panel.tooltip.msResolution = false; - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); + ctx.ctrl.onDataReceived(data); }); it('should not show millisecond resolution tooltip', function() { @@ -100,16 +66,13 @@ describe('GraphCtrl', function() { describe('msResolution with millisecond resolution timestamps in one of the series', function() { beforeEach(function() { - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ - { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, - { target: 'test.cpu2', datapoints: [[1236547890010, 55], [1234456709000, 90]]}, - { target: 'test.cpu3', datapoints: [[1236547890000, 65], [1234456709000, 120]]} - ] - })); + var data = [ + { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, + { target: 'test.cpu2', datapoints: [[1236547890010, 55], [1234456709000, 90]]}, + { target: 'test.cpu3', datapoints: [[1236547890000, 65], [1234456709000, 120]]} + ]; ctx.ctrl.panel.tooltip.msResolution = false; - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); + ctx.ctrl.onDataReceived(data); }); it('should show millisecond resolution tooltip', function() { diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 175f49c9ff5..1baee54e973 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -45,7 +45,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; series: any[]; - data: any[]; + data: any; fontSizes: any[]; unitFormats: any[]; @@ -53,6 +53,9 @@ class SingleStatCtrl extends MetricsPanelCtrl { constructor($scope, $injector, private $location, private linkSrv) { super($scope, $injector); _.defaults(this.panel, panelDefaults); + + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('data-error', this.onDataError.bind(this)); } initEditMode() { @@ -68,23 +71,27 @@ class SingleStatCtrl extends MetricsPanelCtrl { this.render(); } - refreshData(datasource) { - return this.issueQueries(datasource) - .then(this.dataHandler.bind(this)) - .catch(err => { - this.series = []; - this.render(); - throw err; - }); - } - loadSnapshot(snapshotData) { // give element time to get attached and get dimensions this.$timeout(() => this.dataHandler(snapshotData), 50); } - dataHandler(results) { - this.series = _.map(results.data, this.seriesHandler.bind(this)); + onDataError(err) { + this.onDataReceived({data: []}); + } + + onDataReceived(dataList) { + this.series = dataList.map(this.seriesHandler.bind(this)); + + var data: any = {}; + this.setValues(data); + + data.thresholds = this.panel.thresholds.split(',').map(function(strVale) { + return Number(strVale.trim()); + }); + + data.colorMap = this.panel.colors; + this.data = data; this.render(); } @@ -155,20 +162,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { return result; } - render() { - var data: any = {}; - this.setValues(data); - - data.thresholds = this.panel.thresholds.split(',').map(function(strVale) { - return Number(strVale.trim()); - }); - - data.colorMap = this.panel.colors; - - this.data = data; - this.broadcastRender(); - } - setValues(data) { data.flotpairs = []; diff --git a/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts b/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts index 90bd5339737..dc85454b64a 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts @@ -23,12 +23,11 @@ describe('SingleStatCtrl', function() { beforeEach(function() { setupFunc(); - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [{target: 'test.cpu1', datapoints: ctx.datapoints}] - })); + var data = [ + {target: 'test.cpu1', datapoints: ctx.datapoints} + ]; - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); + ctx.ctrl.onDataReceived(data); ctx.data = ctx.ctrl.data; }); }; diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 7887e210d7a..81e850bc813 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -57,6 +57,9 @@ class TablePanelCtrl extends MetricsPanelCtrl { } _.defaults(this.panel, panelDefaults); + + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('data-error', this.onDataError.bind(this)); } initEditMode() { @@ -70,7 +73,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { return menu; } - refreshData(datasource) { + issueQueries(datasource) { this.pageIndex = 0; if (this.panel.transform === 'annotations') { @@ -80,36 +83,19 @@ class TablePanelCtrl extends MetricsPanelCtrl { }); } - return this.issueQueries(datasource).catch(err => { - this.render(); - throw err; - }); + return super.issueQueries(datasource); } - toggleColumnSort(col, colIndex) { - if (this.panel.sort.col === colIndex) { - if (this.panel.sort.desc) { - this.panel.sort.desc = false; - } else { - this.panel.sort.col = null; - } - } else { - this.panel.sort.col = colIndex; - this.panel.sort.desc = true; - } - + onDataError(err) { + this.dataRaw = []; this.render(); } - dataHandler(results) { - this.dataRaw = results.data; + onDataReceived(dataList) { + this.dataRaw = dataList; this.pageIndex = 0; - this.render(); - } - render() { - // automatically correct transform mode - // based on data + // automatically correct transform mode based on data if (this.dataRaw && this.dataRaw.length) { if (this.dataRaw[0].type === 'table') { this.panel.transform = 'table'; @@ -126,7 +112,22 @@ class TablePanelCtrl extends MetricsPanelCtrl { this.table = transformDataToTable(this.dataRaw, this.panel); this.table.sort(this.panel.sort); - this.broadcastRender(this.table); + this.render(this.table); + } + + toggleColumnSort(col, colIndex) { + if (this.panel.sort.col === colIndex) { + if (this.panel.sort.desc) { + this.panel.sort.desc = false; + } else { + this.panel.sort.col = null; + } + } else { + this.panel.sort.col = colIndex; + this.panel.sort.desc = true; + } + + this.render(); } exportCsv() { From 30b2a7169f831a5aa601b440b180f628fda1da96 Mon Sep 17 00:00:00 2001 From: Matt Toback Date: Tue, 22 Mar 2016 18:41:04 -0400 Subject: [PATCH 055/268] Form adjustments for app work --- public/sass/components/_gf-form.scss | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 3d93df79a29..197ad91db2b 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-label--textarea { + line-height: $input-line-height * 2; + padding-top: 0px; + padding-bottom: 16px; // $spacer (1rem) was slightly too short. +} + .gf-form-checkbox { flex-shrink: 0; padding: $input-padding-y $input-padding-x; @@ -67,6 +73,7 @@ $gf-form-label-margin: 0.25rem; display: block; width: 100%; padding: $input-padding-y $input-padding-x; + margin-right: $gf-form-label-margin; font-size: $font-size-base; line-height: $input-line-height; color: $input-color; @@ -111,6 +118,7 @@ $gf-form-label-margin: 0.25rem; .gf-form-select-wrapper { position: relative; background-color: $input-bg; + margin-right: $gf-form-label-margin; select.gf-form-input { text-indent: .01px; @@ -139,8 +147,15 @@ $gf-form-label-margin: 0.25rem; } } -.gf-form-select-wrapper + .gf-form-select-wrapper { - margin-left: $gf-form-label-margin; +.gf-form--textarea { + align-items: flex-start; +} + +input[type="number"].gf-natural-language-form { + font-size: $font-size-base; + line-height: $input-line-height; + margin: -6px -5px 0 5px; + padding: $input-padding-y/2 $input-padding-x/2; } .gf-form-btn { From 6a42b95d398f3130f46886df4e3f3117e643dbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 11:31:31 +0100 Subject: [PATCH 056/268] feat(panels): panel refactorings --- public/app/features/panel/metrics_panel_ctrl.ts | 2 +- public/app/plugins/panel/graph/module.ts | 5 +++-- public/app/plugins/panel/singlestat/module.ts | 14 ++++++++++---- public/app/plugins/panel/table/module.ts | 15 +++++++++------ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index b36c389d9a8..b0857f066d1 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -60,7 +60,7 @@ class MetricsPanelCtrl extends PanelCtrl { // if we have snapshot data use that if (this.panel.snapshotData) { this.updateTimeRange(); - this.events.emit('load-snapshot', this.panel.snapshotData); + this.events.emit('data-snapshot-load', this.panel.snapshotData); return; } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 8ae79827900..736e84b8411 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -109,6 +109,7 @@ class GraphCtrl extends MetricsPanelCtrl { this.events.on('data-received', this.onDataReceived.bind(this)); this.events.on('data-error', this.onDataError.bind(this)); + this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); } initEditMode() { @@ -150,9 +151,9 @@ class GraphCtrl extends MetricsPanelCtrl { this.publishAppEvent('zoom-out', evt); } - loadSnapshot(snapshotData) { + onDataSnapshotLoad(snapshotData) { this.annotationsPromise = this.annotationsSrv.getAnnotations(this.dashboard); - this.dataHandler(snapshotData); + this.onDataReceived(snapshotData.data); } onDataError(err) { diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 1baee54e973..b196ef7e9f1 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -56,6 +56,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { this.events.on('data-received', this.onDataReceived.bind(this)); this.events.on('data-error', this.onDataError.bind(this)); + this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); } initEditMode() { @@ -71,9 +72,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { this.render(); } - loadSnapshot(snapshotData) { - // give element time to get attached and get dimensions - this.$timeout(() => this.dataHandler(snapshotData), 50); + onDataSnapshotLoad(snapshotData) { + this.onDataReceived(snapshotData.data); } onDataError(err) { @@ -284,8 +284,14 @@ class SingleStatCtrl extends MetricsPanelCtrl { function addSparkline() { var width = elem.width() + 20; - var height = ctrl.height; + if (width < 30) { + // element has not gotten it's width yet + // delay sparkline render + setTimeout(addSparkline, 30); + return; + } + var height = ctrl.height; var plotCanvas = $('
    '); var plotCss: any = {}; plotCss.position = 'absolute'; diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 81e850bc813..bcce647b8ca 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -60,6 +60,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { this.events.on('data-received', this.onDataReceived.bind(this)); this.events.on('data-error', this.onDataError.bind(this)); + this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); } initEditMode() { @@ -86,6 +87,10 @@ class TablePanelCtrl extends MetricsPanelCtrl { return super.issueQueries(datasource); } + onDataSnapshotLoad(data) { + this.onDataReceived(data.data); + } + onDataError(err) { this.dataRaw = []; this.render(); @@ -141,15 +146,13 @@ class TablePanelCtrl extends MetricsPanelCtrl { var formaters = []; function getTableHeight() { - var panelHeight = ctrl.height || ctrl.panel.height || ctrl.row.height; - if (_.isString(panelHeight)) { - panelHeight = parseInt(panelHeight.replace('px', ''), 10); - } + var panelHeight = ctrl.height; + if (pageCount > 1) { - panelHeight -= 28; + panelHeight -= 26; } - return (panelHeight - 60) + 'px'; + return (panelHeight - 31) + 'px'; } function appendTableRows(tbodyElem) { From b3c073ab6c0050ebee68e0f29c36aa5dcf05e49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 12:50:56 +0100 Subject: [PATCH 057/268] feat(panels): more panel refactoring, using events instead of overriding base class methods --- public/app/core/utils/emitter.ts | 2 +- public/app/features/dashboard/viewStateSrv.js | 4 +- .../app/features/panel/metrics_panel_ctrl.ts | 8 ++-- public/app/features/panel/panel_ctrl.ts | 18 ++++--- public/app/plugins/panel/dashlist/module.ts | 14 +++--- public/app/plugins/panel/graph/graph.js | 6 +-- public/app/plugins/panel/graph/legend.js | 2 +- public/app/plugins/panel/graph/module.ts | 17 +++---- .../plugins/panel/graph/specs/graph_specs.ts | 48 ++++++++++--------- public/app/plugins/panel/singlestat/module.ts | 14 +++--- public/app/plugins/panel/table/module.ts | 13 +++-- public/app/plugins/panel/text/module.ts | 12 ++--- public/test/core/utils/emitter_specs.ts | 30 ++++++++++++ 13 files changed, 111 insertions(+), 77 deletions(-) create mode 100644 public/test/core/utils/emitter_specs.ts diff --git a/public/app/core/utils/emitter.ts b/public/app/core/utils/emitter.ts index 1b7941dd094..6195f7c5e4b 100644 --- a/public/app/core/utils/emitter.ts +++ b/public/app/core/utils/emitter.ts @@ -14,7 +14,7 @@ export class Emitter { this.subjects = {}; } - emit(name, data) { + emit(name, data?) { var fnName = createName(name); this.subjects[fnName] || (this.subjects[fnName] = new Subject()); this.subjects[fnName].next(data); diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index f3112d6db50..2738105553a 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -139,7 +139,7 @@ function (angular, _, $) { self.$scope.broadcastRefresh(); } else { - self.fullscreenPanel.$broadcast('render'); + ctrl.render(); } delete self.fullscreenPanel; }); @@ -159,7 +159,7 @@ function (angular, _, $) { this.$scope.appEvent('panel-fullscreen-enter', {panelId: ctrl.panel.id}); $timeout(function() { - panelScope.$broadcast('render'); + ctrl.render(); }); }; diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index b0857f066d1..8415172e3d1 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -44,16 +44,18 @@ class MetricsPanelCtrl extends PanelCtrl { if (!this.panel.targets) { this.panel.targets = [{}]; } + + this.events.on('refresh', this.onMetricsPanelRefresh.bind(this)); + this.events.on('init-edit-mode', this.onInitMetricsPanelEditMode.bind(this)); } - initEditMode() { - super.initEditMode(); + private onInitMetricsPanelEditMode() { this.addEditorTab('Metrics', 'public/app/partials/metrics.html'); this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html'); this.datasources = this.datasourceSrv.getMetricSources(); } - refresh() { + private onMetricsPanelRefresh() { // ignore fetching data if another panel is in fullscreen if (this.otherPanelInFullscreenMode()) { return; } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 824b1b87159..2821164bb10 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -18,7 +18,6 @@ export class PanelCtrl { editorTabIndex: number; pluginName: string; pluginId: string; - icon: string; editorTabs: any; $scope: any; $injector: any; @@ -60,7 +59,7 @@ export class PanelCtrl { } refresh() { - this.render(); + this.events.emit('refresh', null); } publishAppEvent(evtName, evt) { @@ -89,6 +88,7 @@ export class PanelCtrl { this.editorTabs = []; this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); this.editModeInitiated = true; + this.events.emit('init-edit-mode', null); } addEditorTab(title, directiveFn, index?) { @@ -118,7 +118,9 @@ export class PanelCtrl { } getExtendedMenu() { - return [{text: 'Panel JSON', click: 'ctrl.editPanelJson(); dismiss();'}]; + var actions = [{text: 'Panel JSON', click: 'ctrl.editPanelJson(); dismiss();'}]; + this.events.emit('init-panel-actions', actions); + return actions; } otherPanelInFullscreenMode() { @@ -126,7 +128,6 @@ export class PanelCtrl { } calculatePanelHeight() { - if (this.fullscreen) { var docHeight = $(window).height(); var editHeight = Math.floor(docHeight * 0.3); @@ -142,8 +143,13 @@ export class PanelCtrl { this.height = this.containerHeight - (PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT)); } - render(arg1?, arg2?) { - this.$scope.$broadcast('render', arg1, arg2); + render(payload?) { + // ignore if other panel is in fullscreen mode + if (this.otherPanelInFullscreenMode()) { + return; + } + + this.events.emit('render', payload); } toggleEditorHelp(index) { diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts index 7d3f9d74bd0..e199e0cfc6e 100644 --- a/public/app/plugins/panel/dashlist/module.ts +++ b/public/app/plugins/panel/dashlist/module.ts @@ -28,18 +28,18 @@ class DashListCtrl extends PanelCtrl { this.panel.tags = [$scope.panel.tag]; delete this.panel.tag; } + + this.events.on('refresh', this.onRefresh.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); } - initEditMode() { - super.initEditMode(); + onInitEditMode() { + this.editorTabIndex = 1; this.modes = ['starred', 'search', 'recently viewed']; - this.icon = "fa fa-star"; - this.addEditorTab('Options', () => { - return {templateUrl: 'public/app/plugins/panel/dashlist/editor.html'}; - }); + this.addEditorTab('Options', 'public/app/plugins/panel/dashlist/editor.html'); } - refresh() { + onRefresh() { var params: any = {limit: this.panel.limit}; if (this.panel.mode === 'recently viewed') { diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 098d86a294f..0f53852cec0 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -54,7 +54,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { }, scope); // Receive render events - scope.$on('render',function(event, renderData) { + ctrl.events.on('render', function(renderData) { data = renderData || data; if (!data) { ctrl.refresh(); @@ -97,10 +97,6 @@ function (angular, $, moment, _, kbn, GraphTooltip) { return true; } - if (ctrl.otherPanelInFullscreenMode()) { - return true; - } - if (!setElementHeight()) { return true; } if(_.isString(data)) { diff --git a/public/app/plugins/panel/graph/legend.js b/public/app/plugins/panel/graph/legend.js index d6d1bdbf2dd..22bbb756b59 100644 --- a/public/app/plugins/panel/graph/legend.js +++ b/public/app/plugins/panel/graph/legend.js @@ -22,7 +22,7 @@ function (angular, _, $) { var seriesList; var i; - scope.$on('render', function() { + ctrl.events.on('render', function() { data = ctrl.seriesList; if (data) { render(); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 736e84b8411..29c2dccbac2 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -110,12 +110,11 @@ class GraphCtrl extends MetricsPanelCtrl { this.events.on('data-received', this.onDataReceived.bind(this)); this.events.on('data-error', this.onDataError.bind(this)); this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.events.on('init-panel-actions', this.onInitPanelActions.bind(this)); } - initEditMode() { - super.initEditMode(); - - this.icon = "fa fa-bar-chart"; + 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); @@ -129,12 +128,10 @@ class GraphCtrl extends MetricsPanelCtrl { this.unitFormats = kbn.getUnitFormats(); } - getExtendedMenu() { - var menu = super.getExtendedMenu(); - menu.push({text: 'Export CSV (series as rows)', click: 'ctrl.exportCsv()'}); - menu.push({text: 'Export CSV (series as columns)', click: 'ctrl.exportCsvColumns()'}); - menu.push({text: 'Toggle legend', click: 'ctrl.toggleLegend()'}); - return menu; + onInitPanelActions(actions) { + actions.push({text: 'Export CSV (series as rows)', click: 'ctrl.exportCsv()'}); + actions.push({text: 'Export CSV (series as columns)', click: 'ctrl.exportCsvColumns()'}); + actions.push({text: 'Toggle legend', click: 'ctrl.toggleLegend()'}); } setUnitFormat(axis, subItem) { diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index dae90224ff8..28b38101004 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -8,6 +8,7 @@ import $ from 'jquery'; import helpers from 'test/specs/helpers'; import TimeSeries from 'app/core/time_series2'; import moment from 'moment'; +import {Emitter} from 'app/core/core'; describe('grafanaGraph', function() { @@ -24,31 +25,33 @@ describe('grafanaGraph', function() { })); beforeEach(angularMocks.inject(function($rootScope, $compile) { - var ctrl: any = {}; + var ctrl: any = { + events: new Emitter(), + height: 200, + panel: { + legend: {}, + grid: { }, + y_formats: [], + seriesOverrides: [], + tooltip: { + shared: true + } + }, + renderingCompleted: sinon.spy(), + hiddenSeries: {}, + dashboard: {timezone: 'browser'}, + range: { + from: moment([2015, 1, 1, 10]), + to: moment([2015, 1, 1, 22]), + }, + }; + var scope = $rootScope.$new(); scope.ctrl = ctrl; - var element = angular.element("
    "); - ctrl.height = '200px'; - ctrl.panel = { - legend: {}, - grid: { }, - y_formats: [], - seriesOverrides: [], - tooltip: { - shared: true - } - }; $rootScope.onAppEvent = sinon.spy(); - ctrl.otherPanelInFullscreenMode = sinon.spy(); - ctrl.renderingCompleted = sinon.spy(); - ctrl.hiddenSeries = {}; - ctrl.dashboard = { timezone: 'browser' }; - ctrl.range = { - from: moment([2015, 1, 1, 10]), - to: moment([2015, 1, 1, 22]), - }; + ctx.data = []; ctx.data.push(new TimeSeries({ datapoints: [[1,1],[2,2]], @@ -61,11 +64,12 @@ describe('grafanaGraph', function() { setupFunc(ctrl, ctx.data); + var element = angular.element("
    "); $compile(element)(scope); scope.$digest(); - $.plot = ctx.plotSpy = sinon.spy(); - scope.$emit('render', ctx.data); + $.plot = ctx.plotSpy = sinon.spy(); + ctrl.events.emit('render', ctx.data); ctx.plotData = ctx.plotSpy.getCall(0).args[1]; ctx.plotOptions = ctx.plotSpy.getCall(0).args[2]; })); diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index b196ef7e9f1..bbae79d4e65 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -235,14 +235,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { var templateSrv = this.templateSrv; var data, linkInfo; var $panelContainer = elem.find('.panel-container'); - // change elem to singlestat panel elem = elem.find('.singlestat-panel'); - hookupDrilldownLinkTooltip(); - - scope.$on('render', function() { - render(); - ctrl.renderingCompleted(); - }); function setElementHeight() { elem.css('height', ctrl.height + 'px'); @@ -417,6 +410,13 @@ class SingleStatCtrl extends MetricsPanelCtrl { drilldownTooltip.place_tt(e.pageX+20, e.pageY-15); }); } + + hookupDrilldownLinkTooltip(); + + this.events.on('render', function() { + render(); + ctrl.renderingCompleted(); + }); } } diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index bcce647b8ca..d5ac802a62b 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -61,17 +61,16 @@ class TablePanelCtrl extends MetricsPanelCtrl { this.events.on('data-received', this.onDataReceived.bind(this)); this.events.on('data-error', this.onDataError.bind(this)); this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.events.on('init-panel-actions', this.onInitPanelActions.bind(this)); } - initEditMode() { - super.initEditMode(); + onInitEditMode() { this.addEditorTab('Options', tablePanelEditor, 2); } - getExtendedMenu() { - var menu = super.getExtendedMenu(); - menu.push({text: 'Export CSV', click: 'ctrl.exportCsv()'}); - return menu; + onInitPanelActions(actions) { + actions.push({text: 'Export CSV', click: 'ctrl.exportCsv()'}); } issueQueries(datasource) { @@ -211,7 +210,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { elem.off('click', '.table-panel-page-link'); }); - scope.$on('render', function(event, renderData) { + ctrl.events.on('render', function(renderData) { data = renderData || data; if (data) { renderPanel(); diff --git a/public/app/plugins/panel/text/module.ts b/public/app/plugins/panel/text/module.ts index f30b7c619bb..42657788c0a 100644 --- a/public/app/plugins/panel/text/module.ts +++ b/public/app/plugins/panel/text/module.ts @@ -20,18 +20,18 @@ export class TextPanelCtrl extends PanelCtrl { super($scope, $injector); _.defaults(this.panel, panelDefaults); + + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.events.on('refresh', this.onRender.bind(this)); + this.events.on('render', this.onRender.bind(this)); } - initEditMode() { - super.initEditMode(); - this.icon = 'fa fa-text-width'; + onInitEditMode() { this.addEditorTab('Options', 'public/app/plugins/panel/text/editor.html'); this.editorTabIndex = 1; } - render() { - super.render(); - + onRender() { if (this.panel.mode === 'markdown') { this.renderMarkdown(this.panel.content); } else if (this.panel.mode === 'html') { diff --git a/public/test/core/utils/emitter_specs.ts b/public/test/core/utils/emitter_specs.ts new file mode 100644 index 00000000000..6ab5f80fa0e --- /dev/null +++ b/public/test/core/utils/emitter_specs.ts @@ -0,0 +1,30 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common' + +import {Emitter} from 'app/core/core'; + +describe("Emitter", () => { + + describe('given 2 subscribers', () => { + + it('should notfiy subscribers', () => { + var events = new Emitter(); + var sub1Called = false; + var sub2Called = false; + + events.on('test', () => { + sub1Called = true; + }); + events.on('test', () => { + sub2Called = true; + }); + + events.emit('test', null); + + expect(sub1Called).to.be(true); + expect(sub2Called).to.be(true); + }); + }); + +}); + + From 4058c85e2adaaef71a61aec012ed761db5f57f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 13:02:37 +0100 Subject: [PATCH 058/268] fix(): singlestat init --- public/app/plugins/panel/singlestat/module.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index bbae79d4e65..cd85fda7c92 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -57,11 +57,10 @@ class SingleStatCtrl extends MetricsPanelCtrl { this.events.on('data-received', this.onDataReceived.bind(this)); this.events.on('data-error', this.onDataError.bind(this)); this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); } - initEditMode() { - super.initEditMode(); - this.icon = "fa fa-dashboard"; + onInitEditMode() { this.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%']; this.addEditorTab('Options', 'public/app/plugins/panel/singlestat/editor.html', 2); this.unitFormats = kbn.getUnitFormats(); From 19e07a610ad1e588393c6506071e9be481fab8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 13:08:09 +0100 Subject: [PATCH 059/268] tech(): added gorilla dependency --- Godeps/Godeps.json | 5 + .../github.com/gorilla/websocket/.gitignore | 22 + .../github.com/gorilla/websocket/.travis.yml | 20 + .../src/github.com/gorilla/websocket/AUTHORS | 8 + .../src/github.com/gorilla/websocket/LICENSE | 22 + .../github.com/gorilla/websocket/README.md | 61 ++ .../github.com/gorilla/websocket/client.go | 350 +++++++ .../src/github.com/gorilla/websocket/conn.go | 915 ++++++++++++++++++ .../src/github.com/gorilla/websocket/doc.go | 148 +++ .../src/github.com/gorilla/websocket/json.go | 55 ++ .../github.com/gorilla/websocket/server.go | 253 +++++ .../src/github.com/gorilla/websocket/util.go | 44 + 12 files changed, 1903 insertions(+) create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/.gitignore create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/AUTHORS create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/LICENSE create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/README.md create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/client.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/conn.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/doc.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/json.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/server.go create mode 100644 Godeps/_workspace/src/github.com/gorilla/websocket/util.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index b5cb1a460df..80ac03cce90 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,7 @@ { "ImportPath": "github.com/grafana/grafana", "GoVersion": "go1.5.1", + "GodepVersion": "v60", "Packages": [ "./pkg/..." ], @@ -209,6 +210,10 @@ "Comment": "v0.4.4-44-gf561133", "Rev": "f56113384f2c63dfe4cd8e768e349f1c35122b58" }, + { + "ImportPath": "github.com/gorilla/websocket", + "Rev": "c45a635370221f34fea2d5163fd156fcb4e38e8a" + }, { "ImportPath": "github.com/gosimple/slug", "Rev": "8d258463b4459f161f51d6a357edacd3eef9d663" diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/.gitignore b/Godeps/_workspace/src/github.com/gorilla/websocket/.gitignore new file mode 100644 index 00000000000..00268614f04 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/.travis.yml b/Godeps/_workspace/src/github.com/gorilla/websocket/.travis.yml new file mode 100644 index 00000000000..ace2e2f41f9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/.travis.yml @@ -0,0 +1,20 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.2 + - go: 1.3 + - go: 1.4 + - go: 1.5 + - go: 1.6 + - go: tip + +install: + - go get golang.org/x/tools/cmd/vet + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - go vet . + - go test -v -race ./... diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/AUTHORS b/Godeps/_workspace/src/github.com/gorilla/websocket/AUTHORS new file mode 100644 index 00000000000..b003eca0ca1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/AUTHORS @@ -0,0 +1,8 @@ +# This is the official list of Gorilla WebSocket authors for copyright +# purposes. +# +# Please keep the list sorted. + +Gary Burd +Joachim Bauch + diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/LICENSE b/Godeps/_workspace/src/github.com/gorilla/websocket/LICENSE new file mode 100644 index 00000000000..9171c972252 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/README.md b/Godeps/_workspace/src/github.com/gorilla/websocket/README.md new file mode 100644 index 00000000000..9d71959ea1a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/README.md @@ -0,0 +1,61 @@ +# Gorilla WebSocket + +Gorilla WebSocket is a [Go](http://golang.org/) implementation of the +[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. + +### Documentation + +* [API Reference](http://godoc.org/github.com/gorilla/websocket) +* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat) +* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command) +* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo) +* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch) + +### Status + +The Gorilla WebSocket package provides a complete and tested implementation of +the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The +package API is stable. + +### Installation + + go get github.com/gorilla/websocket + +### Protocol Compliance + +The Gorilla WebSocket package passes the server tests in the [Autobahn Test +Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn +subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). + +### Gorilla WebSocket compared with other packages + + + + + + + + + + + + + + + + + + +
    github.com/gorillagolang.org/x/net
    RFC 6455 Features
    Passes Autobahn Test SuiteYesNo
    Receive fragmented messageYesNo, see note 1
    Send close messageYesNo
    Send pings and receive pongsYesNo
    Get the type of a received data messageYesYes, see note 2
    Other Features
    Limit size of received messageYesNo
    Read message using io.ReaderYesNo, see note 3
    Write message using io.WriteCloserYesNo, see note 3
    + +Notes: + +1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html). +2. The application can get the type of a received data message by implementing + a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal) + function. +3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries. + Read returns when the input buffer is full or a frame boundary is + encountered. Each call to Write sends a single frame message. The Gorilla + io.Reader and io.WriteCloser operate on a single WebSocket message. + diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/client.go b/Godeps/_workspace/src/github.com/gorilla/websocket/client.go new file mode 100644 index 00000000000..a353e18565c --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/client.go @@ -0,0 +1,350 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "bytes" + "crypto/tls" + "encoding/base64" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// ErrBadHandshake is returned when the server response to opening handshake is +// invalid. +var ErrBadHandshake = errors.New("websocket: bad handshake") + +// NewClient creates a new client connection using the given net connection. +// The URL u specifies the host and request URI. Use requestHeader to specify +// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies +// (Cookie). Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etc. +// +// Deprecated: Use Dialer instead. +func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) { + d := Dialer{ + ReadBufferSize: readBufSize, + WriteBufferSize: writeBufSize, + NetDial: func(net, addr string) (net.Conn, error) { + return netConn, nil + }, + } + return d.Dial(u.String(), requestHeader) +} + +// A Dialer contains options for connecting to WebSocket server. +type Dialer struct { + // NetDial specifies the dial function for creating TCP connections. If + // NetDial is nil, net.Dial is used. + NetDial func(network, addr string) (net.Conn, error) + + // Proxy specifies a function to return a proxy for a given + // Request. If the function returns a non-nil error, the + // request is aborted with the provided error. + // If Proxy is nil or returns a nil *URL, no proxy is used. + Proxy func(*http.Request) (*url.URL, error) + + // TLSClientConfig specifies the TLS configuration to use with tls.Client. + // If nil, the default configuration is used. + TLSClientConfig *tls.Config + + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // Input and output buffer sizes. If the buffer size is zero, then a + // default value of 4096 is used. + ReadBufferSize, WriteBufferSize int + + // Subprotocols specifies the client's requested subprotocols. + Subprotocols []string +} + +var errMalformedURL = errors.New("malformed ws or wss URL") + +// parseURL parses the URL. +// +// This function is a replacement for the standard library url.Parse function. +// In Go 1.4 and earlier, url.Parse loses information from the path. +func parseURL(s string) (*url.URL, error) { + // From the RFC: + // + // ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] + // wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] + + var u url.URL + switch { + case strings.HasPrefix(s, "ws://"): + u.Scheme = "ws" + s = s[len("ws://"):] + case strings.HasPrefix(s, "wss://"): + u.Scheme = "wss" + s = s[len("wss://"):] + default: + return nil, errMalformedURL + } + + if i := strings.Index(s, "?"); i >= 0 { + u.RawQuery = s[i+1:] + s = s[:i] + } + + if i := strings.Index(s, "/"); i >= 0 { + u.Opaque = s[i:] + s = s[:i] + } else { + u.Opaque = "/" + } + + u.Host = s + + if strings.Contains(u.Host, "@") { + // Don't bother parsing user information because user information is + // not allowed in websocket URIs. + return nil, errMalformedURL + } + + return &u, nil +} + +func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { + hostPort = u.Host + hostNoPort = u.Host + if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") { + hostNoPort = hostNoPort[:i] + } else { + switch u.Scheme { + case "wss": + hostPort += ":443" + case "https": + hostPort += ":443" + default: + hostPort += ":80" + } + } + return hostPort, hostNoPort +} + +// DefaultDialer is a dialer with all fields set to the default zero values. +var DefaultDialer = &Dialer{ + Proxy: http.ProxyFromEnvironment, +} + +// Dial creates a new client connection. Use requestHeader to specify the +// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). +// Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etcetera. The response body may not contain the entire response and does not +// need to be closed by the application. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + + if d == nil { + d = &Dialer{ + Proxy: http.ProxyFromEnvironment, + } + } + + challengeKey, err := generateChallengeKey() + if err != nil { + return nil, nil, err + } + + u, err := parseURL(urlStr) + if err != nil { + return nil, nil, err + } + + switch u.Scheme { + case "ws": + u.Scheme = "http" + case "wss": + u.Scheme = "https" + default: + return nil, nil, errMalformedURL + } + + if u.User != nil { + // User name and password are not allowed in websocket URIs. + return nil, nil, errMalformedURL + } + + req := &http.Request{ + Method: "GET", + URL: u, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Host: u.Host, + } + + // Set the request headers using the capitalization for names and values in + // RFC examples. Although the capitalization shouldn't matter, there are + // servers that depend on it. The Header.Set method is not used because the + // method canonicalizes the header names. + req.Header["Upgrade"] = []string{"websocket"} + req.Header["Connection"] = []string{"Upgrade"} + req.Header["Sec-WebSocket-Key"] = []string{challengeKey} + req.Header["Sec-WebSocket-Version"] = []string{"13"} + if len(d.Subprotocols) > 0 { + req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")} + } + for k, vs := range requestHeader { + switch { + case k == "Host": + if len(vs) > 0 { + req.Host = vs[0] + } + case k == "Upgrade" || + k == "Connection" || + k == "Sec-Websocket-Key" || + k == "Sec-Websocket-Version" || + (k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0): + return nil, nil, errors.New("websocket: duplicate header not allowed: " + k) + default: + req.Header[k] = vs + } + } + + hostPort, hostNoPort := hostPortNoPort(u) + + var proxyURL *url.URL + // Check wether the proxy method has been configured + if d.Proxy != nil { + proxyURL, err = d.Proxy(req) + } + if err != nil { + return nil, nil, err + } + + var targetHostPort string + if proxyURL != nil { + targetHostPort, _ = hostPortNoPort(proxyURL) + } else { + targetHostPort = hostPort + } + + var deadline time.Time + if d.HandshakeTimeout != 0 { + deadline = time.Now().Add(d.HandshakeTimeout) + } + + netDial := d.NetDial + if netDial == nil { + netDialer := &net.Dialer{Deadline: deadline} + netDial = netDialer.Dial + } + + netConn, err := netDial("tcp", targetHostPort) + if err != nil { + return nil, nil, err + } + + defer func() { + if netConn != nil { + netConn.Close() + } + }() + + if err := netConn.SetDeadline(deadline); err != nil { + return nil, nil, err + } + + if proxyURL != nil { + connectHeader := make(http.Header) + if user := proxyURL.User; user != nil { + proxyUser := user.Username() + if proxyPassword, passwordSet := user.Password(); passwordSet { + credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) + connectHeader.Set("Proxy-Authorization", "Basic "+credential) + } + } + connectReq := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: hostPort}, + Host: hostPort, + Header: connectHeader, + } + + connectReq.Write(netConn) + + // Read response. + // Okay to use and discard buffered reader here, because + // TLS server will not speak until spoken to. + br := bufio.NewReader(netConn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + return nil, nil, err + } + if resp.StatusCode != 200 { + f := strings.SplitN(resp.Status, " ", 2) + return nil, nil, errors.New(f[1]) + } + } + + if u.Scheme == "https" { + cfg := d.TLSClientConfig + if cfg == nil { + cfg = &tls.Config{ServerName: hostNoPort} + } else if cfg.ServerName == "" { + shallowCopy := *cfg + cfg = &shallowCopy + cfg.ServerName = hostNoPort + } + tlsConn := tls.Client(netConn, cfg) + netConn = tlsConn + if err := tlsConn.Handshake(); err != nil { + return nil, nil, err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return nil, nil, err + } + } + } + + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize) + + if err := req.Write(netConn); err != nil { + return nil, nil, err + } + + resp, err := http.ReadResponse(conn.br, req) + if err != nil { + return nil, nil, err + } + if resp.StatusCode != 101 || + !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || + !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || + resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) { + // Before closing the network connection on return from this + // function, slurp up some of the response to aid application + // debugging. + buf := make([]byte, 1024) + n, _ := io.ReadFull(resp.Body, buf) + resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n])) + return nil, resp, ErrBadHandshake + } + + resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) + conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") + + netConn.SetDeadline(time.Time{}) + netConn = nil // to avoid close in defer. + return conn, resp, nil +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/conn.go b/Godeps/_workspace/src/github.com/gorilla/websocket/conn.go new file mode 100644 index 00000000000..eff26c6328a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/conn.go @@ -0,0 +1,915 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "encoding/binary" + "errors" + "io" + "io/ioutil" + "math/rand" + "net" + "strconv" + "time" +) + +const ( + maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask + maxControlFramePayloadSize = 125 + finalBit = 1 << 7 + maskBit = 1 << 7 + writeWait = time.Second + + defaultReadBufferSize = 4096 + defaultWriteBufferSize = 4096 + + continuationFrame = 0 + noFrame = -1 +) + +// Close codes defined in RFC 6455, section 11.7. +const ( + CloseNormalClosure = 1000 + CloseGoingAway = 1001 + CloseProtocolError = 1002 + CloseUnsupportedData = 1003 + CloseNoStatusReceived = 1005 + CloseAbnormalClosure = 1006 + CloseInvalidFramePayloadData = 1007 + ClosePolicyViolation = 1008 + CloseMessageTooBig = 1009 + CloseMandatoryExtension = 1010 + CloseInternalServerErr = 1011 + CloseTLSHandshake = 1015 +) + +// The message types are defined in RFC 6455, section 11.8. +const ( + // TextMessage denotes a text data message. The text message payload is + // interpreted as UTF-8 encoded text data. + TextMessage = 1 + + // BinaryMessage denotes a binary data message. + BinaryMessage = 2 + + // CloseMessage denotes a close control message. The optional message + // payload contains a numeric code and text. Use the FormatCloseMessage + // function to format a close message payload. + CloseMessage = 8 + + // PingMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PingMessage = 9 + + // PongMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PongMessage = 10 +) + +// ErrCloseSent is returned when the application writes a message to the +// connection after sending a close message. +var ErrCloseSent = errors.New("websocket: close sent") + +// ErrReadLimit is returned when reading a message that is larger than the +// read limit set for the connection. +var ErrReadLimit = errors.New("websocket: read limit exceeded") + +// netError satisfies the net Error interface. +type netError struct { + msg string + temporary bool + timeout bool +} + +func (e *netError) Error() string { return e.msg } +func (e *netError) Temporary() bool { return e.temporary } +func (e *netError) Timeout() bool { return e.timeout } + +// CloseError represents close frame. +type CloseError struct { + + // Code is defined in RFC 6455, section 11.7. + Code int + + // Text is the optional text payload. + Text string +} + +func (e *CloseError) Error() string { + s := []byte("websocket: close ") + s = strconv.AppendInt(s, int64(e.Code), 10) + switch e.Code { + case CloseNormalClosure: + s = append(s, " (normal)"...) + case CloseGoingAway: + s = append(s, " (going away)"...) + case CloseProtocolError: + s = append(s, " (protocol error)"...) + case CloseUnsupportedData: + s = append(s, " (unsupported data)"...) + case CloseNoStatusReceived: + s = append(s, " (no status)"...) + case CloseAbnormalClosure: + s = append(s, " (abnormal closure)"...) + case CloseInvalidFramePayloadData: + s = append(s, " (invalid payload data)"...) + case ClosePolicyViolation: + s = append(s, " (policy violation)"...) + case CloseMessageTooBig: + s = append(s, " (message too big)"...) + case CloseMandatoryExtension: + s = append(s, " (mandatory extension missing)"...) + case CloseInternalServerErr: + s = append(s, " (internal server error)"...) + case CloseTLSHandshake: + s = append(s, " (TLS handshake error)"...) + } + if e.Text != "" { + s = append(s, ": "...) + s = append(s, e.Text...) + } + return string(s) +} + +// IsCloseError returns boolean indicating whether the error is a *CloseError +// with one of the specified codes. +func IsCloseError(err error, codes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range codes { + if e.Code == code { + return true + } + } + } + return false +} + +// IsUnexpectedCloseError returns boolean indicating whether the error is a +// *CloseError with a code not in the list of expected codes. +func IsUnexpectedCloseError(err error, expectedCodes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range expectedCodes { + if e.Code == code { + return false + } + } + return true + } + return false +} + +var ( + errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true, temporary: true} + errUnexpectedEOF = &CloseError{Code: CloseAbnormalClosure, Text: io.ErrUnexpectedEOF.Error()} + errBadWriteOpCode = errors.New("websocket: bad write message type") + errWriteClosed = errors.New("websocket: write closed") + errInvalidControlFrame = errors.New("websocket: invalid control frame") +) + +func hideTempErr(err error) error { + if e, ok := err.(net.Error); ok && e.Temporary() { + err = &netError{msg: e.Error(), timeout: e.Timeout()} + } + return err +} + +func isControl(frameType int) bool { + return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage +} + +func isData(frameType int) bool { + return frameType == TextMessage || frameType == BinaryMessage +} + +func maskBytes(key [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 +} + +func newMaskKey() [4]byte { + n := rand.Uint32() + return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} +} + +// Conn represents a WebSocket connection. +type Conn struct { + conn net.Conn + isServer bool + subprotocol string + + // Write fields + mu chan bool // used as mutex to protect write to conn and closeSent + closeSent bool // true if close message was sent + + // Message writer fields. + writeErr error + writeBuf []byte // frame is constructed in this buffer. + writePos int // end of data in writeBuf. + writeFrameType int // type of the current frame. + writeSeq int // incremented to invalidate message writers. + writeDeadline time.Time + isWriting bool // for best-effort concurrent write detection + + // Read fields + readErr error + br *bufio.Reader + readRemaining int64 // bytes remaining in current frame. + readFinal bool // true the current message has more frames. + readSeq int // incremented to invalidate message readers. + readLength int64 // Message size. + readLimit int64 // Maximum message size. + readMaskPos int + readMaskKey [4]byte + handlePong func(string) error + handlePing func(string) error + readErrCount int +} + +func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { + mu := make(chan bool, 1) + mu <- true + + if readBufferSize == 0 { + readBufferSize = defaultReadBufferSize + } + if writeBufferSize == 0 { + writeBufferSize = defaultWriteBufferSize + } + + c := &Conn{ + isServer: isServer, + br: bufio.NewReaderSize(conn, readBufferSize), + conn: conn, + mu: mu, + readFinal: true, + writeBuf: make([]byte, writeBufferSize+maxFrameHeaderSize), + writeFrameType: noFrame, + writePos: maxFrameHeaderSize, + } + c.SetPingHandler(nil) + c.SetPongHandler(nil) + return c +} + +// Subprotocol returns the negotiated protocol for the connection. +func (c *Conn) Subprotocol() string { + return c.subprotocol +} + +// Close closes the underlying network connection without sending or waiting for a close frame. +func (c *Conn) Close() error { + return c.conn.Close() +} + +// LocalAddr returns the local network address. +func (c *Conn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (c *Conn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +// Write methods + +func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { + <-c.mu + defer func() { c.mu <- true }() + + if c.closeSent { + return ErrCloseSent + } else if frameType == CloseMessage { + c.closeSent = true + } + + c.conn.SetWriteDeadline(deadline) + for _, buf := range bufs { + if len(buf) > 0 { + n, err := c.conn.Write(buf) + if n != len(buf) { + // Close on partial write. + c.conn.Close() + } + if err != nil { + return err + } + } + } + return nil +} + +// WriteControl writes a control message with the given deadline. The allowed +// message types are CloseMessage, PingMessage and PongMessage. +func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error { + if !isControl(messageType) { + return errBadWriteOpCode + } + if len(data) > maxControlFramePayloadSize { + return errInvalidControlFrame + } + + b0 := byte(messageType) | finalBit + b1 := byte(len(data)) + if !c.isServer { + b1 |= maskBit + } + + buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize) + buf = append(buf, b0, b1) + + if c.isServer { + buf = append(buf, data...) + } else { + key := newMaskKey() + buf = append(buf, key[:]...) + buf = append(buf, data...) + maskBytes(key, 0, buf[6:]) + } + + d := time.Hour * 1000 + if !deadline.IsZero() { + d = deadline.Sub(time.Now()) + if d < 0 { + return errWriteTimeout + } + } + + timer := time.NewTimer(d) + select { + case <-c.mu: + timer.Stop() + case <-timer.C: + return errWriteTimeout + } + defer func() { c.mu <- true }() + + if c.closeSent { + return ErrCloseSent + } else if messageType == CloseMessage { + c.closeSent = true + } + + c.conn.SetWriteDeadline(deadline) + n, err := c.conn.Write(buf) + if n != 0 && n != len(buf) { + c.conn.Close() + } + return hideTempErr(err) +} + +// NextWriter returns a writer for the next message to send. The writer's +// Close method flushes the complete message to the network. +// +// There can be at most one open writer on a connection. NextWriter closes the +// previous writer if the application has not already done so. +func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { + if c.writeErr != nil { + return nil, c.writeErr + } + + if c.writeFrameType != noFrame { + if err := c.flushFrame(true, nil); err != nil { + return nil, err + } + } + + if !isControl(messageType) && !isData(messageType) { + return nil, errBadWriteOpCode + } + + c.writeFrameType = messageType + return messageWriter{c, c.writeSeq}, nil +} + +func (c *Conn) flushFrame(final bool, extra []byte) error { + length := c.writePos - maxFrameHeaderSize + len(extra) + + // Check for invalid control frames. + if isControl(c.writeFrameType) && + (!final || length > maxControlFramePayloadSize) { + c.writeSeq++ + c.writeFrameType = noFrame + c.writePos = maxFrameHeaderSize + return errInvalidControlFrame + } + + b0 := byte(c.writeFrameType) + if final { + b0 |= finalBit + } + b1 := byte(0) + if !c.isServer { + b1 |= maskBit + } + + // Assume that the frame starts at beginning of c.writeBuf. + framePos := 0 + if c.isServer { + // Adjust up if mask not included in the header. + framePos = 4 + } + + switch { + case length >= 65536: + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 127 + binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length)) + case length > 125: + framePos += 6 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 126 + binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length)) + default: + framePos += 8 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | byte(length) + } + + if !c.isServer { + key := newMaskKey() + copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) + maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:c.writePos]) + if len(extra) > 0 { + c.writeErr = errors.New("websocket: internal error, extra used in client mode") + return c.writeErr + } + } + + // Write the buffers to the connection with best-effort detection of + // concurrent writes. See the concurrency section in the package + // documentation for more info. + + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + + c.writeErr = c.write(c.writeFrameType, c.writeDeadline, c.writeBuf[framePos:c.writePos], extra) + + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + + // Setup for next frame. + c.writePos = maxFrameHeaderSize + c.writeFrameType = continuationFrame + if final { + c.writeSeq++ + c.writeFrameType = noFrame + } + return c.writeErr +} + +type messageWriter struct { + c *Conn + seq int +} + +func (w messageWriter) err() error { + c := w.c + if c.writeSeq != w.seq { + return errWriteClosed + } + if c.writeErr != nil { + return c.writeErr + } + return nil +} + +func (w messageWriter) ncopy(max int) (int, error) { + n := len(w.c.writeBuf) - w.c.writePos + if n <= 0 { + if err := w.c.flushFrame(false, nil); err != nil { + return 0, err + } + n = len(w.c.writeBuf) - w.c.writePos + } + if n > max { + n = max + } + return n, nil +} + +func (w messageWriter) write(final bool, p []byte) (int, error) { + if err := w.err(); err != nil { + return 0, err + } + + if len(p) > 2*len(w.c.writeBuf) && w.c.isServer { + // Don't buffer large messages. + err := w.c.flushFrame(final, p) + if err != nil { + return 0, err + } + return len(p), nil + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.c.writePos:], p[:n]) + w.c.writePos += n + p = p[n:] + } + return nn, nil +} + +func (w messageWriter) Write(p []byte) (int, error) { + return w.write(false, p) +} + +func (w messageWriter) WriteString(p string) (int, error) { + if err := w.err(); err != nil { + return 0, err + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.c.writePos:], p[:n]) + w.c.writePos += n + p = p[n:] + } + return nn, nil +} + +func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { + if err := w.err(); err != nil { + return 0, err + } + for { + if w.c.writePos == len(w.c.writeBuf) { + err = w.c.flushFrame(false, nil) + if err != nil { + break + } + } + var n int + n, err = r.Read(w.c.writeBuf[w.c.writePos:]) + w.c.writePos += n + nn += int64(n) + if err != nil { + if err == io.EOF { + err = nil + } + break + } + } + return nn, err +} + +func (w messageWriter) Close() error { + if err := w.err(); err != nil { + return err + } + return w.c.flushFrame(true, nil) +} + +// WriteMessage is a helper method for getting a writer using NextWriter, +// writing the message and closing the writer. +func (c *Conn) WriteMessage(messageType int, data []byte) error { + wr, err := c.NextWriter(messageType) + if err != nil { + return err + } + w := wr.(messageWriter) + if _, err := w.write(true, data); err != nil { + return err + } + if c.writeSeq == w.seq { + if err := c.flushFrame(true, nil); err != nil { + return err + } + } + return nil +} + +// SetWriteDeadline sets the write deadline on the underlying network +// connection. After a write has timed out, the websocket state is corrupt and +// all future writes will return an error. A zero value for t means writes will +// not time out. +func (c *Conn) SetWriteDeadline(t time.Time) error { + c.writeDeadline = t + return nil +} + +// Read methods + +// readFull is like io.ReadFull except that io.EOF is never returned. +func (c *Conn) readFull(p []byte) (err error) { + var n int + for n < len(p) && err == nil { + var nn int + nn, err = c.br.Read(p[n:]) + n += nn + } + if n == len(p) { + err = nil + } else if err == io.EOF { + err = errUnexpectedEOF + } + return +} + +func (c *Conn) advanceFrame() (int, error) { + + // 1. Skip remainder of previous frame. + + if c.readRemaining > 0 { + if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil { + return noFrame, err + } + } + + // 2. Read and parse first two bytes of frame header. + + var b [8]byte + if err := c.readFull(b[:2]); err != nil { + return noFrame, err + } + + final := b[0]&finalBit != 0 + frameType := int(b[0] & 0xf) + reserved := int((b[0] >> 4) & 0x7) + mask := b[1]&maskBit != 0 + c.readRemaining = int64(b[1] & 0x7f) + + if reserved != 0 { + return noFrame, c.handleProtocolError("unexpected reserved bits " + strconv.Itoa(reserved)) + } + + switch frameType { + case CloseMessage, PingMessage, PongMessage: + if c.readRemaining > maxControlFramePayloadSize { + return noFrame, c.handleProtocolError("control frame length > 125") + } + if !final { + return noFrame, c.handleProtocolError("control frame not final") + } + case TextMessage, BinaryMessage: + if !c.readFinal { + return noFrame, c.handleProtocolError("message start before final message frame") + } + c.readFinal = final + case continuationFrame: + if c.readFinal { + return noFrame, c.handleProtocolError("continuation after final message frame") + } + c.readFinal = final + default: + return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType)) + } + + // 3. Read and parse frame length. + + switch c.readRemaining { + case 126: + if err := c.readFull(b[:2]); err != nil { + return noFrame, err + } + c.readRemaining = int64(binary.BigEndian.Uint16(b[:2])) + case 127: + if err := c.readFull(b[:8]); err != nil { + return noFrame, err + } + c.readRemaining = int64(binary.BigEndian.Uint64(b[:8])) + } + + // 4. Handle frame masking. + + if mask != c.isServer { + return noFrame, c.handleProtocolError("incorrect mask flag") + } + + if mask { + c.readMaskPos = 0 + if err := c.readFull(c.readMaskKey[:]); err != nil { + return noFrame, err + } + } + + // 5. For text and binary messages, enforce read limit and return. + + if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage { + + c.readLength += c.readRemaining + if c.readLimit > 0 && c.readLength > c.readLimit { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)) + return noFrame, ErrReadLimit + } + + return frameType, nil + } + + // 6. Read control frame payload. + + var payload []byte + if c.readRemaining > 0 { + payload = make([]byte, c.readRemaining) + c.readRemaining = 0 + if err := c.readFull(payload); err != nil { + return noFrame, err + } + if c.isServer { + maskBytes(c.readMaskKey, 0, payload) + } + } + + // 7. Process control frame payload. + + switch frameType { + case PongMessage: + if err := c.handlePong(string(payload)); err != nil { + return noFrame, err + } + case PingMessage: + if err := c.handlePing(string(payload)); err != nil { + return noFrame, err + } + case CloseMessage: + echoMessage := []byte{} + closeCode := CloseNoStatusReceived + closeText := "" + if len(payload) >= 2 { + echoMessage = payload[:2] + closeCode = int(binary.BigEndian.Uint16(payload)) + closeText = string(payload[2:]) + } + c.WriteControl(CloseMessage, echoMessage, time.Now().Add(writeWait)) + return noFrame, &CloseError{Code: closeCode, Text: closeText} + } + + return frameType, nil +} + +func (c *Conn) handleProtocolError(message string) error { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait)) + return errors.New("websocket: " + message) +} + +// NextReader returns the next data message received from the peer. The +// returned messageType is either TextMessage or BinaryMessage. +// +// There can be at most one open reader on a connection. NextReader discards +// the previous message if the application has not already consumed it. +// +// Applications must break out of the application's read loop when this method +// returns a non-nil error value. Errors returned from this method are +// permanent. Once this method returns a non-nil error, all subsequent calls to +// this method return the same error. +func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { + + c.readSeq++ + c.readLength = 0 + + for c.readErr == nil { + frameType, err := c.advanceFrame() + if err != nil { + c.readErr = hideTempErr(err) + break + } + if frameType == TextMessage || frameType == BinaryMessage { + return frameType, messageReader{c, c.readSeq}, nil + } + } + + // Applications that do handle the error returned from this method spin in + // tight loop on connection failure. To help application developers detect + // this error, panic on repeated reads to the failed connection. + c.readErrCount++ + if c.readErrCount >= 1000 { + panic("repeated read on failed websocket connection") + } + + return noFrame, nil, c.readErr +} + +type messageReader struct { + c *Conn + seq int +} + +func (r messageReader) Read(b []byte) (int, error) { + + if r.seq != r.c.readSeq { + return 0, io.EOF + } + + for r.c.readErr == nil { + + if r.c.readRemaining > 0 { + if int64(len(b)) > r.c.readRemaining { + b = b[:r.c.readRemaining] + } + n, err := r.c.br.Read(b) + r.c.readErr = hideTempErr(err) + if r.c.isServer { + r.c.readMaskPos = maskBytes(r.c.readMaskKey, r.c.readMaskPos, b[:n]) + } + r.c.readRemaining -= int64(n) + return n, r.c.readErr + } + + if r.c.readFinal { + r.c.readSeq++ + return 0, io.EOF + } + + frameType, err := r.c.advanceFrame() + switch { + case err != nil: + r.c.readErr = hideTempErr(err) + case frameType == TextMessage || frameType == BinaryMessage: + r.c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") + } + } + + err := r.c.readErr + if err == io.EOF && r.seq == r.c.readSeq { + err = errUnexpectedEOF + } + return 0, err +} + +// ReadMessage is a helper method for getting a reader using NextReader and +// reading from that reader to a buffer. +func (c *Conn) ReadMessage() (messageType int, p []byte, err error) { + var r io.Reader + messageType, r, err = c.NextReader() + if err != nil { + return messageType, nil, err + } + p, err = ioutil.ReadAll(r) + return messageType, p, err +} + +// SetReadDeadline sets the read deadline on the underlying network connection. +// After a read has timed out, the websocket connection state is corrupt and +// all future reads will return an error. A zero value for t means reads will +// not time out. +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +// SetReadLimit sets the maximum size for a message read from the peer. If a +// message exceeds the limit, the connection sends a close frame to the peer +// and returns ErrReadLimit to the application. +func (c *Conn) SetReadLimit(limit int64) { + c.readLimit = limit +} + +// SetPingHandler sets the handler for ping messages received from the peer. +// The appData argument to h is the PING frame application data. The default +// ping handler sends a pong to the peer. +func (c *Conn) SetPingHandler(h func(appData string) error) { + if h == nil { + h = func(message string) error { + err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) + if err == ErrCloseSent { + return nil + } else if e, ok := err.(net.Error); ok && e.Temporary() { + return nil + } + return err + } + } + c.handlePing = h +} + +// SetPongHandler sets the handler for pong messages received from the peer. +// The appData argument to h is the PONG frame application data. The default +// pong handler does nothing. +func (c *Conn) SetPongHandler(h func(appData string) error) { + if h == nil { + h = func(string) error { return nil } + } + c.handlePong = h +} + +// UnderlyingConn returns the internal net.Conn. This can be used to further +// modifications to connection specific flags. +func (c *Conn) UnderlyingConn() net.Conn { + return c.conn +} + +// FormatCloseMessage formats closeCode and text as a WebSocket close message. +func FormatCloseMessage(closeCode int, text string) []byte { + buf := make([]byte, 2+len(text)) + binary.BigEndian.PutUint16(buf, uint16(closeCode)) + copy(buf[2:], text) + return buf +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/doc.go b/Godeps/_workspace/src/github.com/gorilla/websocket/doc.go new file mode 100644 index 00000000000..499b03dbd9a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/doc.go @@ -0,0 +1,148 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package websocket implements the WebSocket protocol defined in RFC 6455. +// +// Overview +// +// The Conn type represents a WebSocket connection. A server application uses +// the Upgrade function from an Upgrader object with a HTTP request handler +// to get a pointer to a Conn: +// +// var upgrader = websocket.Upgrader{ +// ReadBufferSize: 1024, +// WriteBufferSize: 1024, +// } +// +// func handler(w http.ResponseWriter, r *http.Request) { +// conn, err := upgrader.Upgrade(w, r, nil) +// if err != nil { +// log.Println(err) +// return +// } +// ... Use conn to send and receive messages. +// } +// +// Call the connection's WriteMessage and ReadMessage methods to send and +// receive messages as a slice of bytes. This snippet of code shows how to echo +// messages using these methods: +// +// for { +// messageType, p, err := conn.ReadMessage() +// if err != nil { +// return +// } +// if err = conn.WriteMessage(messageType, p); err != nil { +// return err +// } +// } +// +// In above snippet of code, p is a []byte and messageType is an int with value +// websocket.BinaryMessage or websocket.TextMessage. +// +// An application can also send and receive messages using the io.WriteCloser +// and io.Reader interfaces. To send a message, call the connection NextWriter +// method to get an io.WriteCloser, write the message to the writer and close +// the writer when done. To receive a message, call the connection NextReader +// method to get an io.Reader and read until io.EOF is returned. This snippet +// shows how to echo messages using the NextWriter and NextReader methods: +// +// for { +// messageType, r, err := conn.NextReader() +// if err != nil { +// return +// } +// w, err := conn.NextWriter(messageType) +// if err != nil { +// return err +// } +// if _, err := io.Copy(w, r); err != nil { +// return err +// } +// if err := w.Close(); err != nil { +// return err +// } +// } +// +// Data Messages +// +// The WebSocket protocol distinguishes between text and binary data messages. +// Text messages are interpreted as UTF-8 encoded text. The interpretation of +// binary messages is left to the application. +// +// This package uses the TextMessage and BinaryMessage integer constants to +// identify the two data message types. The ReadMessage and NextReader methods +// return the type of the received message. The messageType argument to the +// WriteMessage and NextWriter methods specifies the type of a sent message. +// +// It is the application's responsibility to ensure that text messages are +// valid UTF-8 encoded text. +// +// Control Messages +// +// The WebSocket protocol defines three types of control messages: close, ping +// and pong. Call the connection WriteControl, WriteMessage or NextWriter +// methods to send a control message to the peer. +// +// Connections handle received ping and pong messages by invoking callback +// functions set with SetPingHandler and SetPongHandler methods. The default +// ping handler sends a pong to the client. The callback functions can be +// invoked from the NextReader, ReadMessage or the message Read method. +// +// Connections handle received close messages by sending a close message to the +// peer and returning a *CloseError from the the NextReader, ReadMessage or the +// message Read method. +// +// The application must read the connection to process ping and close messages +// sent from the peer. If the application is not otherwise interested in +// messages from the peer, then the application should start a goroutine to +// read and discard messages from the peer. A simple example is: +// +// func readLoop(c *websocket.Conn) { +// for { +// if _, _, err := c.NextReader(); err != nil { +// c.Close() +// break +// } +// } +// } +// +// Concurrency +// +// Connections support one concurrent reader and one concurrent writer. +// +// Applications are responsible for ensuring that no more than one goroutine +// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage, +// WriteJSON) concurrently and that no more than one goroutine calls the read +// methods (NextReader, SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, +// SetPingHandler) concurrently. +// +// The Close and WriteControl methods can be called concurrently with all other +// methods. +// +// Origin Considerations +// +// Web browsers allow Javascript applications to open a WebSocket connection to +// any host. It's up to the server to enforce an origin policy using the Origin +// request header sent by the browser. +// +// The Upgrader calls the function specified in the CheckOrigin field to check +// the origin. If the CheckOrigin function returns false, then the Upgrade +// method fails the WebSocket handshake with HTTP status 403. +// +// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail +// the handshake if the Origin request header is present and not equal to the +// Host request header. +// +// An application can allow connections from any origin by specifying a +// function that always returns true: +// +// var upgrader = websocket.Upgrader{ +// CheckOrigin: func(r *http.Request) bool { return true }, +// } +// +// The deprecated Upgrade function does not enforce an origin policy. It's the +// application's responsibility to check the Origin header before calling +// Upgrade. +package websocket diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/json.go b/Godeps/_workspace/src/github.com/gorilla/websocket/json.go new file mode 100644 index 00000000000..4f0e36875a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/json.go @@ -0,0 +1,55 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "encoding/json" + "io" +) + +// WriteJSON is deprecated, use c.WriteJSON instead. +func WriteJSON(c *Conn, v interface{}) error { + return c.WriteJSON(v) +} + +// WriteJSON writes the JSON encoding of v to the connection. +// +// See the documentation for encoding/json Marshal for details about the +// conversion of Go values to JSON. +func (c *Conn) WriteJSON(v interface{}) error { + w, err := c.NextWriter(TextMessage) + if err != nil { + return err + } + err1 := json.NewEncoder(w).Encode(v) + err2 := w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +// ReadJSON is deprecated, use c.ReadJSON instead. +func ReadJSON(c *Conn, v interface{}) error { + return c.ReadJSON(v) +} + +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// See the documentation for the encoding/json Unmarshal function for details +// about the conversion of JSON to a Go value. +func (c *Conn) ReadJSON(v interface{}) error { + _, r, err := c.NextReader() + if err != nil { + return err + } + err = json.NewDecoder(r).Decode(v) + if err == io.EOF { + // One value is expected in the message. + err = io.ErrUnexpectedEOF + } + return err +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/server.go b/Godeps/_workspace/src/github.com/gorilla/websocket/server.go new file mode 100644 index 00000000000..85616c79743 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/server.go @@ -0,0 +1,253 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "errors" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// HandshakeError describes an error with the handshake from the peer. +type HandshakeError struct { + message string +} + +func (e HandshakeError) Error() string { return e.message } + +// Upgrader specifies parameters for upgrading an HTTP connection to a +// WebSocket connection. +type Upgrader struct { + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer + // size is zero, then a default value of 4096 is used. The I/O buffer sizes + // do not limit the size of the messages that can be sent or received. + ReadBufferSize, WriteBufferSize int + + // Subprotocols specifies the server's supported protocols in order of + // preference. If this field is set, then the Upgrade method negotiates a + // subprotocol by selecting the first match in this list with a protocol + // requested by the client. + Subprotocols []string + + // Error specifies the function for generating HTTP error responses. If Error + // is nil, then http.Error is used to generate the HTTP response. + Error func(w http.ResponseWriter, r *http.Request, status int, reason error) + + // CheckOrigin returns true if the request Origin header is acceptable. If + // CheckOrigin is nil, the host in the Origin header must not be set or + // must match the host of the request. + CheckOrigin func(r *http.Request) bool +} + +func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) { + err := HandshakeError{reason} + if u.Error != nil { + u.Error(w, r, status, err) + } else { + http.Error(w, http.StatusText(status), status) + } + return nil, err +} + +// checkSameOrigin returns true if the origin is not set or is equal to the request host. +func checkSameOrigin(r *http.Request) bool { + origin := r.Header["Origin"] + if len(origin) == 0 { + return true + } + u, err := url.Parse(origin[0]) + if err != nil { + return false + } + return u.Host == r.Host +} + +func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { + if u.Subprotocols != nil { + clientProtocols := Subprotocols(r) + for _, serverProtocol := range u.Subprotocols { + for _, clientProtocol := range clientProtocols { + if clientProtocol == serverProtocol { + return clientProtocol + } + } + } + } else if responseHeader != nil { + return responseHeader.Get("Sec-Websocket-Protocol") + } + return "" +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// application negotiated subprotocol (Sec-Websocket-Protocol). +// +// If the upgrade fails, then Upgrade replies to the client with an HTTP error +// response. +func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { + if r.Method != "GET" { + return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: method not GET") + } + if values := r.Header["Sec-Websocket-Version"]; len(values) == 0 || values[0] != "13" { + return u.returnError(w, r, http.StatusBadRequest, "websocket: version != 13") + } + + if !tokenListContainsValue(r.Header, "Connection", "upgrade") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: could not find connection header with token 'upgrade'") + } + + if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: could not find upgrade header with token 'websocket'") + } + + checkOrigin := u.CheckOrigin + if checkOrigin == nil { + checkOrigin = checkSameOrigin + } + if !checkOrigin(r) { + return u.returnError(w, r, http.StatusForbidden, "websocket: origin not allowed") + } + + challengeKey := r.Header.Get("Sec-Websocket-Key") + if challengeKey == "" { + return u.returnError(w, r, http.StatusBadRequest, "websocket: key missing or blank") + } + + subprotocol := u.selectSubprotocol(r, responseHeader) + + var ( + netConn net.Conn + br *bufio.Reader + err error + ) + + h, ok := w.(http.Hijacker) + if !ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") + } + var rw *bufio.ReadWriter + netConn, rw, err = h.Hijack() + if err != nil { + return u.returnError(w, r, http.StatusInternalServerError, err.Error()) + } + br = rw.Reader + + if br.Buffered() > 0 { + netConn.Close() + return nil, errors.New("websocket: client sent data before handshake is complete") + } + + c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize) + c.subprotocol = subprotocol + + p := c.writeBuf[:0] + p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) + p = append(p, computeAcceptKey(challengeKey)...) + p = append(p, "\r\n"...) + if c.subprotocol != "" { + p = append(p, "Sec-Websocket-Protocol: "...) + p = append(p, c.subprotocol...) + p = append(p, "\r\n"...) + } + for k, vs := range responseHeader { + if k == "Sec-Websocket-Protocol" { + continue + } + for _, v := range vs { + p = append(p, k...) + p = append(p, ": "...) + for i := 0; i < len(v); i++ { + b := v[i] + if b <= 31 { + // prevent response splitting. + b = ' ' + } + p = append(p, b) + } + p = append(p, "\r\n"...) + } + } + p = append(p, "\r\n"...) + + // Clear deadlines set by HTTP server. + netConn.SetDeadline(time.Time{}) + + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout)) + } + if _, err = netConn.Write(p); err != nil { + netConn.Close() + return nil, err + } + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Time{}) + } + + return c, nil +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// This function is deprecated, use websocket.Upgrader instead. +// +// The application is responsible for checking the request origin before +// calling Upgrade. An example implementation of the same origin policy is: +// +// if req.Header.Get("Origin") != "http://"+req.Host { +// http.Error(w, "Origin not allowed", 403) +// return +// } +// +// If the endpoint supports subprotocols, then the application is responsible +// for negotiating the protocol used on the connection. Use the Subprotocols() +// function to get the subprotocols requested by the client. Use the +// Sec-Websocket-Protocol response header to specify the subprotocol selected +// by the application. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// negotiated subprotocol (Sec-Websocket-Protocol). +// +// The connection buffers IO to the underlying network connection. The +// readBufSize and writeBufSize parameters specify the size of the buffers to +// use. Messages can be larger than the buffers. +// +// If the request is not a valid WebSocket handshake, then Upgrade returns an +// error of type HandshakeError. Applications should handle this error by +// replying to the client with an HTTP error response. +func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) { + u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize} + u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) { + // don't return errors to maintain backwards compatibility + } + u.CheckOrigin = func(r *http.Request) bool { + // allow all connections by default + return true + } + return u.Upgrade(w, r, responseHeader) +} + +// Subprotocols returns the subprotocols requested by the client in the +// Sec-Websocket-Protocol header. +func Subprotocols(r *http.Request) []string { + h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol")) + if h == "" { + return nil + } + protocols := strings.Split(h, ",") + for i := range protocols { + protocols[i] = strings.TrimSpace(protocols[i]) + } + return protocols +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/util.go b/Godeps/_workspace/src/github.com/gorilla/websocket/util.go new file mode 100644 index 00000000000..ffdc265ed78 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/util.go @@ -0,0 +1,44 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "io" + "net/http" + "strings" +) + +// tokenListContainsValue returns true if the 1#token header with the given +// name contains token. +func tokenListContainsValue(header http.Header, name string, value string) bool { + for _, v := range header[name] { + for _, s := range strings.Split(v, ",") { + if strings.EqualFold(value, strings.TrimSpace(s)) { + return true + } + } + } + return false +} + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +func computeAcceptKey(challengeKey string) string { + h := sha1.New() + h.Write([]byte(challengeKey)) + h.Write(keyGUID) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func generateChallengeKey() (string, error) { + p := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, p); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(p), nil +} From 585503f9fb4b9cb0f0be51a04812f087391fa2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 13:11:55 +0100 Subject: [PATCH 060/268] fix(): renamed plugin.json for wip for new work in progress data source --- .../plugins/datasource/grafana-live/{plugin.json => _plugin.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename public/app/plugins/datasource/grafana-live/{plugin.json => _plugin.json} (100%) diff --git a/public/app/plugins/datasource/grafana-live/plugin.json b/public/app/plugins/datasource/grafana-live/_plugin.json similarity index 100% rename from public/app/plugins/datasource/grafana-live/plugin.json rename to public/app/plugins/datasource/grafana-live/_plugin.json From 87aca5bf1b3fc142fbe56571f60d77e5f6a9da39 Mon Sep 17 00:00:00 2001 From: David Warden Date: Wed, 23 Mar 2016 08:21:25 -0400 Subject: [PATCH 061/268] 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 637db29fadfff4be4668a3e341cbd78960e40ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 13:26:38 +0100 Subject: [PATCH 062/268] tech(): updated phantomjs npm package to use phantomjs-prebuilt --- package.json | 4 ++-- tasks/options/phantomjs.js | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index e2e65472cc3..3c6729a575c 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,10 @@ "karma-coveralls": "1.1.2", "karma-expect": "~1.1.0", "karma-mocha": "~0.2.1", - "karma-phantomjs-launcher": "0.2.1", + "karma-phantomjs-launcher": "1.0.0", "load-grunt-tasks": "3.4.0", "mocha": "2.3.4", - "phantomjs": "^2.1.3", + "phantomjs-prebuilt": "^2.1.3", "reflect-metadata": "0.1.2", "rxjs": "5.0.0-beta.2", "sass-lint": "^1.5.0", diff --git a/tasks/options/phantomjs.js b/tasks/options/phantomjs.js index 300688f5ba2..ae777a1cf74 100644 --- a/tasks/options/phantomjs.js +++ b/tasks/options/phantomjs.js @@ -4,12 +4,7 @@ module.exports = function(config,grunt) { grunt.registerTask('phantomjs', 'Copy phantomjs binary from node', function() { var dest = './vendor/phantomjs/phantomjs'; - var confDir = './node_modules/karma-phantomjs-launcher/node_modules/phantomjs/lib/' - - if (!grunt.file.exists(confDir)) { - // npm 3 or npm 2 with dedupe - confDir = './node_modules/phantomjs/lib/'; - } + var confDir = './node_modules/phantomjs-prebuilt/lib/'; if (!grunt.file.exists(dest)){ From 53312852e9941ab3e99f7cd22896612971c394c5 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 22 Mar 2016 20:23:27 +0100 Subject: [PATCH 063/268] tests(influxdb): adds tests for influxdb response --- .../plugins/datasource/influxdb/datasource.ts | 23 ++------ .../datasource/influxdb/response_parser.ts | 23 ++++++++ .../influxdb/specs/response_parser_specs.ts | 58 +++++++++++++++++++ 3 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 public/app/plugins/datasource/influxdb/response_parser.ts create mode 100644 public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 1136c9709bd..7052201b422 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -6,6 +6,7 @@ import _ from 'lodash'; import * as dateMath from 'app/core/utils/datemath'; import InfluxSeries from './influx_series'; import InfluxQuery from './influx_query'; +import ResponseParser from './response_parser'; /** @ngInject */ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) { @@ -22,6 +23,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) this.interval = (instanceSettings.jsonData || {}).timeInterval; this.supportAnnotations = true; this.supportMetrics = true; + this.responseParser = new ResponseParser(); this.query = function(options) { var timeFilter = getTimeFilter(options); @@ -101,7 +103,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) }); }; - this.metricFindQuery = function (query) { + this.metricFindQuery = function (query, queryType) { var interpolated; try { interpolated = templateSrv.replace(query, null, 'regex'); @@ -109,23 +111,8 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) return $q.reject(err); } - return this._seriesQuery(interpolated).then(function (results) { - if (!results || results.results.length === 0) { return []; } - - var influxResults = results.results[0]; - if (!influxResults.series) { - return []; - } - - var series = influxResults.series[0]; - return _.map(series.values, function(value) { - if (_.isArray(value)) { - return { text: value[0] }; - } else { - return { text: value }; - } - }); - }); + return this._seriesQuery(interpolated) + .then(_.curry(this.responseParser.parse)(queryType)); }; this._seriesQuery = function(query) { diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts new file mode 100644 index 00000000000..8d2cdff0e54 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -0,0 +1,23 @@ +/// + +import _ from 'lodash'; + +export default class ResponseParser { + parse(queryType, results) { + if (!results || results.results.length === 0) { return []; } + + var influxResults = results.results[0]; + if (!influxResults.series) { + return []; + } + + var series = influxResults.series[0]; + return _.map(series.values, function(value) { + if (_.isArray(value)) { + return { text: value[0] }; + } else { + return { text: value }; + } + }); + } +} diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts new file mode 100644 index 00000000000..a52c98f0e05 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts @@ -0,0 +1,58 @@ +import _ from 'lodash'; +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; +import ResponseParser from '../response_parser'; + +describe.only("influxdb response parser", () => { + describe("SHOW_TAGS response", () => { + this.parser = new ResponseParser(); + + describe("response from 0.10.0", () => { + var response = { + "results": [ + { + "series": [ + { + "name": "hostnameTagValues", + "columns": ["hostname"], + "values": [ ["server1"], ["server2"] ] + } + ] + } + ] + }; + + var result = this.parser.parse('SHOW_TAGS', response); + + it("should get two responses", () => { + expect(_.size(result)).to.be(2); + expect(result[0].text).to.be("server1"); + expect(result[1].text).to.be("server2"); + }); + }); + }); + + describe("SHOW_FIELDS response", () => { + describe("response from 0.10.0", () => { + var response = { + "results": [ + { + "series": [ + { + "name": "measurements", + "columns": ["name"], + "values": [ + ["cpu"], ["derivative"], ["logins.count"], ["logs"], ["payment.ended"], ["payment.started"] + ] + } + ] + } + ] + }; + + var result = this.parser.parse('SHOW_FIELDS', response); + it("should get two responses", () => { + expect(_.size(result)).to.be(6); + }); + }); + }); +}); From 6fafc8dba1dee490dc782bd4122522b0054026b7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 22 Mar 2016 22:43:55 +0100 Subject: [PATCH 064/268] add(influxdb): add support for 0.11.0 tags --- .../datasource/influxdb/response_parser.ts | 33 +++++++----- .../influxdb/specs/response_parser_specs.ts | 51 +++++++++++++++++-- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts index 8d2cdff0e54..ff99ee24284 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -3,21 +3,26 @@ import _ from 'lodash'; export default class ResponseParser { - parse(queryType, results) { - if (!results || results.results.length === 0) { return []; } - var influxResults = results.results[0]; - if (!influxResults.series) { - return []; - } + parse(queryType, results) { + if (!results || results.results.length === 0) { return []; } - var series = influxResults.series[0]; - return _.map(series.values, function(value) { - if (_.isArray(value)) { - return { text: value[0] }; - } else { - return { text: value }; - } - }); + var influxResults = results.results[0]; + if (!influxResults.series) { + return []; } + + var series = influxResults.series[0]; + return _.map(series.values, function(value) { + if (_.isArray(value)) { + if (queryType === 'SHOW_TAGS') { + return { text: (value[1] || value[0]) }; + } else { + return { text: value[0] }; + } + } else { + return { text: value }; + } + }); + } } diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts index a52c98f0e05..506d828ab7e 100644 --- a/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts +++ b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts @@ -2,10 +2,9 @@ import _ from 'lodash'; import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; import ResponseParser from '../response_parser'; -describe.only("influxdb response parser", () => { +describe("influxdb response parser", () => { + this.parser = new ResponseParser(); describe("SHOW_TAGS response", () => { - this.parser = new ResponseParser(); - describe("response from 0.10.0", () => { var response = { "results": [ @@ -29,6 +28,30 @@ describe.only("influxdb response parser", () => { expect(result[1].text).to.be("server2"); }); }); + + describe("response from 0.11.0", () => { + var response = { + "results": [ + { + "series": [ + { + "name": "cpu", + "columns": [ "key", "value"], + "values": [ [ "source", "site" ], [ "source", "api" ] ] + } + ] + } + ] + }; + + var result = this.parser.parse('SHOW_TAGS', response); + + it("should get two responses", () => { + expect(_.size(result)).to.be(2); + expect(result[0].text).to.be('site'); + expect(result[1].text).to.be('api'); + }); + }); }); describe("SHOW_FIELDS response", () => { @@ -54,5 +77,27 @@ describe.only("influxdb response parser", () => { expect(_.size(result)).to.be(6); }); }); + + describe("response from 0.11.0", () => { + var response = { + "results": [ + { + "series": [ + { + "name": "cpu", + "columns": ["fieldKey"], + "values": [ [ "value"] ] + } + ] + } + ] + }; + + var result = this.parser.parse('SHOW_FIELDS', response); + + it("should get two responses", () => { + expect(_.size(result)).to.be(1); + }); + }); }); }); From ba48f40d2181eae2eace4eedfb562dc5be267a4d Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 23 Mar 2016 11:09:57 +0100 Subject: [PATCH 065/268] feat(influxdb): bases parsing upon query --- .../plugins/datasource/influxdb/datasource.ts | 2 +- .../datasource/influxdb/response_parser.ts | 6 +- .../influxdb/specs/response_parser_specs.ts | 66 +++++++++++++++++-- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 7052201b422..fd5f4b0bb77 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -112,7 +112,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) } return this._seriesQuery(interpolated) - .then(_.curry(this.responseParser.parse)(queryType)); + .then(_.curry(this.responseParser.parse)(query)); }; this._seriesQuery = function(query) { diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts index ff99ee24284..ae6f2cb75a9 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -4,7 +4,7 @@ import _ from 'lodash'; export default class ResponseParser { - parse(queryType, results) { + parse(query, results) { if (!results || results.results.length === 0) { return []; } var influxResults = results.results[0]; @@ -13,9 +13,9 @@ export default class ResponseParser { } var series = influxResults.series[0]; - return _.map(series.values, function(value) { + return _.map(series.values, (value) => { if (_.isArray(value)) { - if (queryType === 'SHOW_TAGS') { + if (query.indexOf('SHOW TAG VALUES') >= 0) { return { text: (value[1] || value[0]) }; } else { return { text: value[0] }; diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts index 506d828ab7e..d83b3ab4fa2 100644 --- a/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts +++ b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts @@ -4,7 +4,56 @@ import ResponseParser from '../response_parser'; describe("influxdb response parser", () => { this.parser = new ResponseParser(); - describe("SHOW_TAGS response", () => { + describe("SHOW TAG response", () => { + var query = 'SHOW TAG KEYS FROM "cpu"'; + describe("response from 0.10.0", () => { + var response = { + "results": [ + { + "series": [ + { + "name": "cpu", + "columns": ["tagKey"], + "values": [ ["datacenter"], ["hostname"], ["source"] ] + } + ] + } + ] + }; + + var result = this.parser.parse(query, response); + + it("expects three results", () => { + expect(_.size(result)).to.be(3); + }); + }); + + describe("response from 0.11.0", () => { + var response = { + "results": [ + { + "series": [ + { + "name": "cpu", + "columns": ["tagKey"], + "values": [ ["datacenter"], ["hostname"], ["source"] ] + } + ] + } + ] + }; + + var result = this.parser.parse(query, response); + + it("expects three results", () => { + expect(_.size(result)).to.be(3); + }); + }); + }); + + describe("SHOW TAG VALUES response", () => { + var query = 'SHOW TAG VALUES FROM "cpu" WITH KEY = "hostname"'; + describe("response from 0.10.0", () => { var response = { "results": [ @@ -20,7 +69,7 @@ describe("influxdb response parser", () => { ] }; - var result = this.parser.parse('SHOW_TAGS', response); + var result = this.parser.parse(query, response); it("should get two responses", () => { expect(_.size(result)).to.be(2); @@ -44,7 +93,7 @@ describe("influxdb response parser", () => { ] }; - var result = this.parser.parse('SHOW_TAGS', response); + var result = this.parser.parse(query, response); it("should get two responses", () => { expect(_.size(result)).to.be(2); @@ -52,9 +101,14 @@ describe("influxdb response parser", () => { expect(result[1].text).to.be('api'); }); }); + + + + }); - describe("SHOW_FIELDS response", () => { + describe("SHOW FIELD response", () => { + var query = 'SHOW FIELD KEYS FROM "cpu"'; describe("response from 0.10.0", () => { var response = { "results": [ @@ -72,7 +126,7 @@ describe("influxdb response parser", () => { ] }; - var result = this.parser.parse('SHOW_FIELDS', response); + var result = this.parser.parse(query, response); it("should get two responses", () => { expect(_.size(result)).to.be(6); }); @@ -93,7 +147,7 @@ describe("influxdb response parser", () => { ] }; - var result = this.parser.parse('SHOW_FIELDS', response); + var result = this.parser.parse(query, response); it("should get two responses", () => { expect(_.size(result)).to.be(1); From 5b75eea8cf22f9e69a5e4d6d2d9621521bf685cb Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 23 Mar 2016 14:00:22 +0100 Subject: [PATCH 066/268] fix(influxdb): remove unused parameter --- public/app/plugins/datasource/influxdb/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index fd5f4b0bb77..dabfd73bd76 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -103,7 +103,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) }); }; - this.metricFindQuery = function (query, queryType) { + this.metricFindQuery = (query) => { var interpolated; try { interpolated = templateSrv.replace(query, null, 'regex'); From 991b6eafcae2a412b8db89351d2eb61ca8b437c2 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 23 Mar 2016 14:42:18 +0100 Subject: [PATCH 067/268] tech(influxdb): convert datasource to TS class --- .../plugins/datasource/influxdb/datasource.ts | 96 +++++++++++-------- .../datasource/influxdb/influx_query.ts | 1 + .../app/plugins/datasource/influxdb/module.ts | 2 +- 3 files changed, 57 insertions(+), 42 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index dabfd73bd76..daa6ada3420 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -8,35 +8,49 @@ import InfluxSeries from './influx_series'; import InfluxQuery from './influx_query'; import ResponseParser from './response_parser'; -/** @ngInject */ -export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) { - this.type = 'influxdb'; - this.urls = _.map(instanceSettings.url.split(','), function(url) { - return url.trim(); - }); +export default class InfluxDatasource { + type: string; + urls: any; + username: string; + password: string; + name: string; + database: any; + basicAuth: any; + interval: any; + supportAnnotations: boolean; + supportMetrics: boolean; + responseParser: any; - this.username = instanceSettings.username; - this.password = instanceSettings.password; - this.name = instanceSettings.name; - this.database = instanceSettings.database; - this.basicAuth = instanceSettings.basicAuth; - this.interval = (instanceSettings.jsonData || {}).timeInterval; - this.supportAnnotations = true; - this.supportMetrics = true; - this.responseParser = new ResponseParser(); + /** @ngInject */ + constructor(instanceSettings, private $q, private backendSrv, private templateSrv) { + this.type = 'influxdb'; + this.urls = _.map(instanceSettings.url.split(','), function(url) { + return url.trim(); + }); - this.query = function(options) { - var timeFilter = getTimeFilter(options); + this.username = instanceSettings.username; + this.password = instanceSettings.password; + this.name = instanceSettings.name; + this.database = instanceSettings.database; + this.basicAuth = instanceSettings.basicAuth; + this.interval = (instanceSettings.jsonData || {}).timeInterval; + this.supportAnnotations = true; + this.supportMetrics = true; + this.responseParser = new ResponseParser(); + } + + query(options) { + var timeFilter = this.getTimeFilter(options); var queryTargets = []; var i, y; - var allQueries = _.map(options.targets, function(target) { + var allQueries = _.map(options.targets, (target) => { if (target.hide) { return []; } queryTargets.push(target); // build query - var queryModel = new InfluxQuery(target, templateSrv, options.scopedVars); + var queryModel = new InfluxQuery(target, this.templateSrv, options.scopedVars); var query = queryModel.render(true); query = query.replace(/\$interval/g, (target.interval || options.interval)); return query; @@ -47,9 +61,9 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) allQueries = allQueries.replace(/\$timeFilter/g, timeFilter); // replace templated variables - allQueries = templateSrv.replace(allQueries, options.scopedVars); + allQueries = this.templateSrv.replace(allQueries, options.scopedVars); - return this._seriesQuery(allQueries).then(function(data): any { + return this._seriesQuery(allQueries).then((data): any => { if (!data || !data.results) { return []; } @@ -62,7 +76,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) var target = queryTargets[i]; var alias = target.alias; if (alias) { - alias = templateSrv.replace(target.alias, options.scopedVars); + alias = this.templateSrv.replace(target.alias, options.scopedVars); } var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias }); @@ -86,16 +100,16 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) }); }; - this.annotationQuery = function(options) { + annotationQuery(options) { if (!options.annotation.query) { - return $q.reject({message: 'Query missing in annotation definition'}); + return this.$q.reject({message: 'Query missing in annotation definition'}); } - var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw}); + var timeFilter = this.getTimeFilter({rangeRaw: options.rangeRaw}); var query = options.annotation.query.replace('$timeFilter', timeFilter); - query = templateSrv.replace(query); + query = this.templateSrv.replace(query); - return this._seriesQuery(query).then(function(data) { + return this._seriesQuery(query).then(data => { if (!data || !data.results || !data.results[0]) { throw { message: 'No results in response from InfluxDB' }; } @@ -103,29 +117,29 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) }); }; - this.metricFindQuery = (query) => { + metricFindQuery(query) { var interpolated; try { - interpolated = templateSrv.replace(query, null, 'regex'); + interpolated = this.templateSrv.replace(query, null, 'regex'); } catch (err) { - return $q.reject(err); + return this.$q.reject(err); } return this._seriesQuery(interpolated) .then(_.curry(this.responseParser.parse)(query)); }; - this._seriesQuery = function(query) { + _seriesQuery(query) { return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}); - }; + } - this.testDatasource = function() { - return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () { + testDatasource() { + return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(() => { return { status: "success", message: "Data source is working", title: "Success" }; }); - }; + } - this._influxRequest = function(method, url, data) { + _influxRequest(method, url, data) { var self = this; var currentUrl = self.urls.shift(); @@ -159,7 +173,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) options.headers.Authorization = self.basicAuth; } - return backendSrv.datasourceRequest(options).then(function(result) { + return this.backendSrv.datasourceRequest(options).then(result => { return result.data; }, function(err) { if (err.status !== 0 || err.status >= 300) { @@ -172,9 +186,9 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) }); }; - function getTimeFilter(options) { - var from = getInfluxTime(options.rangeRaw.from, false); - var until = getInfluxTime(options.rangeRaw.to, true); + getTimeFilter(options) { + var from = this.getInfluxTime(options.rangeRaw.from, false); + var until = this.getInfluxTime(options.rangeRaw.to, true); var fromIsAbsolute = from[from.length-1] === 's'; if (until === 'now()' && !fromIsAbsolute) { @@ -184,7 +198,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) return 'time > ' + from + ' and time < ' + until; } - function getInfluxTime(date, roundUp) { + getInfluxTime(date, roundUp) { if (_.isString(date)) { if (date === 'now') { return 'now()'; diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index 72d08909ac4..85457c7a8b3 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -11,6 +11,7 @@ export default class InfluxQuery { templateSrv: any; scopedVars: any; + /** @ngInject */ constructor(target, templateSrv?, scopedVars?) { this.target = target; this.templateSrv = templateSrv; diff --git a/public/app/plugins/datasource/influxdb/module.ts b/public/app/plugins/datasource/influxdb/module.ts index f2a21cc9022..26b067227a0 100644 --- a/public/app/plugins/datasource/influxdb/module.ts +++ b/public/app/plugins/datasource/influxdb/module.ts @@ -1,4 +1,4 @@ -import {InfluxDatasource} from './datasource'; +import InfluxDatasource from './datasource'; import {InfluxQueryCtrl} from './query_ctrl'; class InfluxConfigCtrl { From b7d198793f4591cd71e78da31be8e9119d8529b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 15:09:48 +0100 Subject: [PATCH 068/268] refactor(): minor refactoring of #4455 --- pkg/api/datasources.go | 2 +- public/sass/components/_gf-form.scss | 31 +++++++++++----------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index f685dcc2520..63c2ec57b7a 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -40,7 +40,7 @@ func GetDataSources(c *middleware.Context) { if plugin, exists := plugins.DataSources[ds.Type]; exists { dsItem.TypeLogoUrl = plugin.Info.Logos.Small } else { - dsItem.TypeLogoUrl = "public/img/icn-datasources.svg" + dsItem.TypeLogoUrl = "public/img/icn-datasource.svg" } result = append(result, dsItem) diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 197ad91db2b..6315f1ecf79 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; @@ -44,18 +42,12 @@ $gf-form-label-margin: 0.25rem; background-color: $input-label-bg; display: block; font-size: $font-size-sm; - margin-right: $gf-form-label-margin; + margin-right: $gf-form-margin; border: $input-btn-border-width solid transparent; @include border-radius($label-border-radius-sm); } -.gf-form-label--textarea { - line-height: $input-line-height * 2; - padding-top: 0px; - padding-bottom: 16px; // $spacer (1rem) was slightly too short. -} - .gf-form-checkbox { flex-shrink: 0; padding: $input-padding-y $input-padding-x; @@ -73,7 +65,7 @@ $gf-form-label-margin: 0.25rem; display: block; width: 100%; padding: $input-padding-y $input-padding-x; - margin-right: $gf-form-label-margin; + margin-right: $gf-form-margin; font-size: $font-size-base; line-height: $input-line-height; color: $input-color; @@ -118,7 +110,7 @@ $gf-form-label-margin: 0.25rem; .gf-form-select-wrapper { position: relative; background-color: $input-bg; - margin-right: $gf-form-label-margin; + margin-right: $gf-form-margin; select.gf-form-input { text-indent: .01px; @@ -147,18 +139,19 @@ $gf-form-label-margin: 0.25rem; } } -.gf-form--textarea { - align-items: flex-start; +.gf-form--v-stretch { + align-items: stretch; } -input[type="number"].gf-natural-language-form { - font-size: $font-size-base; - line-height: $input-line-height; - margin: -6px -5px 0 5px; - padding: $input-padding-y/2 $input-padding-x/2; -} +// input[type="number"].gf-natural-language-form { +// font-size: $font-size-base; +// line-height: $input-line-height; +// margin: -6px -5px 0 5px; +// padding: $input-padding-y/2 $input-padding-x/2; +// } .gf-form-btn { + margin-right: $gf-form-margin; padding: $input-padding-y $input-padding-x; line-height: $input-line-height; flex-shrink: 0; From 5f2f4a0897d9d460bd4c3b5b378268593522f41c Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 23 Mar 2016 15:23:14 +0100 Subject: [PATCH 069/268] test(influxdb): remove redundant test --- .../influxdb/specs/response_parser_specs.ts | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts index d83b3ab4fa2..e58fe32dd1b 100644 --- a/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts +++ b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts @@ -6,48 +6,24 @@ describe("influxdb response parser", () => { this.parser = new ResponseParser(); describe("SHOW TAG response", () => { var query = 'SHOW TAG KEYS FROM "cpu"'; - describe("response from 0.10.0", () => { - var response = { - "results": [ - { - "series": [ - { - "name": "cpu", - "columns": ["tagKey"], - "values": [ ["datacenter"], ["hostname"], ["source"] ] - } - ] - } - ] - }; + var response = { + "results": [ + { + "series": [ + { + "name": "cpu", + "columns": ["tagKey"], + "values": [ ["datacenter"], ["hostname"], ["source"] ] + } + ] + } + ] + }; - var result = this.parser.parse(query, response); + var result = this.parser.parse(query, response); - it("expects three results", () => { - expect(_.size(result)).to.be(3); - }); - }); - - describe("response from 0.11.0", () => { - var response = { - "results": [ - { - "series": [ - { - "name": "cpu", - "columns": ["tagKey"], - "values": [ ["datacenter"], ["hostname"], ["source"] ] - } - ] - } - ] - }; - - var result = this.parser.parse(query, response); - - it("expects three results", () => { - expect(_.size(result)).to.be(3); - }); + it("expects three results", () => { + expect(_.size(result)).to.be(3); }); }); From f8fb97d912dcc935267c746815d943fd5afb89bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 15:41:01 +0100 Subject: [PATCH 070/268] ux(): restored natural language input class --- public/sass/components/_gf-form.scss | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 6315f1ecf79..423e65a66ac 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -143,13 +143,6 @@ $gf-form-margin: 0.25rem; align-items: stretch; } -// input[type="number"].gf-natural-language-form { -// font-size: $font-size-base; -// line-height: $input-line-height; -// margin: -6px -5px 0 5px; -// padding: $input-padding-y/2 $input-padding-x/2; -// } - .gf-form-btn { margin-right: $gf-form-margin; padding: $input-padding-y $input-padding-x; @@ -157,3 +150,13 @@ $gf-form-margin: 0.25rem; flex-shrink: 0; flex-grow: 0; } + +.gf-form-natural-language-input { + &input[type="number"] { + font-size: $font-size-base; + line-height: $input-line-height; + margin: -6px -5px 0 5px; + padding: $input-padding-y/2 $input-padding-x/2; + } +} + From b06d8093642d14d47af6f8972af83fd9560234da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 15:42:46 +0100 Subject: [PATCH 071/268] ux(): changed name again --- public/sass/components/_gf-form.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 423e65a66ac..0b34cc1fa6a 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -151,7 +151,7 @@ $gf-form-margin: 0.25rem; flex-grow: 0; } -.gf-form-natural-language-input { +.natural-language-input { &input[type="number"] { font-size: $font-size-base; line-height: $input-line-height; From 5dfac9a7658baa07dc6b99ad5fdd74cb24ff1ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 16:37:58 +0100 Subject: [PATCH 072/268] feat(plugins): added plugin logo to navbar on plugin page, #4452 --- public/app/core/components/navbar/navbar.html | 3 ++- public/app/core/components/navbar/navbar.ts | 1 + .../app/features/panel/metrics_panel_ctrl.ts | 10 ++++++++-- .../plugins/partials/plugin_page.html | 2 +- .../app/features/plugins/plugin_page_ctrl.ts | 14 ++++++++++++++ .../plugins/datasource/graphite/datasource.ts | 2 +- .../graphite/partials/query.options.html | 19 +++++++++---------- public/sass/components/_navbar.scss | 5 +++++ 8 files changed, 41 insertions(+), 15 deletions(-) diff --git a/public/app/core/components/navbar/navbar.html b/public/app/core/components/navbar/navbar.html index 8c050174201..af6534dd63d 100644 --- a/public/app/core/components/navbar/navbar.html +++ b/public/app/core/components/navbar/navbar.html @@ -9,7 +9,8 @@ - + + {{ctrl.title}} diff --git a/public/app/core/components/navbar/navbar.ts b/public/app/core/components/navbar/navbar.ts index baca7721fe8..e815adf84f6 100644 --- a/public/app/core/components/navbar/navbar.ts +++ b/public/app/core/components/navbar/navbar.ts @@ -22,6 +22,7 @@ export function navbarDirective() { scope: { title: "@", titleUrl: "@", + iconUrl: "@", }, link: function(scope, elem, attrs, ctrl) { ctrl.icon = attrs.icon; diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 8415172e3d1..c0c8bc82126 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -62,7 +62,13 @@ class MetricsPanelCtrl extends PanelCtrl { // if we have snapshot data use that if (this.panel.snapshotData) { this.updateTimeRange(); - this.events.emit('data-snapshot-load', this.panel.snapshotData); + var data = this.panel.snapshotData; + // backward compatability + if (!_.isArray(data)) { + data = data; + } + + this.events.emit('data-snapshot-load', data); return; } @@ -191,7 +197,7 @@ class MetricsPanelCtrl extends PanelCtrl { } if (this.dashboard.snapshot) { - this.panel.snapshotData = result; + this.panel.snapshotData = result.data; } return this.events.emit('data-received', result.data); diff --git a/public/app/features/plugins/partials/plugin_page.html b/public/app/features/plugins/partials/plugin_page.html index 5aadf6db86b..b08089ebb1d 100644 --- a/public/app/features/plugins/partials/plugin_page.html +++ b/public/app/features/plugins/partials/plugin_page.html @@ -1,4 +1,4 @@ - +
    diff --git a/public/app/features/plugins/plugin_page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts index f9150702de6..c21cb4fdad7 100644 --- a/public/app/features/plugins/plugin_page_ctrl.ts +++ b/public/app/features/plugins/plugin_page_ctrl.ts @@ -3,18 +3,32 @@ import angular from 'angular'; import _ from 'lodash'; +var pluginInfoCache = {}; + export class AppPageCtrl { page: any; pluginId: any; appModel: any; + appLogoUrl: any; /** @ngInject */ constructor(private backendSrv, private $routeParams: any, private $rootScope) { this.pluginId = $routeParams.pluginId; + if (pluginInfoCache[this.pluginId]) { + this.appModel = pluginInfoCache[this.pluginId]; + } else { + this.loadPluginInfo(); + } + } + + loadPluginInfo() { this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(app => { this.appModel = app; this.page = _.findWhere(app.includes, {slug: this.$routeParams.slug}); + this.appLogoUrl = app.info.logos.small; + + pluginInfoCache[this.pluginId] = app; if (!this.page) { this.$rootScope.appEvent('alert-error', ['App Page Not Found', '']); diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 615f0e9eee6..515cd490937 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -27,7 +27,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv var params = this.buildGraphiteParams(graphOptions, options.scopedVars); if (params.length === 0) { - return $q.when([]); + return $q.when({data: []}); } if (options.format === 'png') { diff --git a/public/app/plugins/datasource/graphite/partials/query.options.html b/public/app/plugins/datasource/graphite/partials/query.options.html index b00028d0e05..05aecf4b44a 100644 --- a/public/app/plugins/datasource/graphite/partials/query.options.html +++ b/public/app/plugins/datasource/graphite/partials/query.options.html @@ -1,10 +1,9 @@
    -
    - - +
    + + Cache timeout - Cache timeout
    -
    - Max data points +
    + Max data points - shorter legend names + Shorter legend names - series as parameters + Series as parameters - stacking + Stacking - templating + Templating diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 1034cb59958..7ac46505f1b 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -131,6 +131,11 @@ font-size: 20px; line-height: 8px; } + + > img { + max-width: 27px; + max-height: 27px; + } } .sidemenu-pinned { From 35c853e9062097254623aeea3166fb1b8eadc462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 16:43:32 +0100 Subject: [PATCH 073/268] feat(plugins): fix for plugin logo to navbar on plugin page, #4452 --- .../app/features/plugins/plugin_page_ctrl.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/public/app/features/plugins/plugin_page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts index c21cb4fdad7..298587ec0d9 100644 --- a/public/app/features/plugins/plugin_page_ctrl.ts +++ b/public/app/features/plugins/plugin_page_ctrl.ts @@ -16,23 +16,27 @@ export class AppPageCtrl { this.pluginId = $routeParams.pluginId; if (pluginInfoCache[this.pluginId]) { - this.appModel = pluginInfoCache[this.pluginId]; + this.initPage(pluginInfoCache[this.pluginId]); } else { this.loadPluginInfo(); } } + initPage(app) { + this.appModel = app; + this.page = _.findWhere(app.includes, {slug: this.$routeParams.slug}); + this.appLogoUrl = app.info.logos.small; + + pluginInfoCache[this.pluginId] = app; + + if (!this.page) { + this.$rootScope.appEvent('alert-error', ['App Page Not Found', '']); + } + } + loadPluginInfo() { this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(app => { - this.appModel = app; - this.page = _.findWhere(app.includes, {slug: this.$routeParams.slug}); - this.appLogoUrl = app.info.logos.small; - - pluginInfoCache[this.pluginId] = app; - - if (!this.page) { - this.$rootScope.appEvent('alert-error', ['App Page Not Found', '']); - } + this.initPage(app); }); } } From 32a1d1445cbd8350bc43e1ebe887b799cb156255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 23 Mar 2016 17:39:10 +0100 Subject: [PATCH 074/268] refactor(): trying to move app events away from rootScope --- public/app/core/app_events.ts | 6 ++ public/app/core/core.ts | 5 +- public/app/core/services/alert_srv.js | 91 ------------------ public/app/core/services/alert_srv.ts | 99 ++++++++++++++++++++ public/app/core/utils/emitter.ts | 1 + public/app/features/plugins/ds_edit_ctrl.ts | 5 +- public/test/specs/app_specs.ts | 14 +++ public/vendor/angular-other/angular-strap.js | 72 +------------- 8 files changed, 129 insertions(+), 164 deletions(-) create mode 100644 public/app/core/app_events.ts delete mode 100644 public/app/core/services/alert_srv.js create mode 100644 public/app/core/services/alert_srv.ts create mode 100644 public/test/specs/app_specs.ts diff --git a/public/app/core/app_events.ts b/public/app/core/app_events.ts new file mode 100644 index 00000000000..4507246d2be --- /dev/null +++ b/public/app/core/app_events.ts @@ -0,0 +1,6 @@ +/// + +import {Emitter} from './utils/emitter'; + +var appEvents = new Emitter(); +export default appEvents; diff --git a/public/app/core/core.ts b/public/app/core/core.ts index e97c7179afe..5b220d6a1a4 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -36,6 +36,8 @@ import 'app/core/services/all'; import 'app/core/routes/routes'; import './filters/filters'; import coreModule from './core_module'; +import appEvents from './app_events'; + export { arrayJoin, @@ -48,5 +50,6 @@ export { liveSrv, layoutSelector, infoPopover, - Emitter + Emitter, + appEvents, }; diff --git a/public/app/core/services/alert_srv.js b/public/app/core/services/alert_srv.js deleted file mode 100644 index 70965d2947f..00000000000 --- a/public/app/core/services/alert_srv.js +++ /dev/null @@ -1,91 +0,0 @@ -define([ - 'angular', - 'lodash', - '../core_module', -], -function (angular, _, coreModule) { - 'use strict'; - - coreModule.default.service('alertSrv', function($timeout, $sce, $rootScope, $modal, $q) { - var self = this; - - this.init = function() { - $rootScope.onAppEvent('alert-error', function(e, alert) { - self.set(alert[0], alert[1], 'error'); - }, $rootScope); - $rootScope.onAppEvent('alert-warning', function(e, alert) { - self.set(alert[0], alert[1], 'warning', 5000); - }, $rootScope); - $rootScope.onAppEvent('alert-success', function(e, alert) { - self.set(alert[0], alert[1], 'success', 3000); - }, $rootScope); - $rootScope.onAppEvent('confirm-modal', this.showConfirmModal, $rootScope); - }; - - // List of all alert objects - this.list = []; - - this.set = function(title,text,severity,timeout) { - var newAlert = { - title: title || '', - text: text || '', - severity: severity || 'info', - }; - - var newAlertJson = angular.toJson(newAlert); - - // remove same alert if it already exists - _.remove(self.list, function(value) { - return angular.toJson(value) === newAlertJson; - }); - - self.list.push(newAlert); - if (timeout > 0) { - $timeout(function() { - self.list = _.without(self.list,newAlert); - }, timeout); - } - - if (!$rootScope.$$phase) { - $rootScope.$digest(); - } - - return(newAlert); - }; - - this.clear = function(alert) { - self.list = _.without(self.list,alert); - }; - - this.clearAll = function() { - self.list = []; - }; - - this.showConfirmModal = function(e, payload) { - var scope = $rootScope.$new(); - - scope.title = payload.title; - scope.text = payload.text; - scope.text2 = payload.text2; - scope.onConfirm = payload.onConfirm; - scope.icon = payload.icon || "fa-check"; - scope.yesText = payload.yesText || "Yes"; - scope.noText = payload.noText || "Cancel"; - - var confirmModal = $modal({ - template: 'public/app/partials/confirm_modal.html', - persist: false, - modalClass: 'confirm-modal', - show: false, - scope: scope, - keyboard: false - }); - - $q.when(confirmModal).then(function(modalEl) { - modalEl.modal('show'); - }); - - }; - - }); -}); diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts new file mode 100644 index 00000000000..971743b7285 --- /dev/null +++ b/public/app/core/services/alert_srv.ts @@ -0,0 +1,99 @@ +/// + +import angular from 'angular'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; + +export class AlertSrv { + list: any[]; + + /** @ngInject */ + constructor(private $timeout, private $sce, private $rootScope, private $modal) { + this.list = []; + } + + init() { + this.$rootScope.onAppEvent('alert-error', (e, alert) => { + this.set(alert[0], alert[1], 'error', 0); + }, this.$rootScope); + + this.$rootScope.onAppEvent('alert-warning', (e, alert) => { + this.set(alert[0], alert[1], 'warning', 5000); + }, this.$rootScope); + + this.$rootScope.onAppEvent('alert-success', (e, alert) => { + this.set(alert[0], alert[1], 'success', 3000); + }, this.$rootScope); + + appEvents.on('confirm-modal', this.showConfirmModal.bind(this)); + + this.$rootScope.onAppEvent('confirm-modal', (e, data) => { + this.showConfirmModal(data); + }, this.$rootScope); + } + + set(title, text, severity, timeout) { + var newAlert = { + title: title || '', + text: text || '', + severity: severity || 'info', + }; + + var newAlertJson = angular.toJson(newAlert); + + // remove same alert if it already exists + _.remove(this.list, function(value) { + return angular.toJson(value) === newAlertJson; + }); + + this.list.push(newAlert); + if (timeout > 0) { + this.$timeout(() => { + this.list = _.without(this.list, newAlert); + }, timeout); + } + + if (!this.$rootScope.$$phase) { + this.$rootScope.$digest(); + } + + return(newAlert); + } + + clear(alert) { + this.list = _.without(this.list, alert); + } + + clearAll() { + this.list = []; + } + + showConfirmModal(payload) { + var scope = this.$rootScope.$new(); + + scope.title = payload.title; + scope.text = payload.text; + scope.text2 = payload.text2; + scope.onConfirm = payload.onConfirm; + scope.icon = payload.icon || "fa-check"; + scope.yesText = payload.yesText || "Yes"; + scope.noText = payload.noText || "Cancel"; + + var confirmModal = this.$modal({ + template: 'public/app/partials/confirm_modal.html', + persist: false, + modalClass: 'confirm-modal', + show: false, + scope: scope, + keyboard: false + }); + + confirmModal.then(function(modalEl) { + modalEl.modal('show'); + }); + } +} + +coreModule.service('alertSrv', AlertSrv); diff --git a/public/app/core/utils/emitter.ts b/public/app/core/utils/emitter.ts index 6195f7c5e4b..8a38adcc625 100644 --- a/public/app/core/utils/emitter.ts +++ b/public/app/core/utils/emitter.ts @@ -1,3 +1,4 @@ +/// import {Subject} from 'vendor/npm/rxjs/Subject'; diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index 57e43983b8c..b76407a90e8 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -2,8 +2,9 @@ import angular from 'angular'; import _ from 'lodash'; -import coreModule from 'app/core/core_module'; + import config from 'app/core/config'; +import {coreModule, appEvents} from 'app/core/core'; var datasourceTypes = []; @@ -142,7 +143,7 @@ export class DataSourceEditCtrl { } delete(s) { - this.$scope.appEvent('confirm-modal', { + appEvents.emit('confirm-modal', { title: 'Delete', text: 'Are you sure you want to delete this datasource?', yesText: "Delete", diff --git a/public/test/specs/app_specs.ts b/public/test/specs/app_specs.ts new file mode 100644 index 00000000000..3c57f2d625a --- /dev/null +++ b/public/test/specs/app_specs.ts @@ -0,0 +1,14 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import {GrafanaApp} from 'app/app'; + +describe('GrafanaApp', () => { + + var app = new GrafanaApp(); + + it('can call inits', () => { + expect(app).to.not.be(null); + }); +}); + + diff --git a/public/vendor/angular-other/angular-strap.js b/public/vendor/angular-other/angular-strap.js index 204da8a728b..d9721bda038 100644 --- a/public/vendor/angular-other/angular-strap.js +++ b/public/vendor/angular-other/angular-strap.js @@ -79,76 +79,7 @@ angular.module('$strap.directives').factory('$modal', [ return ModalFactory; } ]) -'use strict'; -angular.module('$strap.directives').directive('bsTabs', [ - '$parse', - '$compile', - '$timeout', - function ($parse, $compile, $timeout) { - var template = '
    ' + '' + '
    ' + '
    '; - return { - restrict: 'A', - require: '?ngModel', - priority: 0, - scope: true, - template: template, - replace: true, - transclude: true, - compile: function compile(tElement, tAttrs, transclude) { - return function postLink(scope, iElement, iAttrs, controller) { - var getter = $parse(iAttrs.bsTabs), setter = getter.assign, value = getter(scope); - scope.panes = []; - var $tabs = iElement.find('ul.nav-tabs'); - var $panes = iElement.find('div.tab-content'); - var activeTab = 0, id, title, active; - $timeout(function () { - $panes.find('[data-title], [data-tab]').each(function (index) { - var $this = angular.element(this); - id = 'tab-' + scope.$id + '-' + index; - title = $this.data('title') || $this.data('tab'); - active = !active && $this.hasClass('active'); - $this.attr('id', id).addClass('tab-pane'); - if (iAttrs.fade) - $this.addClass('fade'); - scope.panes.push({ - id: id, - title: title, - content: this.innerHTML, - active: active - }); - }); - if (scope.panes.length && !active) { - $panes.find('.tab-pane:first-child').addClass('active' + (iAttrs.fade ? ' in' : '')); - scope.panes[0].active = true; - } - }); - if (controller) { - iElement.on('show', function (ev) { - var $target = $(ev.target); - scope.$apply(function () { - controller.$setViewValue($target.data('index')); - }); - }); - scope.$watch(iAttrs.ngModel, function (newValue, oldValue) { - if (angular.isUndefined(newValue)) - return; - activeTab = newValue; - setTimeout(function () { - // Check if we're still on the same tab before making the switch - if(activeTab === newValue) { - var $next = $($tabs[0].querySelectorAll('li')[newValue * 1]); - if (!$next.hasClass('active')) { - $next.children('a').tab('show'); - } - } - }); - }); - } - }; - } - }; - } -]); + 'use strict'; angular.module('$strap.directives').directive('bsTooltip', [ '$parse', @@ -202,6 +133,7 @@ angular.module('$strap.directives').directive('bsTooltip', [ }; } ]); + 'use strict'; angular.module('$strap.directives').directive('bsTypeahead', [ '$parse', 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 075/268] 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 076/268] 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 077/268] 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 078/268] 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 f85a3ad44df7f675dcbe96b428199d59989c77be Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 24 Mar 2016 11:29:11 +0100 Subject: [PATCH 079/268] docs(plugins): minor improvesments to plugin dev guide --- docs/sources/plugins/developing_plugins.md | 32 ++++++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/sources/plugins/developing_plugins.md b/docs/sources/plugins/developing_plugins.md index 91d1ff3f84e..c78bd45c0e4 100644 --- a/docs/sources/plugins/developing_plugins.md +++ b/docs/sources/plugins/developing_plugins.md @@ -8,21 +8,35 @@ page_keywords: grafana, plugins, documentation, development From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users. +## Short version + +1. [Setup grafana](https://github.com/grafana/grafana/blob/master/DEVELOPMENT.md) +2. Clone an example plugin into ```data/plugins``` +3. Code away! + ## What languages? -Since everything turns into javascript its up to you to choose which language you want. That said its proberbly a good idea to choose es6 or typescript since we use es6 classes in Grafana. +Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages. -##Buildscript +## Buildscript -You can use any buildsystem you like that support systemjs. All the built content should endup in a folder named dist and commited to the repository. +You can use any build system you like that support systemjs. All the built content should end up in a folder named ```dist``` and committed to the repository.By committing the dist folder the person who installs your plugin does not have to run any buildscript. -##Loading plugins -The easiset way to try your plugin with grafana is to [setup grafana for development](https://github.com/grafana/grafana/blob/master/DEVELOPMENT.md) and place your plugin in the /data/plugins folder in grafana. When grafana starts it will scan that folder for folders that contains a plugin.json file and mount them as plugins. If your plugin folder contains a folder named dist it will mount that folder instead of the plugin base folder. +All our example plugins have build scripted configured. -## Examples / boilerplate -We currently have three different examples that you can fork to get started developing your grafana plugin. +## 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. +2. Install Grafana and place your plugin the plugins directory which is set in your [config file](http://docs.grafana.org/installation/configuration/) + +We encourage people to setup the full Grafana environment so that you can get inspiration from the rest of grafana code base. + +When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless the folder contains a subfolder named dist. In that case grafana will mount the dist folder instead. +This makes it possible to have both built and src content in the same plugin folder. + +## 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) - - [panel-boilderplate-es5](https://github.com/grafana/grafana/tree/master/examples/panel-boilerplate-es5) + - [piechart-panel](https://github.com/grafana/piechart-panel) - [example-app](https://github.com/grafana/example-app) - From a531a1709eaa5904f4ec9acac6f387a1d888f5c8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 24 Mar 2016 11:55:36 +0100 Subject: [PATCH 080/268] docs(plugin): add apps page --- docs/mkdocs.yml | 1 + docs/sources/plugins/developing_plugins.md | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index b527fa8f046..1ecefe85c68 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -88,6 +88,7 @@ pages: - ['plugins/overview.md', 'Plugins', 'Overview'] - ['plugins/installation.md', 'Plugins', 'Installation'] +- ['plugins/app.md', 'Plugins', 'App plugins'] - ['plugins/datasources.md', 'Plugins', 'Datasource plugins'] - ['plugins/panels.md', 'Plugins', 'Panel plugins'] - ['plugins/development.md', 'Plugins', 'Plugin development'] diff --git a/docs/sources/plugins/developing_plugins.md b/docs/sources/plugins/developing_plugins.md index c78bd45c0e4..def5b825df9 100644 --- a/docs/sources/plugins/developing_plugins.md +++ b/docs/sources/plugins/developing_plugins.md @@ -24,6 +24,11 @@ You can use any build system you like that support systemjs. All the built conte All our example plugins have build scripted configured. +## module.(js|ts) + +This is the entry point for every plugin. This is the place where you should export your plugin implementation. Depending on what kind of plugin you are developing you will be expected to export different things. You can find whats expected for [datasource](http://docs.grafana.org/v3.0/plugins/datasources/), [panels](http://docs.grafana.org/v3.0/plugins/panels/) and [apps](http://docs.grafana.org/v3.0/plugins/app/) +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. 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 081/268] 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 694c6e8fd2e4e54c29db315d68738b1b50d6edb7 Mon Sep 17 00:00:00 2001 From: Trent White Date: Thu, 24 Mar 2016 16:14:51 -0400 Subject: [PATCH 082/268] added new icons for default panels --- .../panel/dashlist/img/icn-dashlist-panel.svg | 119 +++++++++++ public/app/plugins/panel/dashlist/plugin.json | 4 + .../panel/graph/img/icn-graph-panel.svg | 186 ++++++++++++++++++ public/app/plugins/panel/graph/plugin.json | 5 + .../singlestat/img/icn-singlestat-panel.svg | 33 ++++ .../app/plugins/panel/singlestat/plugin.json | 5 + .../panel/table/img/icn-table-panel.svg | 67 +++++++ public/app/plugins/panel/table/plugin.json | 5 + .../plugins/panel/text/img/icn-text-panel.svg | 26 +++ public/app/plugins/panel/text/plugin.json | 5 + 10 files changed, 455 insertions(+) create mode 100644 public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg create mode 100644 public/app/plugins/panel/graph/img/icn-graph-panel.svg create mode 100644 public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg create mode 100644 public/app/plugins/panel/table/img/icn-table-panel.svg create mode 100644 public/app/plugins/panel/text/img/icn-text-panel.svg diff --git a/public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg b/public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg new file mode 100644 index 00000000000..f584770dff0 --- /dev/null +++ b/public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/dashlist/plugin.json b/public/app/plugins/panel/dashlist/plugin.json index 9cac424ac68..d46622e5e27 100644 --- a/public/app/plugins/panel/dashlist/plugin.json +++ b/public/app/plugins/panel/dashlist/plugin.json @@ -7,6 +7,10 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-dashlist-panel.svg", + "large": "img/icn-dashlist-panel.svg" } } } diff --git a/public/app/plugins/panel/graph/img/icn-graph-panel.svg b/public/app/plugins/panel/graph/img/icn-graph-panel.svg new file mode 100644 index 00000000000..3fd6e2bf02e --- /dev/null +++ b/public/app/plugins/panel/graph/img/icn-graph-panel.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/graph/plugin.json b/public/app/plugins/panel/graph/plugin.json index baa9fe12a39..b2976c27e04 100644 --- a/public/app/plugins/panel/graph/plugin.json +++ b/public/app/plugins/panel/graph/plugin.json @@ -7,6 +7,11 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-graph-panel.svg", + "large": "img/icn-graph-panel.svg" } } } + diff --git a/public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg b/public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg new file mode 100644 index 00000000000..a1e15d4d58d --- /dev/null +++ b/public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/singlestat/plugin.json b/public/app/plugins/panel/singlestat/plugin.json index ea9ae32853a..197cf3ec27d 100644 --- a/public/app/plugins/panel/singlestat/plugin.json +++ b/public/app/plugins/panel/singlestat/plugin.json @@ -7,6 +7,11 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-singlestat-panel.svg", + "large": "img/icn-singlestat-panel.svg" } } } + diff --git a/public/app/plugins/panel/table/img/icn-table-panel.svg b/public/app/plugins/panel/table/img/icn-table-panel.svg new file mode 100644 index 00000000000..83097e259dc --- /dev/null +++ b/public/app/plugins/panel/table/img/icn-table-panel.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/table/plugin.json b/public/app/plugins/panel/table/plugin.json index e25efdfbe36..84a527db565 100644 --- a/public/app/plugins/panel/table/plugin.json +++ b/public/app/plugins/panel/table/plugin.json @@ -7,6 +7,11 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-table-panel.svg", + "large": "img/icn-table-panel.svg" } } } + diff --git a/public/app/plugins/panel/text/img/icn-text-panel.svg b/public/app/plugins/panel/text/img/icn-text-panel.svg new file mode 100644 index 00000000000..4274cd6c35c --- /dev/null +++ b/public/app/plugins/panel/text/img/icn-text-panel.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/text/plugin.json b/public/app/plugins/panel/text/plugin.json index 485f42942f2..e63974c6167 100644 --- a/public/app/plugins/panel/text/plugin.json +++ b/public/app/plugins/panel/text/plugin.json @@ -7,6 +7,11 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-text-panel.svg", + "large": "img/icn-text-panel.svg" } } } + From 82b1a118202f5b88797e0c3163d9ad5c952efba4 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 24 Mar 2016 22:01:33 -0400 Subject: [PATCH 083/268] Added a new util class for highlight words, proper variables added for light and dark --- public/sass/_variables.dark.scss | 14 ++++++++------ public/sass/_variables.light.scss | 13 +++++++------ public/sass/utils/_utils.scss | 5 +++++ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 84b01692069..26dabd4336f 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -48,12 +48,14 @@ $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-emphasis: $gray-5; + // 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 279e284b0be..69584df580a 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -54,12 +54,13 @@ $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 $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%); diff --git a/public/sass/utils/_utils.scss b/public/sass/utils/_utils.scss index efccb434d48..b97f1aee50d 100644 --- a/public/sass/utils/_utils.scss +++ b/public/sass/utils/_utils.scss @@ -6,6 +6,11 @@ color: $brand-primary; } +.emphasis-word { + font-weight: 500; + color: $text-color-emphasis; +} + // Close icons // -------------------------------------------------- .close { From 963e202745ad6e3ff5770e5d665299c70420d12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 25 Mar 2016 09:40:05 +0100 Subject: [PATCH 084/268] fix(): data push fix --- public/app/features/panel/metrics_panel_ctrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index c0c8bc82126..52e9c64981c 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -217,7 +217,7 @@ class MetricsPanelCtrl extends PanelCtrl { if (data.range) { this.range = data.range; } - this.events.emit('data-received', data); + this.events.emit('data-received', data.data); }, error: (error) => { this.events.emit('data-error', error); From 05cc370c559be7cfb0cb1c9e396f55f568987033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 25 Mar 2016 13:43:13 +0100 Subject: [PATCH 085/268] feat(plugins): added disable button to app config tab --- public/app/features/plugins/partials/plugin_edit.html | 1 + public/app/features/plugins/plugin_edit_ctrl.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/public/app/features/plugins/partials/plugin_edit.html b/public/app/features/plugins/partials/plugin_edit.html index 20b129b94e9..6cb0ae5bf78 100644 --- a/public/app/features/plugins/partials/plugin_edit.html +++ b/public/app/features/plugins/partials/plugin_edit.html @@ -42,6 +42,7 @@
    +
    diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index 8e9dc32eeee..ae6f9b166aa 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -122,6 +122,11 @@ export class PluginEditCtrl { this.update(); } + disable() { + this.model.enabled = false; + this.model.pinned = false; + this.update(); + } } angular.module('grafana.controllers').controller('PluginEditCtrl', PluginEditCtrl); From 61017fc204d62519a92e1393799b7fdfd00c76c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 25 Mar 2016 15:35:58 +0100 Subject: [PATCH 086/268] feat(plugins): import all dashboards feature --- public/app/core/utils/emitter.ts | 12 ++- .../plugins/import_list/import_list.ts | 29 +++++++- .../app/features/plugins/plugin_edit_ctrl.ts | 74 +++++++++++-------- 3 files changed, 80 insertions(+), 35 deletions(-) diff --git a/public/app/core/utils/emitter.ts b/public/app/core/utils/emitter.ts index 8a38adcc625..045182c76d1 100644 --- a/public/app/core/utils/emitter.ts +++ b/public/app/core/utils/emitter.ts @@ -21,10 +21,18 @@ export class Emitter { this.subjects[fnName].next(data); } - on(name, handler) { + on(name, handler, $scope) { var fnName = createName(name); this.subjects[fnName] || (this.subjects[fnName] = new Subject()); - this.subjects[fnName].subscribe(handler); + var subscription = this.subjects[fnName].subscribe(handler); + + if ($scope) { + $scope.$on('$destroy', function() { + subscription.unsubscribe(); + }); + } + + return subscription; }; off(name, handler) { diff --git a/public/app/features/plugins/import_list/import_list.ts b/public/app/features/plugins/import_list/import_list.ts index dd0a8eef524..8eb726b81c0 100644 --- a/public/app/features/plugins/import_list/import_list.ts +++ b/public/app/features/plugins/import_list/import_list.ts @@ -3,18 +3,43 @@ import angular from 'angular'; import _ from 'lodash'; import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; export class DashImportListCtrl { dashboards: any[]; plugin: any; datasource: any; - constructor(private $http, private backendSrv, private $rootScope) { + constructor($scope, private $http, private backendSrv, private $rootScope) { this.dashboards = []; backendSrv.get(`/api/plugins/${this.plugin.id}/dashboards`).then(dashboards => { this.dashboards = dashboards; }); + + appEvents.on('dashboard-list-import-all', this.importAll.bind(this), $scope); + } + + importAll(payload) { + return this.importNext(0).then(() => { + payload.resolve("All dashboards imported"); + }).catch(err => { + payload.reject(err); + }); + } + + importNext(index) { + return this.import(this.dashboards[index], true).then(() => { + if (index+1 < this.dashboards.length) { + return new Promise(resolve => { + setTimeout(() => { + this.importNext(index+1).then(() => { + resolve(); + }); + }, 500); + }); + } + }); } import(dash, reinstall) { @@ -34,7 +59,7 @@ export class DashImportListCtrl { }); } - this.backendSrv.post(`/api/dashboards/import`, installCmd).then(res => { + return this.backendSrv.post(`/api/dashboards/import`, installCmd).then(res => { this.$rootScope.appEvent('alert-success', ['Dashboard Installed', dash.title]); _.extend(dash, res); }); diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index ae6f9b166aa..7f3f6bff08f 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -2,6 +2,7 @@ import angular from 'angular'; import _ from 'lodash'; +import appEvents from 'app/core/app_events'; export class PluginEditCtrl { model: any; @@ -17,11 +18,18 @@ export class PluginEditCtrl { postUpdateHook: () => any; /** @ngInject */ - constructor(private backendSrv, private $routeParams, private $sce, private $http) { + constructor(private $scope, + private backendSrv, + private $routeParams, + private $sce, + private $http) { this.model = {}; this.pluginId = $routeParams.pluginId; this.tabIndex = 0; this.tabs = ['Overview']; + + this.preUpdateHook = () => Promise.resolve(); + this.postUpdateHook = () => Promise.resolve(); } init() { @@ -71,43 +79,47 @@ export class PluginEditCtrl { } update() { - var chain = Promise.resolve(); - var self = this; - // if set, handle the preUpdateHook. If this returns a promise, - // the next step of execution will block until the promise resolves. - // if the promise is rejected, this update will be aborted. - if (this.preUpdateHook != null) { - chain = self.preUpdateHook(); - } - - // Perform the core update procedure - chain = chain.then(function() { + this.preUpdateHook().then(() => { var updateCmd = _.extend({ - enabled: self.model.enabled, - pinned: self.model.pinned, - jsonData: self.model.jsonData, - secureJsonData: self.model.secureJsonData, + enabled: this.model.enabled, + pinned: this.model.pinned, + jsonData: this.model.jsonData, + secureJsonData: this.model.secureJsonData, }, {}); - return self.backendSrv.post(`/api/plugins/${self.pluginId}/settings`, updateCmd); - }); - - // if set, performt he postUpdate hook. If a promise is returned it will block - // the final step of the update procedure (reloading the page) until the promise - // resolves. If the promise is rejected the page will not be reloaded. - if (this.postUpdateHook != null) { - chain = chain.then(function() { - return this.postUpdateHook(); - }); - } - - // all stesp in the update procedure are complete, so reload the page to make changes - // take effect. - chain.then(function() { + return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd); + }) + .then(this.postUpdateHook) + .then((res) => { window.location.href = window.location.href; }); } + importDashboards() { + // move to dashboards tab + this.tabIndex = 2; + + return new Promise((resolve) => { + if (!this.$scope.$$phase) { + this.$scope.$digest(); + } + + // let angular load dashboards tab + setTimeout(() => { + resolve(); + }, 1000); + + }).then(() => { + return new Promise((resolve, reject) => { + // send event to import list component + appEvents.emit('dashboard-list-import-all', { + resolve: resolve, + reject: reject + }); + }); + }); + } + setPreUpdateHook(callback: () => any) { this.preUpdateHook = callback; } From 319b9349672e1e1353f0c8350800f7a79bd49641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 25 Mar 2016 15:52:28 +0100 Subject: [PATCH 087/268] fix(): fixed failing test --- public/app/core/utils/emitter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/core/utils/emitter.ts b/public/app/core/utils/emitter.ts index 045182c76d1..5de9dc78cfa 100644 --- a/public/app/core/utils/emitter.ts +++ b/public/app/core/utils/emitter.ts @@ -21,13 +21,13 @@ export class Emitter { this.subjects[fnName].next(data); } - on(name, handler, $scope) { + on(name, handler, scope?) { var fnName = createName(name); this.subjects[fnName] || (this.subjects[fnName] = new Subject()); var subscription = this.subjects[fnName].subscribe(handler); - if ($scope) { - $scope.$on('$destroy', function() { + if (scope) { + scope.$on('$destroy', function() { subscription.unsubscribe(); }); } From 70d536c6c470b20f45e86d6ea8c2e3080898abc9 Mon Sep 17 00:00:00 2001 From: Trent White Date: Fri, 25 Mar 2016 11:54:50 -0400 Subject: [PATCH 088/268] 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 089/268] 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 090/268] 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 091/268] 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 092/268] 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 093/268] 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 094/268] 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 095/268] 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 096/268] 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 108/268] 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 109/268] 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 110/268] 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 111/268] 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 112/268] 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 24a7d4f8166186d2d4c70175fa9ec6b7e34fdce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 Mar 2016 17:56:34 +0200 Subject: [PATCH 113/268] ux(): query editors, added group by --- .../influxdb/partials/query.editor.html | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 7942952f48f..01b541cee72 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -1,6 +1,6 @@
    - + @@ -22,7 +22,7 @@
    - +
    +
    +
    +
    + + + + + + +
    @@ -75,7 +92,7 @@
    - +
    From d2239427f20bc5adbbcaea98ceb188b0ac5337b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 Mar 2016 19:03:24 +0200 Subject: [PATCH 114/268] ux(): big progress on updating query editors --- .../panel/partials/query_editor_row.html | 5 +- .../influxdb/partials/query.editor.html | 178 ++++++++---------- .../influxdb/partials/query_part.html | 2 +- public/sass/_grafana.scss | 1 + public/sass/components/_gf-form.scss | 38 +++- public/sass/components/_query_part.scss | 16 ++ 6 files changed, 136 insertions(+), 104 deletions(-) create mode 100644 public/sass/components/_query_part.scss diff --git a/public/app/features/panel/partials/query_editor_row.html b/public/app/features/panel/partials/query_editor_row.html index 7fa5bbd113e..75df55a73da 100644 --- a/public/app/features/panel/partials/query_editor_row.html +++ b/public/app/features/panel/partials/query_editor_row.html @@ -1,4 +1,4 @@ -
    +
    -
    + +
    diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 01b541cee72..53588f1f51a 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -1,105 +1,89 @@ -
    - - - +
    + +
    - +
    +
    +
    + -
    - + + +
    + +
    + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    + + +
    + +
    + +
    + +
    +
    + +
    +
    + + + + + + +
    + +
    +
    + +
    + +
    +
    + + +
    +
    + +
    +
    +
    +
    -
    - -
    - -
    -
    -
    - - - - - - -
    -
    -
    -
    - - - - - - - -
    -
    - -
    -
    -
      -
    • - SELECT -
    • -
    • - -
    • - -
    -
    -
    - -
    -
      -
    • - GROUP BY -
    • -
    • - -
    • -
    • - -
    • -
    -
    -
    -
    - -
    -
    - - -
    -
    - -
    - -
    -
    -
    - diff --git a/public/app/plugins/datasource/influxdb/partials/query_part.html b/public/app/plugins/datasource/influxdb/partials/query_part.html index 0eb0146ec13..478edfe5c29 100644 --- a/public/app/plugins/datasource/influxdb/partials/query_part.html +++ b/public/app/plugins/datasource/influxdb/partials/query_part.html @@ -2,4 +2,4 @@
    -{{part.def.type}}() +{{part.def.type}}() diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index c7b4567bfb5..9da4ed0dfbc 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -68,6 +68,7 @@ @import "components/drop"; @import "components/query_editor"; @import "components/tabbed_view"; +@import "components/query_part"; // PAGES @import "pages/login"; diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 468754f82da..3fae08f6ad8 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -49,6 +49,16 @@ $gf-form-margin: 0.25rem; @include border-radius($label-border-radius-sm); } +.gf-form-filler { + margin-right: $gf-form-margin; + margin-bottom: $gf-form-margin; + flex-grow: 1; + background-color: $input-label-bg; + + border: $input-btn-border-width solid transparent; + @include border-radius($label-border-radius-sm); +} + .gf-form-checkbox { flex-shrink: 0; padding: $input-padding-y $input-padding-x; @@ -152,10 +162,6 @@ $gf-form-margin: 0.25rem; flex-grow: 0; } -.query-editor-secondary-row { - margin-left: 5.2rem; -} - .gf-form-switch { margin-right: $gf-form-margin; } @@ -186,3 +192,27 @@ $gf-form-margin: 0.25rem; pointer-events: none; } } + +.gf-form-query { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-content: flex-start; + align-items: flex-start; + + .gf-form, + .gf-form-filler { + margin-bottom: 2px; + } + + .gf-form-input, + .gf-form-select-wrapper, + .gf-form-filler, + .gf-form-label { + margin-right: 2px; + } +} + +.gf-form-query-content { + flex-grow: 1; +} diff --git a/public/sass/components/_query_part.scss b/public/sass/components/_query_part.scss new file mode 100644 index 00000000000..a2543e1a75c --- /dev/null +++ b/public/sass/components/_query_part.scss @@ -0,0 +1,16 @@ + +.query-part { + background-color: $input-bg !important; + + &.show-function-controls { + padding-top: 5px; + min-width: 100px; + text-align: center; + } +} + +.query-part-name { +} + +.query-part-parameters { +} From 50d773988bdf0cfec3ecea03094b052fdb969014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 Mar 2016 19:17:32 +0200 Subject: [PATCH 115/268] ux(): updated graphite editor --- .../datasource/graphite/add_graphite_func.js | 4 +-- .../graphite/partials/query.editor.html | 32 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js index 4547526d08f..bb4558b154b 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ b/public/app/plugins/datasource/graphite/add_graphite_func.js @@ -11,10 +11,10 @@ function (angular, _, $, gfunc) { .module('grafana.directives') .directive('graphiteAddFunc', function($compile) { var inputTemplate = ''; - var buttonTemplate = '' + ''; diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index 90dfe0794c0..6cbe13680cf 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -1,21 +1,25 @@ -
  • - -
  • +
    + +
    -
  • +
    +
    + -
  • - -
  • -
  • - - -
  • - +
    + +
    -
  • + + +
    +
    +
    From aa62c2eff63e830eb0f383cabe8ba37e0e439b6c Mon Sep 17 00:00:00 2001 From: Sebastian Borza Date: Tue, 29 Mar 2016 12:36:21 -0500 Subject: [PATCH 116/268] 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 117/268] 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 118/268] 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 128/268] 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 129/268] 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 130/268] 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 131/268] 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 132/268] 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 133/268] 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 134/268] 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 135/268] 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 136/268] 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 137/268] 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 | 17 ++- 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, 218 insertions(+), 108 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 UpdateUserPrefsCmd struct { +type Prefs struct { + Theme string `json:"theme"` + HomeDashboardId int64 `json:"homeDashboardId"` + Timezone string `json:"timezone"` +} + +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 138/268] 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 139/268] 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 140/268] 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 141/268] 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 142/268] 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 143/268] 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 144/268] 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 145/268] 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 146/268] 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 147/268] 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 148/268] 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 149/268] 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 150/268] 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 151/268] 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 152/268] 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 153/268] 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 154/268] 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 155/268] 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 156/268] 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 157/268] 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 158/268] 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 159/268] 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 160/268] 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 161/268] 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 162/268] 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 163/268] 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 @@
    -
    +
    Alias patterns
    diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index a732661106b..94cc795d9eb 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -8,8 +8,8 @@ $gf-form-margin: 0.25rem; text-align: left; position: relative; - .cr1 { - margin-left: 8px; + &--offset-1 { + margin-left: $spacer; } } @@ -162,9 +162,11 @@ $gf-form-margin: 0.25rem; } .gf-form-btn { - margin-right: $gf-form-margin; padding: $input-padding-y $input-padding-x; + margin-right: $gf-form-margin; line-height: $input-line-height; + font-size: $font-size-sm; + flex-shrink: 0; flex-grow: 0; } diff --git a/public/sass/components/_query_part.scss b/public/sass/components/_query_part.scss index a2543e1a75c..1e2fb9622c2 100644 --- a/public/sass/components/_query_part.scss +++ b/public/sass/components/_query_part.scss @@ -9,8 +9,3 @@ } } -.query-part-name { -} - -.query-part-parameters { -} From 805fd18b7b55bbea7996fa8cf3640c127c47a0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 17 Apr 2016 16:43:13 -0400 Subject: [PATCH 264/268] feat(query editors): more work on query editors make over --- .../app/features/panel/metrics_ds_selector.ts | 2 +- .../panel/partials/query_editor_row.html | 16 +- public/app/features/panel/query_ctrl.ts | 36 ----- public/app/features/panel/query_editor_row.ts | 88 +++++++++- .../graphite/partials/query.editor.html | 2 +- .../influxdb/partials/query.editor.html | 151 ++++++++---------- .../plugins/datasource/influxdb/query_ctrl.ts | 5 + 7 files changed, 176 insertions(+), 124 deletions(-) diff --git a/public/app/features/panel/metrics_ds_selector.ts b/public/app/features/panel/metrics_ds_selector.ts index f93ee5ad15a..b5457c21164 100644 --- a/public/app/features/panel/metrics_ds_selector.ts +++ b/public/app/features/panel/metrics_ds_selector.ts @@ -79,7 +79,7 @@ export class MetricsDsSelectorCtrl { } addDataQuery(datasource) { - var target: any = {}; + var target: any = {isNew: true}; if (datasource) { target.datasource = datasource.name; diff --git a/public/app/features/panel/partials/query_editor_row.html b/public/app/features/panel/partials/query_editor_row.html index 722c7c27b6f..f4c1854824c 100644 --- a/public/app/features/panel/partials/query_editor_row.html +++ b/public/app/features/panel/partials/query_editor_row.html @@ -10,14 +10,22 @@
    -
    +
    +
    + +
    +
    + +
    @@ -83,7 +91,9 @@
    -
    +
    +
    +
    diff --git a/public/sass/components/_drop.scss b/public/sass/components/_drop.scss index 4045c1daa4b..570c1862ef0 100644 --- a/public/sass/components/_drop.scss +++ b/public/sass/components/_drop.scss @@ -1,4 +1,4 @@ -$popover-arrow-size: 1rem; +$popover-arrow-size: 0.7rem; $color: inherit; $backgroundColor: $btn-secondary-bg; $color: $text-color; diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 94cc795d9eb..75042f958e7 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -11,6 +11,10 @@ $gf-form-margin: 0.25rem; &--offset-1 { margin-left: $spacer; } + + &--grow { + flex-grow: 1; + } } .gf-form-group { @@ -22,10 +26,6 @@ $gf-form-margin: 0.25rem; flex-direction: row; flex-wrap: wrap; align-content: flex-start; - - .gf-form-flex { - flex-grow: 1; - } } .gf-form-button-row { @@ -48,16 +48,12 @@ $gf-form-margin: 0.25rem; border: $input-btn-border-width solid transparent; @include border-radius($label-border-radius-sm); -} -.gf-form-filler { - margin-right: $gf-form-margin; - margin-bottom: $gf-form-margin; - flex-grow: 1; - background-color: $input-label-bg; - border: $input-btn-border-width solid transparent; - @include border-radius($label-border-radius-sm); + &--grow { + flex-grow: 1; + min-height: 2.7rem; + } } .gf-form-checkbox { diff --git a/public/sass/mixins/_drop_element.scss b/public/sass/mixins/_drop_element.scss index d08e50e63b8..290e49f4cad 100644 --- a/public/sass/mixins/_drop_element.scss +++ b/public/sass/mixins/_drop_element.scss @@ -10,7 +10,7 @@ font-family: inherit; background: $theme-bg; color: $theme-color; - padding: $spacer; + padding: 0.65rem; font-size: $font-size-sm; max-width: 20rem; From 1069f485daf3335822897772d6a1588fd3cdb193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 17 Apr 2016 22:24:40 -0400 Subject: [PATCH 267/268] ux(query_editors): handle text overflow in collapsed mode --- .../features/panel/partials/query_editor_row.html | 2 +- public/sass/components/_gf-form.scss | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/public/app/features/panel/partials/query_editor_row.html b/public/app/features/panel/partials/query_editor_row.html index 6c0b0d69a80..0529a178d28 100644 --- a/public/app/features/panel/partials/query_editor_row.html +++ b/public/app/features/panel/partials/query_editor_row.html @@ -10,7 +10,7 @@
    -
    +
    - - - + + +
    @@ -38,78 +38,49 @@
    -
    -
    - - + +
    + +
    -
    - - +
    + +
    -
    - - +
    + +
    -
    - - +
    + +
    -
    -
      -
    • - {{stat.text}} -
    • -
    • - -
    • -
    -
    + + +
    + +
    -
    -
      -
    • - Sigma -
    • -
    • - -
    • -
    -
    + +
    + +
    -
    -
      -
    • - Script -
    • -
    • - -
    • -
    -
    +
    + +
    - -
    -
      -
    • - Missing - The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value -
    • -
    • - -
    • -
    -
    -
    -
    diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 5aaea7bda7b..d3d5a9a07c9 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -210,6 +210,8 @@ $gf-form-margin: 0.25rem; margin-bottom: 2px; } + .gf-form-switch input, + .gf-form-switch label, .gf-form-input, .gf-form-select-wrapper, .gf-form-filler, @@ -220,7 +222,6 @@ $gf-form-margin: 0.25rem; .gf-form-query-content { flex-grow: 1; - overflow: hidden; &--collapsed { overflow: hidden; diff --git a/public/sass/utils/_widths.scss b/public/sass/utils/_widths.scss index b4bdbdb6ccc..cf324b35c72 100644 --- a/public/sass/utils/_widths.scss +++ b/public/sass/utils/_widths.scss @@ -17,3 +17,9 @@ } } +@for $i from 1 through 30 { + .offset-width-#{$i} { + margin-left: ($spacer * $i) !important; + } +} +