Skip to content

Commit f7fecdc

Browse files
committed
Merge pull request grafana#3655 from grafana/playlist
Persistable playlists closes grafana#515 closes grafana#1137
2 parents 144c703 + cabefa4 commit f7fecdc

File tree

22 files changed

+1066
-171
lines changed

22 files changed

+1066
-171
lines changed

docs/sources/reference/playlist.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,32 @@ The Playlist is a special type of Dashboard that rotates through a list of Dashb
1010

1111
Since Grafana automatically scales Dashboards to any resolution they're perfect for big screens!
1212

13-
## Configuring the Playlist
13+
## Creating a Playlist
1414

15-
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.
15+
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.
1616

1717
<img src="/img/v2/dashboard_search.png" class="no-shadow">
1818

19-
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).
19+
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.
2020

2121
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.
2222

23-
Be sure to click the right arrow appearing next to the Dashboard name to add it to the Playlist.
23+
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.
2424

25-
You can configure a time interval for Grafana to wait on a particular Dashboard before advancing to the next one on the Playlist.
25+
Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here.
2626

27-
## Starting and controlling the Playlist
27+
## Saving the playlist
2828

29-
To start the Playlist, click the green "Start" button
29+
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.
30+
31+
## Starting the playlist
32+
33+
Also, if you want, you can start the playlist without saving it by clicking on the green "Start" button at the bottom.
34+
35+
## Controlling the Playlist
3036

3137
Playlists can also be manually controlled utilizing the Playlist controls at the top of screen when in Playlist mode.
3238

3339
Click the stop button to stop the Playlist, and exit to the current Dashboard.
3440
Click the next button to advance to the next Dashboard in the Playlist.
3541
Click the back button to rewind to the previous Dashboard in the Playlist.
36-

pkg/api/api.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ func Register(r *macaron.Macaron) {
4747
r.Get("/dashboard/*", reqSignedIn, Index)
4848
r.Get("/dashboard-solo/*", reqSignedIn, Index)
4949

50+
r.Get("/playlists/", reqSignedIn, Index)
51+
r.Get("/playlists/*", reqSignedIn, Index)
52+
5053
// sign up
5154
r.Get("/signup", Index)
5255
r.Get("/api/user/signup/options", wrap(GetSignUpOptions))
@@ -169,6 +172,17 @@ func Register(r *macaron.Macaron) {
169172
r.Get("/tags", GetDashboardTags)
170173
})
171174

175+
// Playlist
176+
r.Group("/playlists", func() {
177+
r.Get("/", wrap(SearchPlaylists))
178+
r.Get("/:id", ValidateOrgPlaylist, wrap(GetPlaylist))
179+
r.Get("/:id/items", ValidateOrgPlaylist, wrap(GetPlaylistItems))
180+
r.Get("/:id/dashboards", ValidateOrgPlaylist, wrap(GetPlaylistDashboards))
181+
r.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, wrap(DeletePlaylist))
182+
r.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistQuery{}), ValidateOrgPlaylist, wrap(UpdatePlaylist))
183+
r.Post("/", reqEditorRole, bind(m.CreatePlaylistQuery{}), wrap(CreatePlaylist))
184+
})
185+
172186
// Search
173187
r.Get("/search/", Search)
174188

pkg/api/index.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
5353
Href: "/",
5454
})
5555

56+
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
57+
Text: "Playlists",
58+
Icon: "fa fa-fw fa-list",
59+
Href: "/playlists",
60+
})
61+
5662
if c.OrgRole == m.ROLE_ADMIN {
5763
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
5864
Text: "Data Sources",

pkg/api/playlist.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package api
2+
3+
import (
4+
"errors"
5+
"github.com/grafana/grafana/pkg/bus"
6+
"github.com/grafana/grafana/pkg/log"
7+
"github.com/grafana/grafana/pkg/middleware"
8+
m "github.com/grafana/grafana/pkg/models"
9+
"strconv"
10+
)
11+
12+
func ValidateOrgPlaylist(c *middleware.Context) {
13+
id := c.ParamsInt64(":id")
14+
query := m.GetPlaylistByIdQuery{Id: id}
15+
err := bus.Dispatch(&query)
16+
17+
if err != nil {
18+
c.JsonApiErr(404, "Playlist not found", err)
19+
return
20+
}
21+
22+
if query.Result.OrgId != c.OrgId {
23+
c.JsonApiErr(403, "You are not allowed to edit/view playlist", nil)
24+
return
25+
}
26+
}
27+
28+
func SearchPlaylists(c *middleware.Context) Response {
29+
query := c.Query("query")
30+
limit := c.QueryInt("limit")
31+
32+
if limit == 0 {
33+
limit = 1000
34+
}
35+
36+
searchQuery := m.PlaylistQuery{
37+
Title: query,
38+
Limit: limit,
39+
OrgId: c.OrgId,
40+
}
41+
42+
err := bus.Dispatch(&searchQuery)
43+
if err != nil {
44+
return ApiError(500, "Search failed", err)
45+
}
46+
47+
return Json(200, searchQuery.Result)
48+
}
49+
50+
func GetPlaylist(c *middleware.Context) Response {
51+
id := c.ParamsInt64(":id")
52+
cmd := m.GetPlaylistByIdQuery{Id: id}
53+
54+
if err := bus.Dispatch(&cmd); err != nil {
55+
return ApiError(500, "Playlist not found", err)
56+
}
57+
58+
playlistDTOs, _ := LoadPlaylistItemDTOs(id)
59+
60+
dto := &m.PlaylistDTO{
61+
Id: cmd.Result.Id,
62+
Title: cmd.Result.Title,
63+
Interval: cmd.Result.Interval,
64+
OrgId: cmd.Result.OrgId,
65+
Items: playlistDTOs,
66+
}
67+
68+
return Json(200, dto)
69+
}
70+
71+
func LoadPlaylistItemDTOs(id int64) ([]m.PlaylistItemDTO, error) {
72+
playlistitems, err := LoadPlaylistItems(id)
73+
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
playlistDTOs := make([]m.PlaylistItemDTO, 0)
79+
80+
for _, item := range playlistitems {
81+
playlistDTOs = append(playlistDTOs, m.PlaylistItemDTO{
82+
Id: item.Id,
83+
PlaylistId: item.PlaylistId,
84+
Type: item.Type,
85+
Value: item.Value,
86+
Order: item.Order,
87+
Title: item.Title,
88+
})
89+
}
90+
91+
return playlistDTOs, nil
92+
}
93+
94+
func LoadPlaylistItems(id int64) ([]m.PlaylistItem, error) {
95+
itemQuery := m.GetPlaylistItemsByIdQuery{PlaylistId: id}
96+
if err := bus.Dispatch(&itemQuery); err != nil {
97+
return nil, err
98+
}
99+
100+
return *itemQuery.Result, nil
101+
}
102+
103+
func LoadPlaylistDashboards(id int64) ([]m.PlaylistDashboardDto, error) {
104+
playlistItems, _ := LoadPlaylistItems(id)
105+
106+
dashboardIds := make([]int64, 0)
107+
108+
for _, i := range playlistItems {
109+
dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
110+
dashboardIds = append(dashboardIds, dashboardId)
111+
}
112+
113+
if len(dashboardIds) == 0 {
114+
return make([]m.PlaylistDashboardDto, 0), nil
115+
}
116+
117+
dashboardQuery := m.GetPlaylistDashboardsQuery{DashboardIds: dashboardIds}
118+
if err := bus.Dispatch(&dashboardQuery); err != nil {
119+
log.Warn("dashboardquery failed: %v", err)
120+
return nil, errors.New("Playlist not found")
121+
}
122+
123+
dtos := make([]m.PlaylistDashboardDto, 0)
124+
for _, item := range *dashboardQuery.Result {
125+
dtos = append(dtos, m.PlaylistDashboardDto{
126+
Id: item.Id,
127+
Slug: item.Slug,
128+
Title: item.Title,
129+
Uri: "db/" + item.Slug,
130+
})
131+
}
132+
133+
return dtos, nil
134+
}
135+
136+
func GetPlaylistItems(c *middleware.Context) Response {
137+
id := c.ParamsInt64(":id")
138+
139+
playlistDTOs, err := LoadPlaylistItemDTOs(id)
140+
141+
if err != nil {
142+
return ApiError(500, "Could not load playlist items", err)
143+
}
144+
145+
return Json(200, playlistDTOs)
146+
}
147+
148+
func GetPlaylistDashboards(c *middleware.Context) Response {
149+
id := c.ParamsInt64(":id")
150+
151+
playlists, err := LoadPlaylistDashboards(id)
152+
if err != nil {
153+
return ApiError(500, "Could not load dashboards", err)
154+
}
155+
156+
return Json(200, playlists)
157+
}
158+
159+
func DeletePlaylist(c *middleware.Context) Response {
160+
id := c.ParamsInt64(":id")
161+
162+
cmd := m.DeletePlaylistQuery{Id: id}
163+
if err := bus.Dispatch(&cmd); err != nil {
164+
return ApiError(500, "Failed to delete playlist", err)
165+
}
166+
167+
return Json(200, "")
168+
}
169+
170+
func CreatePlaylist(c *middleware.Context, query m.CreatePlaylistQuery) Response {
171+
query.OrgId = c.OrgId
172+
err := bus.Dispatch(&query)
173+
if err != nil {
174+
return ApiError(500, "Failed to create playlist", err)
175+
}
176+
177+
return Json(200, query.Result)
178+
}
179+
180+
func UpdatePlaylist(c *middleware.Context, query m.UpdatePlaylistQuery) Response {
181+
err := bus.Dispatch(&query)
182+
if err != nil {
183+
return ApiError(500, "Failed to save playlist", err)
184+
}
185+
186+
playlistDTOs, err := LoadPlaylistItemDTOs(query.Id)
187+
if err != nil {
188+
return ApiError(500, "Failed to save playlist", err)
189+
}
190+
191+
query.Result.Items = playlistDTOs
192+
193+
return Json(200, query.Result)
194+
}

pkg/models/playlist.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package models
2+
3+
import (
4+
"errors"
5+
)
6+
7+
// Typed errors
8+
var (
9+
ErrPlaylistNotFound = errors.New("Playlist not found")
10+
ErrPlaylistWithSameNameExists = errors.New("A playlist with the same name already exists")
11+
)
12+
13+
// Playlist model
14+
type Playlist struct {
15+
Id int64 `json:"id"`
16+
Title string `json:"title"`
17+
Interval string `json:"interval"`
18+
OrgId int64 `json:"-"`
19+
}
20+
21+
type PlaylistDTO struct {
22+
Id int64 `json:"id"`
23+
Title string `json:"title"`
24+
Interval string `json:"interval"`
25+
OrgId int64 `json:"-"`
26+
Items []PlaylistItemDTO `json:"items"`
27+
}
28+
29+
type PlaylistItemDTO struct {
30+
Id int64 `json:"id"`
31+
PlaylistId int64 `json:"playlistid"`
32+
Type string `json:"type"`
33+
Title string `json:"title"`
34+
Value string `json:"value"`
35+
Order int `json:"order"`
36+
}
37+
38+
type PlaylistDashboard struct {
39+
Id int64 `json:"id"`
40+
Slug string `json:"slug"`
41+
Title string `json:"title"`
42+
}
43+
44+
type PlaylistItem struct {
45+
Id int64
46+
PlaylistId int64
47+
Type string
48+
Value string
49+
Order int
50+
Title string
51+
}
52+
53+
func (this PlaylistDashboard) TableName() string {
54+
return "dashboard"
55+
}
56+
57+
type Playlists []*Playlist
58+
type PlaylistDashboards []*PlaylistDashboard
59+
60+
//
61+
// DTOS
62+
//
63+
64+
type PlaylistDashboardDto struct {
65+
Id int64 `json:"id"`
66+
Slug string `json:"slug"`
67+
Title string `json:"title"`
68+
Uri string `json:"uri"`
69+
}
70+
71+
//
72+
// COMMANDS
73+
//
74+
type PlaylistQuery struct {
75+
Title string
76+
Limit int
77+
OrgId int64
78+
79+
Result Playlists
80+
}
81+
82+
type UpdatePlaylistQuery struct {
83+
Id int64
84+
Title string
85+
Type string
86+
Interval string
87+
Items []PlaylistItemDTO
88+
89+
Result *PlaylistDTO
90+
}
91+
92+
type CreatePlaylistQuery struct {
93+
Title string
94+
Type string
95+
Interval string
96+
Data []int64
97+
OrgId int64
98+
Items []PlaylistItemDTO
99+
100+
Result *Playlist
101+
}
102+
103+
type GetPlaylistByIdQuery struct {
104+
Id int64
105+
Result *Playlist
106+
}
107+
108+
type GetPlaylistItemsByIdQuery struct {
109+
PlaylistId int64
110+
Result *[]PlaylistItem
111+
}
112+
113+
type GetPlaylistDashboardsQuery struct {
114+
DashboardIds []int64
115+
Result *PlaylistDashboards
116+
}
117+
118+
type DeletePlaylistQuery struct {
119+
Id int64
120+
}

0 commit comments

Comments
 (0)