mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #3655 from grafana/playlist
Persistable playlists closes #515 closes #1137
This commit is contained in:
@@ -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.
|
||||
|
||||
<img src="/img/v2/dashboard_search.png" class="no-shadow">
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
194
pkg/api/playlist.go
Normal file
194
pkg/api/playlist.go
Normal file
@@ -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)
|
||||
}
|
||||
120
pkg/models/playlist.go
Normal file
120
pkg/models/playlist.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -20,6 +20,7 @@ func AddMigrations(mg *Migrator) {
|
||||
addQuotaMigration(mg)
|
||||
addPluginBundleMigration(mg)
|
||||
addSessionMigration(mg)
|
||||
addPlaylistMigrations(mg)
|
||||
}
|
||||
|
||||
func addMigrationLogMigrations(mg *Migrator) {
|
||||
|
||||
32
pkg/services/sqlstore/migrations/playlist_mig.go
Normal file
32
pkg/services/sqlstore/migrations/playlist_mig.go
Normal file
@@ -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))
|
||||
}
|
||||
180
pkg/services/sqlstore/playlist.go
Normal file
180
pkg/services/sqlstore/playlist.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -4,6 +4,7 @@ define([
|
||||
'./annotations/annotationsSrv',
|
||||
'./templating/templateSrv',
|
||||
'./dashboard/all',
|
||||
'./playlist/all',
|
||||
'./panel/all',
|
||||
'./profile/profileCtrl',
|
||||
'./profile/changePasswordCtrl',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
6
public/app/features/playlist/all.js
Normal file
6
public/app/features/playlist/all.js
Normal file
@@ -0,0 +1,6 @@
|
||||
define([
|
||||
'./playlists_ctrl',
|
||||
'./playlistSrv',
|
||||
'./playlist_edit_ctrl',
|
||||
'./playlist_routes'
|
||||
], function () {});
|
||||
@@ -0,0 +1,5 @@
|
||||
<p class="text-center">Are you sure want to delete "{{playlist.title}}" playlist?</p>
|
||||
<p class="text-center">
|
||||
<button type="button" class="btn btn-danger" ng-click="removePlaylist()">Yes</button>
|
||||
<button type="button" class="btn btn-default" ng-click="dismiss()">No</button>
|
||||
</p>
|
||||
120
public/app/features/playlist/partials/playlist.html
Normal file
120
public/app/features/playlist/partials/playlist.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<topnav icon="fa fa-fw fa-list" title="Playlists"></topnav>
|
||||
|
||||
<div class="page-container" ng-form="playlistEditForm">
|
||||
<div class="page">
|
||||
<h2 ng-show="isNew()">New playlist</h2>
|
||||
<h2 ng-show="!isNew()">Edit playlist</h2>
|
||||
|
||||
<h5>1. Name and interval</h5>
|
||||
|
||||
<div style="margin-bottom: 10px;">
|
||||
<div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Title</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" required ng-model="playlist.title" class="input-xlarge tight-form-input">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Interval</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" required ng-model="playlist.interval" placeholder="5m" class="input-xlarge tight-form-input">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<h5>2. Add dashboards</h5>
|
||||
|
||||
<div style="display: inline-block">
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li>
|
||||
<input type="text"
|
||||
class="tight-form-input input-xlarge last"
|
||||
ng-model="searchQuery"
|
||||
placeholder="dashboard search term"
|
||||
ng-trim="true"
|
||||
ng-change="search()">
|
||||
</li>
|
||||
<li class="tight-form-item last" style="padding: 5px 4px">
|
||||
<button ng-click="search()" class="btn btn-mini btn-inverse">Search</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="span5 pull-left">
|
||||
<h5>Search results ({{filteredPlaylistItems.length}})</h5>
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="playlistItem in filteredPlaylistItems">
|
||||
<td style="white-space: nowrap;">
|
||||
{{playlistItem.title}}
|
||||
</td>
|
||||
<td style="text-align: center">
|
||||
<button class="btn btn-inverse btn-mini pull-right" ng-click="addPlaylistItem(playlistItem)">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add to playlist
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="isSearchResultsEmpty() || isSearchQueryEmpty()">
|
||||
<td colspan="2">
|
||||
<i class="fa fa-warning"></i> Search results empty
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="span5 pull-left">
|
||||
<h5>Playlist dashboards</h5>
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="playlistItem in playlistItems">
|
||||
<td style="white-space: nowrap;">
|
||||
{{playlistItem.title}}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
<button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="movePlaylistItemUp(playlistItem)">
|
||||
<i class="fa fa-arrow-up"></i>
|
||||
</button>
|
||||
<button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="movePlaylistItemDown(playlistItem)">
|
||||
<i class="fa fa-arrow-down"></i>
|
||||
</button>
|
||||
<button class="btn btn-inverse btn-mini" ng-click="removePlaylistItem(playlistItem)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="pull-left" style="margin-top: 25px;">
|
||||
<!-- <div class="tight-form"> -->
|
||||
<button type="button"
|
||||
class="btn btn-success"
|
||||
ng-disabled="playlistEditForm.$invalid || isPlaylistEmpty()"
|
||||
ng-click="savePlaylist(playlist, playlistItems)">Save</button>
|
||||
<button type="button"
|
||||
class="btn btn-default"
|
||||
ng-click="backToList()">Cancel</button>
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
48
public/app/features/playlist/partials/playlists.html
Normal file
48
public/app/features/playlist/partials/playlists.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<topnav icon="fa fa-fw fa-list" title="Playlists"></topnav>
|
||||
|
||||
<div class="page-container" style="background: transparent; border: 0;">
|
||||
<div class="page-wide">
|
||||
<h2>Saved playlists</h2>
|
||||
|
||||
<button type="submit" class="btn btn-success pull-right" ng-click="createPlaylist()">
|
||||
<i class="fa fa-plus"></i> New playlist</button>
|
||||
<br />
|
||||
|
||||
<table class="filter-table" style="margin-top: 20px">
|
||||
<thead>
|
||||
<th><strong>Title</strong></th>
|
||||
<th><strong>Url</strong></th>
|
||||
<th style="width: 61px"></th>
|
||||
<th style="width: 61px"></th>
|
||||
<th style="width: 25px"></th>
|
||||
|
||||
</thead>
|
||||
<tr ng-repeat="playlist in playlists">
|
||||
<td >
|
||||
{{playlist.title}}
|
||||
</td>
|
||||
<td >
|
||||
<a href="{{ playlistUrl(playlist) }}">{{ playlistUrl(playlist) }}</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ playlistUrl(playlist) }}" class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-play"></i>
|
||||
Play
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="playlists/edit/{{playlist.id}}" class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a ng-click="removePlaylist(playlist)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
144
public/app/features/playlist/playlist_edit_ctrl.js
Normal file
144
public/app/features/playlist/playlist_edit_ctrl.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
43
public/app/features/playlist/playlist_routes.js
Normal file
43
public/app/features/playlist/playlist_routes.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
51
public/app/features/playlist/playlists_ctrl.js
Normal file
51
public/app/features/playlist/playlists_ctrl.js
Normal file
@@ -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');
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
<div ng-controller="PlaylistCtrl" ng-init="init()">
|
||||
<div class="gf-box-header">
|
||||
<div class="gf-box-title">
|
||||
<i class="fa fa-play"></i>
|
||||
Start dashboard playlist
|
||||
</div>
|
||||
|
||||
<button class="gf-box-header-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-box-body">
|
||||
<div class="row-fluid" style="margin-bottom: 10px;">
|
||||
<div class="span12">
|
||||
<div style="display: inline-block">
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
Search
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="tight-form-input input-xlarge last" ng-model="searchQuery" placeholder="query or empty for starred" ng-change="search()">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<h5>Search result</h5>
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="dashboard in filteredHits">
|
||||
<td style="white-space: nowrap;">
|
||||
{{dashboard.title}}
|
||||
</td>
|
||||
<td style="text-align: center">
|
||||
<button class="btn btn-inverse btn-mini pull-right" ng-click="addDashboard(dashboard)">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add to playlist
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-hide="searchHits.length">
|
||||
<td colspan="2">
|
||||
<i class="fa fa-warning"></i> No dashboards found
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<h5>Playlist dashboards</h5>
|
||||
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="dashboard in playlist">
|
||||
<td style="white-space: nowrap;">
|
||||
{{dashboard.title}}
|
||||
</td>
|
||||
<td style="text-align: center">
|
||||
<button class="btn btn-inverse btn-mini pull-right" ng-click="removeDashboard(dashboard)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-hide="playlist.length">
|
||||
<td colspan="2">
|
||||
Playlist empty
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<div class="pull-left">
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
Timespan between dashboard change
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="tight-form-input input-small" ng-model="timespan" />
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-success tight-form-btn" ng-click="start();dismiss();"><i class="fa fa-play"></i> Start</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,10 +71,6 @@
|
||||
<i class="fa fa-download"></i>
|
||||
Import
|
||||
</a>
|
||||
<button class="btn btn-inverse pull-left" dash-editor-link="app/partials/playlist.html" editor-scope="isolated" ng-click="dismiss();">
|
||||
<i class="fa fa-play"></i>
|
||||
Playlist
|
||||
</button>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user