diff --git a/pkg/api/api.go b/pkg/api/api.go index 839978845ab..1e1348a51ab 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,16 @@ func Register(r *macaron.Macaron) { r.Get("/tags", GetDashboardTags) }) + // Playlist + r.Group("/playlists", func() { + r.Get("/", SearchPlaylists) + r.Get("/:id", ValidateOrgPlaylist, GetPlaylist) + r.Get("/:id/dashboards", ValidateOrgPlaylist, GetPlaylistDashboards) + r.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, DeletePlaylist) + r.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistQuery{}), ValidateOrgPlaylist, UpdatePlaylist) + r.Post("/", reqEditorRole, bind(m.CreatePlaylistQuery{}), 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..83355418b4b --- /dev/null +++ b/pkg/api/playlist.go @@ -0,0 +1,103 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" +) + +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) { + 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 { + c.JsonApiErr(500, "Search failed", err) + return + } + + c.JSON(200, searchQuery.Result) +} + +func GetPlaylist(c *middleware.Context) { + id := c.ParamsInt64(":id") + cmd := m.GetPlaylistByIdQuery{Id: id} + + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Playlist not found", err) + return + } + + c.JSON(200, cmd.Result) +} + +func GetPlaylistDashboards(c *middleware.Context) { + id := c.ParamsInt64(":id") + + query := m.GetPlaylistDashboardsQuery{Id: id} + if err := bus.Dispatch(&query); err != nil { + c.JsonApiErr(500, "Playlist not found", err) + return + } + + c.JSON(200, query.Result) +} + +func DeletePlaylist(c *middleware.Context) { + id := c.ParamsInt64(":id") + + cmd := m.DeletePlaylistQuery{Id: id} + if err := bus.Dispatch(&cmd); err != nil { + c.JsonApiErr(500, "Failed to delete playlist", err) + return + } + + c.JSON(200, "") +} + +func CreatePlaylist(c *middleware.Context, query m.CreatePlaylistQuery) { + query.OrgId = c.OrgId + err := bus.Dispatch(&query) + if err != nil { + c.JsonApiErr(500, "Failed to create playlist", err) + return + } + + c.JSON(200, query.Result) +} + +func UpdatePlaylist(c *middleware.Context, query m.UpdatePlaylistQuery) { + err := bus.Dispatch(&query) + if err != nil { + c.JsonApiErr(500, "Failed to save playlist", err) + return + } + + c.JSON(200, query.Result) +} diff --git a/pkg/models/playlist.go b/pkg/models/playlist.go new file mode 100644 index 00000000000..1bfa5949fe2 --- /dev/null +++ b/pkg/models/playlist.go @@ -0,0 +1,79 @@ +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"` + Type string `json:"type"` + Timespan string `json:"timespan"` + Data []int `json:"data"` + OrgId int64 `json:"-"` +} + +type PlaylistDashboard struct { + Id int64 `json:"id"` + Slug string `json:"slug"` + Title string `json:"title"` +} + +func (this PlaylistDashboard) TableName() string { + return "dashboard" +} + +type Playlists []*Playlist +type PlaylistDashboards []*PlaylistDashboard + +// +// COMMANDS +// +type PlaylistQuery struct { + Title string + Limit int + OrgId int64 + + Result Playlists +} + +type UpdatePlaylistQuery struct { + Id int64 + Title string + Type string + Timespan string + Data []int + + Result *Playlist +} + +type CreatePlaylistQuery struct { + Title string + Type string + Timespan string + Data []int + OrgId int64 + + Result *Playlist +} + +type GetPlaylistByIdQuery struct { + Id int64 + Result *Playlist +} + +type GetPlaylistDashboardsQuery struct { + Id 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 569d26282ed..0faddb144c9 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -19,6 +19,7 @@ func AddMigrations(mg *Migrator) { addDashboardSnapshotMigrations(mg) addQuotaMigration(mg) addPluginBundleMigration(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..d8625f301ae --- /dev/null +++ b/pkg/services/sqlstore/migrations/playlist_mig.go @@ -0,0 +1,20 @@ +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: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "data", Type: DB_Text, Nullable: false}, + {Name: "timespan", 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)) +} diff --git a/pkg/services/sqlstore/playlist.go b/pkg/services/sqlstore/playlist.go new file mode 100644 index 00000000000..3e04f65f9d4 --- /dev/null +++ b/pkg/services/sqlstore/playlist.go @@ -0,0 +1,125 @@ +package sqlstore + +import ( + "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) +} + +func CreatePlaylist(query *m.CreatePlaylistQuery) error { + var err error + + playlist := m.Playlist{ + Title: query.Title, + Type: query.Type, + Data: query.Data, + Timespan: query.Timespan, + OrgId: query.OrgId, + } + + _, err = x.Insert(&playlist) + + 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, + Type: query.Type, + Data: query.Data, + Timespan: query.Timespan, + } + + existingPlaylist := x.Where("id = ?", query.Id).Find(m.Playlist{}) + + if existingPlaylist == nil { + return m.ErrPlaylistNotFound + } + + _, err = x.Id(query.Id).Cols("id", "title", "data", "timespan").Update(&playlist) + + query.Result = &playlist + 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 rawSql = "DELETE FROM playlist WHERE id = ?" + _, err := sess.Exec(rawSql, query.Id) + return err + }) +} + +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 GetPlaylistDashboards(query *m.GetPlaylistDashboardsQuery) error { + if query.Id == 0 { + return m.ErrCommandValidationFailed + } + + var dashboards = make(m.PlaylistDashboards, 0) + var playlist = m.Playlist{} + + hasPlaylist, err := x.Id(query.Id).Get(&playlist) + query.Result = &dashboards + + if err != nil { + return err + } + + if !hasPlaylist || len(playlist.Data) == 0 { + return nil + } + + err = x.In("id", playlist.Data).Find(&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/playlist/all.js b/public/app/features/playlist/all.js new file mode 100644 index 00000000000..85831247eb7 --- /dev/null +++ b/public/app/features/playlist/all.js @@ -0,0 +1,5 @@ +define([ + './playlistsCtrl', + './playlistEditCtrl', + './playlistRoutes' +], 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..d40b8b9ca42 --- /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..5b81649f636 --- /dev/null +++ b/public/app/features/playlist/partials/playlist.html @@ -0,0 +1,115 @@ ++ {{dashboard.title}} + | ++ + | +
+ No dashboards found + | +|
+ Playlist empty + | +
+ {{dashboard.title}} + | ++ + | +
Title | +Url | ++ | + |
+ {{playlist.title}} + | ++ {{ playlistUrl(playlist) }} + | ++ + + Edit + + | ++ + + + | +