diff --git a/docs/sources/reference/playlist.md b/docs/sources/reference/playlist.md index b87ebc25be0..3e90f7361a3 100644 --- a/docs/sources/reference/playlist.md +++ b/docs/sources/reference/playlist.md @@ -10,27 +10,32 @@ The Playlist is a special type of Dashboard that rotates through a list of Dashb Since Grafana automatically scales Dashboards to any resolution they're perfect for big screens! -## Configuring the Playlist +## Creating a Playlist -The Playlist can be accessed from the main Dashboard picker. Click the 'Playlist' button at the bottom of the picker to access the Playlist functionality. +The Playlist feature can be accessed from Grafana's sidemenu. Click the 'Playlist' button from the sidemenu to access the Playlist functionality. When 'Playlist' button is clicked, playlist view will open up showing saved playlists and an option to create new playlists. -Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here. You can search Dashboards by name (or use a regular expression). +Click on "New Playlist" button to create a new playlist. Firstly, name your playlist and configure a time interval for Grafana to wait on a particular Dashboard before advancing to the next one on the Playlist. You can search Dashboards by name (or use a regular expression), and add them to your Playlist. By default, your starred dashboards will appear as candidates for the Playlist. -Be sure to click the right arrow appearing next to the Dashboard name to add it to the Playlist. +Be sure to click the "Add to dashboard" button next to the Dashboard name to add it to the Playlist. To remove a dashboard from the playlist click on "Remove[x]" button from the playlist. -You can configure a time interval for Grafana to wait on a particular Dashboard before advancing to the next one on the Playlist. +Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here. -## Starting and controlling the Playlist +## Saving the playlist -To start the Playlist, click the green "Start" button +Once all the wanted dashboards are added to a playlist, you can save this playlist by clicking on the green "Save" button. This will generate a unique URL for you playlist which can be shared if needed. Click on the generated URL or on the "Play" button from the "Saved playlists" list to start the playlist. If you want to share the URL, right click on the URL and copy the URL link and share. + +## Starting the playlist + +Also, if you want, you can start the playlist without saving it by clicking on the green "Start" button at the bottom. + +## Controlling the Playlist Playlists can also be manually controlled utilizing the Playlist controls at the top of screen when in Playlist mode. Click the stop button to stop the Playlist, and exit to the current Dashboard. Click the next button to advance to the next Dashboard in the Playlist. Click the back button to rewind to the previous Dashboard in the Playlist. - diff --git a/pkg/api/api.go b/pkg/api/api.go index 839978845ab..2155978fc81 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -47,6 +47,9 @@ func Register(r *macaron.Macaron) { r.Get("/dashboard/*", reqSignedIn, Index) r.Get("/dashboard-solo/*", reqSignedIn, Index) + r.Get("/playlists/", reqSignedIn, Index) + r.Get("/playlists/*", reqSignedIn, Index) + // sign up r.Get("/signup", Index) r.Get("/api/user/signup/options", wrap(GetSignUpOptions)) @@ -169,6 +172,17 @@ func Register(r *macaron.Macaron) { r.Get("/tags", GetDashboardTags) }) + // Playlist + r.Group("/playlists", func() { + r.Get("/", wrap(SearchPlaylists)) + r.Get("/:id", ValidateOrgPlaylist, wrap(GetPlaylist)) + r.Get("/:id/items", ValidateOrgPlaylist, wrap(GetPlaylistItems)) + r.Get("/:id/dashboards", ValidateOrgPlaylist, wrap(GetPlaylistDashboards)) + r.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, wrap(DeletePlaylist)) + r.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistQuery{}), ValidateOrgPlaylist, wrap(UpdatePlaylist)) + r.Post("/", reqEditorRole, bind(m.CreatePlaylistQuery{}), wrap(CreatePlaylist)) + }) + // Search r.Get("/search/", Search) diff --git a/pkg/api/index.go b/pkg/api/index.go index 9a39837261f..ff5a923ebba 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -53,6 +53,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Href: "/", }) + data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ + Text: "Playlists", + Icon: "fa fa-fw fa-list", + Href: "/playlists", + }) + if c.OrgRole == m.ROLE_ADMIN { data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ Text: "Data Sources", diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go new file mode 100644 index 00000000000..d9503114ff3 --- /dev/null +++ b/pkg/api/playlist.go @@ -0,0 +1,194 @@ +package api + +import ( + "errors" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" + "strconv" +) + +func ValidateOrgPlaylist(c *middleware.Context) { + id := c.ParamsInt64(":id") + query := m.GetPlaylistByIdQuery{Id: id} + err := bus.Dispatch(&query) + + if err != nil { + c.JsonApiErr(404, "Playlist not found", err) + return + } + + if query.Result.OrgId != c.OrgId { + c.JsonApiErr(403, "You are not allowed to edit/view playlist", nil) + return + } +} + +func SearchPlaylists(c *middleware.Context) Response { + query := c.Query("query") + limit := c.QueryInt("limit") + + if limit == 0 { + limit = 1000 + } + + searchQuery := m.PlaylistQuery{ + Title: query, + Limit: limit, + OrgId: c.OrgId, + } + + err := bus.Dispatch(&searchQuery) + if err != nil { + return ApiError(500, "Search failed", err) + } + + return Json(200, searchQuery.Result) +} + +func GetPlaylist(c *middleware.Context) Response { + id := c.ParamsInt64(":id") + cmd := m.GetPlaylistByIdQuery{Id: id} + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Playlist not found", err) + } + + playlistDTOs, _ := LoadPlaylistItemDTOs(id) + + dto := &m.PlaylistDTO{ + Id: cmd.Result.Id, + Title: cmd.Result.Title, + Interval: cmd.Result.Interval, + OrgId: cmd.Result.OrgId, + Items: playlistDTOs, + } + + return Json(200, dto) +} + +func LoadPlaylistItemDTOs(id int64) ([]m.PlaylistItemDTO, error) { + playlistitems, err := LoadPlaylistItems(id) + + if err != nil { + return nil, err + } + + playlistDTOs := make([]m.PlaylistItemDTO, 0) + + for _, item := range playlistitems { + playlistDTOs = append(playlistDTOs, m.PlaylistItemDTO{ + Id: item.Id, + PlaylistId: item.PlaylistId, + Type: item.Type, + Value: item.Value, + Order: item.Order, + Title: item.Title, + }) + } + + return playlistDTOs, nil +} + +func LoadPlaylistItems(id int64) ([]m.PlaylistItem, error) { + itemQuery := m.GetPlaylistItemsByIdQuery{PlaylistId: id} + if err := bus.Dispatch(&itemQuery); err != nil { + return nil, err + } + + return *itemQuery.Result, nil +} + +func LoadPlaylistDashboards(id int64) ([]m.PlaylistDashboardDto, error) { + playlistItems, _ := LoadPlaylistItems(id) + + dashboardIds := make([]int64, 0) + + for _, i := range playlistItems { + dashboardId, _ := strconv.ParseInt(i.Value, 10, 64) + dashboardIds = append(dashboardIds, dashboardId) + } + + if len(dashboardIds) == 0 { + return make([]m.PlaylistDashboardDto, 0), nil + } + + dashboardQuery := m.GetPlaylistDashboardsQuery{DashboardIds: dashboardIds} + if err := bus.Dispatch(&dashboardQuery); err != nil { + log.Warn("dashboardquery failed: %v", err) + return nil, errors.New("Playlist not found") + } + + dtos := make([]m.PlaylistDashboardDto, 0) + for _, item := range *dashboardQuery.Result { + dtos = append(dtos, m.PlaylistDashboardDto{ + Id: item.Id, + Slug: item.Slug, + Title: item.Title, + Uri: "db/" + item.Slug, + }) + } + + return dtos, nil +} + +func GetPlaylistItems(c *middleware.Context) Response { + id := c.ParamsInt64(":id") + + playlistDTOs, err := LoadPlaylistItemDTOs(id) + + if err != nil { + return ApiError(500, "Could not load playlist items", err) + } + + return Json(200, playlistDTOs) +} + +func GetPlaylistDashboards(c *middleware.Context) Response { + id := c.ParamsInt64(":id") + + playlists, err := LoadPlaylistDashboards(id) + if err != nil { + return ApiError(500, "Could not load dashboards", err) + } + + return Json(200, playlists) +} + +func DeletePlaylist(c *middleware.Context) Response { + id := c.ParamsInt64(":id") + + cmd := m.DeletePlaylistQuery{Id: id} + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to delete playlist", err) + } + + return Json(200, "") +} + +func CreatePlaylist(c *middleware.Context, query m.CreatePlaylistQuery) Response { + query.OrgId = c.OrgId + err := bus.Dispatch(&query) + if err != nil { + return ApiError(500, "Failed to create playlist", err) + } + + return Json(200, query.Result) +} + +func UpdatePlaylist(c *middleware.Context, query m.UpdatePlaylistQuery) Response { + err := bus.Dispatch(&query) + if err != nil { + return ApiError(500, "Failed to save playlist", err) + } + + playlistDTOs, err := LoadPlaylistItemDTOs(query.Id) + if err != nil { + return ApiError(500, "Failed to save playlist", err) + } + + query.Result.Items = playlistDTOs + + return Json(200, query.Result) +} diff --git a/pkg/models/playlist.go b/pkg/models/playlist.go new file mode 100644 index 00000000000..5d861dc11b0 --- /dev/null +++ b/pkg/models/playlist.go @@ -0,0 +1,120 @@ +package models + +import ( + "errors" +) + +// Typed errors +var ( + ErrPlaylistNotFound = errors.New("Playlist not found") + ErrPlaylistWithSameNameExists = errors.New("A playlist with the same name already exists") +) + +// Playlist model +type Playlist struct { + Id int64 `json:"id"` + Title string `json:"title"` + Interval string `json:"interval"` + OrgId int64 `json:"-"` +} + +type PlaylistDTO struct { + Id int64 `json:"id"` + Title string `json:"title"` + Interval string `json:"interval"` + OrgId int64 `json:"-"` + Items []PlaylistItemDTO `json:"items"` +} + +type PlaylistItemDTO struct { + Id int64 `json:"id"` + PlaylistId int64 `json:"playlistid"` + Type string `json:"type"` + Title string `json:"title"` + Value string `json:"value"` + Order int `json:"order"` +} + +type PlaylistDashboard struct { + Id int64 `json:"id"` + Slug string `json:"slug"` + Title string `json:"title"` +} + +type PlaylistItem struct { + Id int64 + PlaylistId int64 + Type string + Value string + Order int + Title string +} + +func (this PlaylistDashboard) TableName() string { + return "dashboard" +} + +type Playlists []*Playlist +type PlaylistDashboards []*PlaylistDashboard + +// +// DTOS +// + +type PlaylistDashboardDto struct { + Id int64 `json:"id"` + Slug string `json:"slug"` + Title string `json:"title"` + Uri string `json:"uri"` +} + +// +// COMMANDS +// +type PlaylistQuery struct { + Title string + Limit int + OrgId int64 + + Result Playlists +} + +type UpdatePlaylistQuery struct { + Id int64 + Title string + Type string + Interval string + Items []PlaylistItemDTO + + Result *PlaylistDTO +} + +type CreatePlaylistQuery struct { + Title string + Type string + Interval string + Data []int64 + OrgId int64 + Items []PlaylistItemDTO + + Result *Playlist +} + +type GetPlaylistByIdQuery struct { + Id int64 + Result *Playlist +} + +type GetPlaylistItemsByIdQuery struct { + PlaylistId int64 + Result *[]PlaylistItem +} + +type GetPlaylistDashboardsQuery struct { + DashboardIds []int64 + Result *PlaylistDashboards +} + +type DeletePlaylistQuery struct { + Id int64 +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 4c9283e36ac..c789db1966a 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -20,6 +20,7 @@ func AddMigrations(mg *Migrator) { addQuotaMigration(mg) addPluginBundleMigration(mg) addSessionMigration(mg) + addPlaylistMigrations(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/playlist_mig.go b/pkg/services/sqlstore/migrations/playlist_mig.go new file mode 100644 index 00000000000..b972994e45b --- /dev/null +++ b/pkg/services/sqlstore/migrations/playlist_mig.go @@ -0,0 +1,32 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addPlaylistMigrations(mg *Migrator) { + playlistV1 := Table{ + Name: "playlist", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "interval", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + }, + } + + // create table + mg.AddMigration("create playlist table v1", NewAddTableMigration(playlistV1)) + + playlistItemV1 := Table{ + Name: "playlist_item", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "playlist_id", Type: DB_BigInt, Nullable: false}, + {Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "value", Type: DB_Text, Nullable: false}, + {Name: "title", Type: DB_Text, Nullable: false}, + {Name: "order", Type: DB_Int, Nullable: false}, + }, + } + + mg.AddMigration("create playlist item table v1", NewAddTableMigration(playlistItemV1)) +} diff --git a/pkg/services/sqlstore/playlist.go b/pkg/services/sqlstore/playlist.go new file mode 100644 index 00000000000..fd3a49b2b2a --- /dev/null +++ b/pkg/services/sqlstore/playlist.go @@ -0,0 +1,180 @@ +package sqlstore + +import ( + "fmt" + "github.com/go-xorm/xorm" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", CreatePlaylist) + bus.AddHandler("sql", UpdatePlaylist) + bus.AddHandler("sql", DeletePlaylist) + bus.AddHandler("sql", SearchPlaylists) + bus.AddHandler("sql", GetPlaylist) + bus.AddHandler("sql", GetPlaylistDashboards) + bus.AddHandler("sql", GetPlaylistItem) +} + +func CreatePlaylist(query *m.CreatePlaylistQuery) error { + var err error + + playlist := m.Playlist{ + Title: query.Title, + Interval: query.Interval, + OrgId: query.OrgId, + } + + _, err = x.Insert(&playlist) + + fmt.Printf("%v", playlist.Id) + + playlistItems := make([]m.PlaylistItem, 0) + for _, item := range query.Items { + playlistItems = append(playlistItems, m.PlaylistItem{ + PlaylistId: playlist.Id, + Type: item.Type, + Value: item.Value, + Order: item.Order, + Title: item.Title, + }) + } + + _, err = x.Insert(&playlistItems) + + query.Result = &playlist + return err +} + +func UpdatePlaylist(query *m.UpdatePlaylistQuery) error { + var err error + x.Logger.SetLevel(5) + playlist := m.Playlist{ + Id: query.Id, + Title: query.Title, + Interval: query.Interval, + } + + existingPlaylist := x.Where("id = ?", query.Id).Find(m.Playlist{}) + + if existingPlaylist == nil { + return m.ErrPlaylistNotFound + } + + query.Result = &m.PlaylistDTO{ + Id: playlist.Id, + OrgId: playlist.OrgId, + Title: playlist.Title, + Interval: playlist.Interval, + } + + _, err = x.Id(query.Id).Cols("id", "title", "timespan").Update(&playlist) + + if err != nil { + return err + } + + rawSql := "DELETE FROM playlist_item WHERE playlist_id = ?" + _, err = x.Exec(rawSql, query.Id) + + if err != nil { + return err + } + + playlistItems := make([]m.PlaylistItem, 0) + + for _, item := range query.Items { + playlistItems = append(playlistItems, m.PlaylistItem{ + PlaylistId: playlist.Id, + Type: item.Type, + Value: item.Value, + Order: item.Order, + Title: item.Title, + }) + } + + _, err = x.Insert(&playlistItems) + + return err +} + +func GetPlaylist(query *m.GetPlaylistByIdQuery) error { + if query.Id == 0 { + return m.ErrCommandValidationFailed + } + + playlist := m.Playlist{} + _, err := x.Id(query.Id).Get(&playlist) + + query.Result = &playlist + + return err +} + +func DeletePlaylist(query *m.DeletePlaylistQuery) error { + if query.Id == 0 { + return m.ErrCommandValidationFailed + } + + return inTransaction(func(sess *xorm.Session) error { + var rawPlaylistSql = "DELETE FROM playlist WHERE id = ?" + _, err := sess.Exec(rawPlaylistSql, query.Id) + + if err != nil { + return err + } + + var rawItemSql = "DELETE FROM playlist_item WHERE playlist_id = ?" + _, err2 := sess.Exec(rawItemSql, query.Id) + + return err2 + }) +} + +func SearchPlaylists(query *m.PlaylistQuery) error { + var playlists = make(m.Playlists, 0) + + sess := x.Limit(query.Limit) + + if query.Title != "" { + sess.Where("title LIKE ?", query.Title) + } + + sess.Where("org_id = ?", query.OrgId) + err := sess.Find(&playlists) + query.Result = playlists + + return err +} + +func GetPlaylistItem(query *m.GetPlaylistItemsByIdQuery) error { + if query.PlaylistId == 0 { + return m.ErrCommandValidationFailed + } + + var playlistItems = make([]m.PlaylistItem, 0) + err := x.Where("playlist_id=?", query.PlaylistId).Find(&playlistItems) + + query.Result = &playlistItems + + return err +} + +func GetPlaylistDashboards(query *m.GetPlaylistDashboardsQuery) error { + if len(query.DashboardIds) == 0 { + return m.ErrCommandValidationFailed + } + + var dashboards = make(m.PlaylistDashboards, 0) + + err := x.In("id", query.DashboardIds).Find(&dashboards) + query.Result = &dashboards + + if err != nil { + return err + } + + return nil +} diff --git a/public/app/features/all.js b/public/app/features/all.js index 6519d112b74..9019ec06172 100644 --- a/public/app/features/all.js +++ b/public/app/features/all.js @@ -4,6 +4,7 @@ define([ './annotations/annotationsSrv', './templating/templateSrv', './dashboard/all', + './playlist/all', './panel/all', './profile/profileCtrl', './profile/changePasswordCtrl', diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index 0ee3a64a806..4bddbf8dd95 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -4,7 +4,6 @@ define([ './dashboardNavCtrl', './snapshotTopNavCtrl', './saveDashboardAsCtrl', - './playlistCtrl', './rowCtrl', './shareModalCtrl', './shareSnapshotCtrl', @@ -12,7 +11,6 @@ define([ './dashboardSrv', './keybindings', './viewStateSrv', - './playlistSrv', './timeSrv', './unsavedChangesSrv', './directives/dashSearchView', diff --git a/public/app/features/dashboard/playlistCtrl.js b/public/app/features/dashboard/playlistCtrl.js deleted file mode 100644 index 9242905405a..00000000000 --- a/public/app/features/dashboard/playlistCtrl.js +++ /dev/null @@ -1,55 +0,0 @@ -define([ - 'angular', - 'lodash', - 'app/core/config' -], -function (angular, _, config) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('PlaylistCtrl', function($scope, playlistSrv, backendSrv) { - - $scope.init = function() { - $scope.playlist = []; - $scope.timespan = config.playlist_timespan; - $scope.search(); - }; - - $scope.search = function() { - var query = {starred: true, limit: 10}; - - if ($scope.searchQuery) { - query.query = $scope.searchQuery; - query.starred = false; - } - - backendSrv.search(query).then(function(results) { - $scope.searchHits = results; - $scope.filterHits(); - }); - }; - - $scope.filterHits = function() { - $scope.filteredHits = _.reject($scope.searchHits, function(dash) { - return _.findWhere($scope.playlist, {uri: dash.uri}); - }); - }; - - $scope.addDashboard = function(dashboard) { - $scope.playlist.push(dashboard); - $scope.filterHits(); - }; - - $scope.removeDashboard = function(dashboard) { - $scope.playlist = _.without($scope.playlist, dashboard); - $scope.filterHits(); - }; - - $scope.start = function() { - playlistSrv.start($scope.playlist, $scope.timespan); - }; - - }); - -}); diff --git a/public/app/features/playlist/all.js b/public/app/features/playlist/all.js new file mode 100644 index 00000000000..df0d6983c06 --- /dev/null +++ b/public/app/features/playlist/all.js @@ -0,0 +1,6 @@ +define([ + './playlists_ctrl', + './playlistSrv', + './playlist_edit_ctrl', + './playlist_routes' +], function () {}); diff --git a/public/app/features/playlist/partials/playlist-remove.html b/public/app/features/playlist/partials/playlist-remove.html new file mode 100644 index 00000000000..8474f97f8de --- /dev/null +++ b/public/app/features/playlist/partials/playlist-remove.html @@ -0,0 +1,5 @@ +

Are you sure want to delete "{{playlist.title}}" playlist?

+

+ + +

diff --git a/public/app/features/playlist/partials/playlist.html b/public/app/features/playlist/partials/playlist.html new file mode 100644 index 00000000000..077ef5cbdd0 --- /dev/null +++ b/public/app/features/playlist/partials/playlist.html @@ -0,0 +1,120 @@ + + +
+
+

New playlist

+

Edit playlist

+ +
1. Name and interval
+ +
+
+
+
    +
  • + Title +
  • +
  • + +
  • +
+
+
+
+
    +
  • + Interval +
  • +
  • + +
  • +
+
+
+
+ +
+
2. Add dashboards
+ +
+
+
    +
  • + +
  • +
  • + +
  • +
+
+
+
+
+
+ +
+
+
Search results ({{filteredPlaylistItems.length}})
+ + + + + + + + +
+ {{playlistItem.title}} + + +
+ Search results empty +
+
+
+
Playlist dashboards
+ + + + + +
+ {{playlistItem.title}} + + + + +
+
+
+ +
+ +
+ + + + +
+ +
+
diff --git a/public/app/features/playlist/partials/playlists.html b/public/app/features/playlist/partials/playlists.html new file mode 100644 index 00000000000..e424f1a2d88 --- /dev/null +++ b/public/app/features/playlist/partials/playlists.html @@ -0,0 +1,48 @@ + + +
+
+

Saved playlists

+ + +
+ + + + + + + + + + + + + + + + + +
TitleUrl
+ {{playlist.title}} + + {{ playlistUrl(playlist) }} + + + + Play + + + + + Edit + + + + + +
+ +
+
diff --git a/public/app/features/dashboard/playlistSrv.js b/public/app/features/playlist/playlistSrv.js similarity index 91% rename from public/app/features/dashboard/playlistSrv.js rename to public/app/features/playlist/playlistSrv.js index bf4587f6c48..f7ea59b6c98 100644 --- a/public/app/features/dashboard/playlistSrv.js +++ b/public/app/features/playlist/playlistSrv.js @@ -28,11 +28,11 @@ function (angular, _, kbn) { self.next(); }; - this.start = function(dashboards, timespan) { + this.start = function(dashboards, interval) { self.stop(); self.index = 0; - self.interval = kbn.interval_to_ms(timespan); + self.interval = kbn.interval_to_ms(interval); self.dashboards = dashboards; $rootScope.playlistSrv = this; diff --git a/public/app/features/playlist/playlist_edit_ctrl.js b/public/app/features/playlist/playlist_edit_ctrl.js new file mode 100644 index 00000000000..610558cc5fa --- /dev/null +++ b/public/app/features/playlist/playlist_edit_ctrl.js @@ -0,0 +1,144 @@ +define([ + 'angular', + 'app/core/config', + 'lodash' +], +function (angular, config, _) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('PlaylistEditCtrl', function($scope, playlistSrv, backendSrv, $location, $route) { + $scope.filteredPlaylistItems = []; + $scope.foundPlaylistItems = []; + $scope.searchQuery = ''; + $scope.loading = false; + $scope.playlist = {}; + $scope.playlistItems = []; + + $scope.init = function() { + if ($route.current.params.id) { + var playlistId = $route.current.params.id; + + backendSrv.get('/api/playlists/' + playlistId) + .then(function(result) { + $scope.playlist = result; + }); + + backendSrv.get('/api/playlists/' + playlistId + '/items') + .then(function(result) { + $scope.playlistItems = result; + }); + } + + $scope.search(); + }; + + $scope.search = function() { + var query = {starred: true, limit: 10}; + + if ($scope.searchQuery) { + query.query = $scope.searchQuery; + query.starred = false; + } + + $scope.loading = true; + + backendSrv.search(query) + .then(function(results) { + $scope.foundPlaylistItems = results; + $scope.filterFoundPlaylistItems(); + }) + .finally(function() { + $scope.loading = false; + }); + }; + + $scope.filterFoundPlaylistItems = function() { + $scope.filteredPlaylistItems = _.reject($scope.foundPlaylistItems, function(playlistItem) { + return _.findWhere($scope.playlistItems, function(listPlaylistItem) { + return parseInt(listPlaylistItem.value) === playlistItem.id; + }); + }); + }; + + $scope.addPlaylistItem = function(playlistItem) { + playlistItem.value = playlistItem.id.toString(); + playlistItem.type = 'dashboard_by_id'; + playlistItem.order = $scope.playlistItems.length + 1; + + $scope.playlistItems.push(playlistItem); + $scope.filterFoundPlaylistItems(); + + }; + + $scope.removePlaylistItem = function(playlistItem) { + _.remove($scope.playlistItems, function(listedPlaylistItem) { + return playlistItem === listedPlaylistItem; + }); + $scope.filterFoundPlaylistItems(); + }; + + $scope.savePlaylist = function(playlist, playlistItems) { + var savePromise; + + playlist.items = playlistItems; + + savePromise = playlist.id + ? backendSrv.put('/api/playlists/' + playlist.id, playlist) + : backendSrv.post('/api/playlists', playlist); + + savePromise + .then(function() { + $scope.appEvent('alert-success', ['Playlist saved', '']); + $location.path('/playlists'); + }, function() { + $scope.appEvent('alert-error', ['Unable to save playlist', '']); + }); + }; + + $scope.isNew = function() { + return !$scope.playlist.id; + }; + + $scope.isPlaylistEmpty = function() { + return !$scope.playlistItems.length; + }; + + $scope.isSearchResultsEmpty = function() { + return !$scope.foundPlaylistItems.length; + }; + + $scope.isSearchQueryEmpty = function() { + return $scope.searchQuery === ''; + }; + + $scope.backToList = function() { + $location.path('/playlists'); + }; + + $scope.isLoading = function() { + return $scope.loading; + }; + + $scope.movePlaylistItem = function(playlistItem, offset) { + var currentPosition = $scope.playlistItems.indexOf(playlistItem); + var newPosition = currentPosition + offset; + + if (newPosition >= 0 && newPosition < $scope.playlistItems.length) { + $scope.playlistItems.splice(currentPosition, 1); + $scope.playlistItems.splice(newPosition, 0, playlistItem); + } + }; + + $scope.movePlaylistItemUp = function(playlistItem) { + $scope.moveDashboard(playlistItem, -1); + }; + + $scope.movePlaylistItemDown = function(playlistItem) { + $scope.moveDashboard(playlistItem, 1); + }; + + $scope.init(); + }); +}); diff --git a/public/app/features/playlist/playlist_routes.js b/public/app/features/playlist/playlist_routes.js new file mode 100644 index 00000000000..b9358148f8a --- /dev/null +++ b/public/app/features/playlist/playlist_routes.js @@ -0,0 +1,43 @@ +define([ + 'angular', + 'app/core/config', + 'lodash' +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.routes'); + + module.config(function($routeProvider) { + $routeProvider + .when('/playlists', { + templateUrl: 'app/features/playlist/partials/playlists.html', + controller : 'PlaylistsCtrl' + }) + .when('/playlists/create', { + templateUrl: 'app/features/playlist/partials/playlist.html', + controller : 'PlaylistEditCtrl' + }) + .when('/playlists/edit/:id', { + templateUrl: 'app/features/playlist/partials/playlist.html', + controller : 'PlaylistEditCtrl' + }) + .when('/playlists/play/:id', { + templateUrl: 'app/partials/dashboard.html', + controller : 'LoadDashboardCtrl', + resolve: { + init: function(backendSrv, playlistSrv, $route) { + var playlistId = $route.current.params.id; + + return backendSrv.get('/api/playlists/' + playlistId) + .then(function(playlist) { + return backendSrv.get('/api/playlists/' + playlistId + '/dashboards') + .then(function(dashboards) { + playlistSrv.start(dashboards, playlist.interval); + }); + }); + } + } + }); + }); +}); diff --git a/public/app/features/playlist/playlists_ctrl.js b/public/app/features/playlist/playlists_ctrl.js new file mode 100644 index 00000000000..092bb00f56c --- /dev/null +++ b/public/app/features/playlist/playlists_ctrl.js @@ -0,0 +1,51 @@ +define([ + 'angular', + 'lodash' +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.controllers'); + + module.controller('PlaylistsCtrl', function( + $scope, + $location, + backendSrv + ) { + backendSrv.get('/api/playlists') + .then(function(result) { + $scope.playlists = result; + }); + + $scope.playlistUrl = function(playlist) { + return '/playlists/play/' + playlist.id; + }; + + $scope.removePlaylist = function(playlist) { + var modalScope = $scope.$new(true); + + modalScope.playlist = playlist; + modalScope.removePlaylist = function() { + modalScope.dismiss(); + _.remove($scope.playlists, {id: playlist.id}); + + backendSrv.delete('/api/playlists/' + playlist.id) + .then(function() { + $scope.appEvent('alert-success', ['Playlist deleted', '']); + }, function() { + $scope.appEvent('alert-error', ['Unable to delete playlist', '']); + $scope.playlists.push(playlist); + }); + }; + + $scope.appEvent('show-modal', { + src: './app/features/playlist/partials/playlist-remove.html', + scope: modalScope + }); + }; + + $scope.createPlaylist = function() { + $location.path('/playlists/create'); + }; + }); +}); diff --git a/public/app/features/playlist/specs/playlist-edit-ctrl-specs.ts b/public/app/features/playlist/specs/playlist-edit-ctrl-specs.ts new file mode 100644 index 00000000000..e0ffdd49893 --- /dev/null +++ b/public/app/features/playlist/specs/playlist-edit-ctrl-specs.ts @@ -0,0 +1,86 @@ +import '../playlist_edit_ctrl'; +import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; +import helpers from 'test/specs/helpers'; + +describe('PlaylistEditCtrl', function() { + var ctx = new helpers.ControllerTestContext(); + + var searchResult = [ + { + id: 2, + title: 'dashboard: 2' + }, + { + id: 3, + title: 'dashboard: 3' + } + ]; + + var playlistSrv = {}; + var backendSrv = { + search: (query) => { + return ctx.$q.when(searchResult); + } + }; + + beforeEach(angularMocks.module('grafana.core')); + beforeEach(angularMocks.module('grafana.controllers')); + beforeEach(angularMocks.module('grafana.services')); + beforeEach(ctx.providePhase({ + playlistSrv: playlistSrv, + backendSrv: backendSrv, + $route: { current: { params: { } } }, + })); + + beforeEach(ctx.createControllerPhase('PlaylistEditCtrl')); + + beforeEach(() => { + ctx.scope.$digest(); + }); + + describe('searchresult returns 2 dashboards', function() { + it('found dashboard should be 2', function() { + expect(ctx.scope.foundPlaylistItems.length).to.be(2); + }); + + it('filtred dashboard should be 2', function() { + expect(ctx.scope.filteredPlaylistItems.length).to.be(2); + }); + + describe('adds one dashboard to playlist', () => { + beforeEach(() => { + ctx.scope.addPlaylistItem({ id: 2, title: 'dashboard: 2' }); + }); + + it('playlistitems should be increased by one', () => { + expect(ctx.scope.playlistItems.length).to.be(1); + }); + + it('filtred playlistitems should be reduced by one', () => { + expect(ctx.scope.filteredPlaylistItems.length).to.be(1); + }); + + it('found dashboard should be 2', function() { + expect(ctx.scope.foundPlaylistItems.length).to.be(2); + }); + + describe('removes one dashboard from playlist', () => { + beforeEach(() => { + ctx.scope.removePlaylistItem(ctx.scope.playlistItems[0]); + }); + + it('playlistitems should be increased by one', () => { + expect(ctx.scope.playlistItems.length).to.be(0); + }); + + it('found dashboard should be 2', function() { + expect(ctx.scope.foundPlaylistItems.length).to.be(2); + }); + + it('filtred playlist should be reduced by one', () => { + expect(ctx.scope.filteredPlaylistItems.length).to.be(2); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/public/app/partials/playlist.html b/public/app/partials/playlist.html deleted file mode 100644 index 3d12e5af2d6..00000000000 --- a/public/app/partials/playlist.html +++ /dev/null @@ -1,100 +0,0 @@ -
-
-
- - Start dashboard playlist -
- - - -
- -
-
-
-
-
-
    -
  • - Search -
  • -
  • - -
  • -
-
-
-
-
-
- -
-
-
Search result
- - - - - - - - -
- {{dashboard.title}} - - -
- No dashboards found -
-
-
-
Playlist dashboards
- - - - - - - - - -
- {{dashboard.title}} - - -
- Playlist empty -
-
-
- -
-
-
-
-
    -
  • - Timespan between dashboard change -
  • -
  • - -
  • -
  • - -
  • -
-
-
-
- -
- -
-
diff --git a/public/app/partials/search.html b/public/app/partials/search.html index 97bdbc496bf..fab8fd9291a 100644 --- a/public/app/partials/search.html +++ b/public/app/partials/search.html @@ -71,10 +71,6 @@ Import -