mirror of https://github.com/grafana/grafana
Merge pull request #3655 from grafana/playlist
Persistable playlists closes #515 closes #1137pull/3712/head
commit
f7fecdc6de
@ -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) |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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)) |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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); |
||||
}; |
||||
|
||||
}); |
||||
|
||||
}); |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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(); |
||||
}); |
||||
}); |
||||
@ -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); |
||||
}); |
||||
}); |
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
}); |
||||
@ -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> |
||||
Loading…
Reference in new issue