Commit 9ec3216a by Torkel Ödegaard

Merge branch 'master' of github.com:grafana/grafana

parents 92c2f8d5 eef506d8
...@@ -18,11 +18,11 @@ The Playlist feature can be accessed from Grafana's sidemenu. Click the 'Playlis ...@@ -18,11 +18,11 @@ The Playlist feature can be accessed from Grafana's sidemenu. Click the 'Playlis
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. 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. You can search Dashboards by name (or use a regular expression), and add them to your Playlist. Or you could add tags which will include all the dashboards that belongs to a tag when the playlist start playing. By default, your starred dashboards will appear as candidates for 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. 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.
Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here. Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here.
## Saving the playlist ## Saving the playlist
......
package api package api
import ( import (
"errors"
"strconv"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log" _ "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
) )
...@@ -101,39 +98,6 @@ func LoadPlaylistItems(id int64) ([]m.PlaylistItem, error) { ...@@ -101,39 +98,6 @@ func LoadPlaylistItems(id int64) ([]m.PlaylistItem, error) {
return *itemQuery.Result, nil 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 { func GetPlaylistItems(c *middleware.Context) Response {
id := c.ParamsInt64(":id") id := c.ParamsInt64(":id")
...@@ -147,9 +111,9 @@ func GetPlaylistItems(c *middleware.Context) Response { ...@@ -147,9 +111,9 @@ func GetPlaylistItems(c *middleware.Context) Response {
} }
func GetPlaylistDashboards(c *middleware.Context) Response { func GetPlaylistDashboards(c *middleware.Context) Response {
id := c.ParamsInt64(":id") playlistId := c.ParamsInt64(":id")
playlists, err := LoadPlaylistDashboards(id) playlists, err := LoadPlaylistDashboards(c.OrgId, c.UserId, playlistId)
if err != nil { if err != nil {
return ApiError(500, "Could not load dashboards", err) return ApiError(500, "Could not load dashboards", err)
} }
......
package api
import (
"errors"
"strconv"
"github.com/grafana/grafana/pkg/bus"
_ "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
)
func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, error) {
result := make([]m.PlaylistDashboardDto, 0)
if len(dashboardByIds) > 0 {
dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIds}
if err := bus.Dispatch(&dashboardQuery); err != nil {
return result, errors.New("Playlist not found") //TODO: dont swallow error
}
for _, item := range *dashboardQuery.Result {
result = append(result, m.PlaylistDashboardDto{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
Uri: "db/" + item.Slug,
})
}
}
return result, nil
}
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.PlaylistDashboardDto {
result := make([]m.PlaylistDashboardDto, 0)
if len(dashboardByTag) > 0 {
for _, tag := range dashboardByTag {
searchQuery := search.Query{
Title: "",
Tags: []string{tag},
UserId: userId,
Limit: 100,
IsStarred: false,
OrgId: orgId,
}
if err := bus.Dispatch(&searchQuery); err == nil {
for _, item := range searchQuery.Result {
result = append(result, m.PlaylistDashboardDto{
Id: item.Id,
Title: item.Title,
Uri: item.Uri,
})
}
}
}
}
return result
}
func LoadPlaylistDashboards(orgId, userId, playlistId int64) ([]m.PlaylistDashboardDto, error) {
playlistItems, _ := LoadPlaylistItems(playlistId)
dashboardByIds := make([]int64, 0)
dashboardByTag := make([]string, 0)
for _, i := range playlistItems {
if i.Type == "dashboard_by_id" {
dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
dashboardByIds = append(dashboardByIds, dashboardId)
}
if i.Type == "dashboard_by_tag" {
dashboardByTag = append(dashboardByTag, i.Value)
}
}
result := make([]m.PlaylistDashboardDto, 0)
var k, _ = populateDashboardsById(dashboardByIds)
result = append(result, k...)
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag)...)
return result, nil
}
...@@ -146,3 +146,8 @@ type GetDashboardTagsQuery struct { ...@@ -146,3 +146,8 @@ type GetDashboardTagsQuery struct {
OrgId int64 OrgId int64
Result []*DashboardTagCloudItem Result []*DashboardTagCloudItem
} }
type GetDashboardsQuery struct {
DashboardIds []int64
Result *[]Dashboard
}
...@@ -76,9 +76,7 @@ type UpdatePlaylistCommand struct { ...@@ -76,9 +76,7 @@ type UpdatePlaylistCommand struct {
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
Id int64 `json:"id" binding:"Required"` Id int64 `json:"id" binding:"Required"`
Name string `json:"name" binding:"Required"` Name string `json:"name" binding:"Required"`
Type string `json:"type"`
Interval string `json:"interval"` Interval string `json:"interval"`
Data []int64 `json:"data"`
Items []PlaylistItemDTO `json:"items"` Items []PlaylistItemDTO `json:"items"`
Result *PlaylistDTO Result *PlaylistDTO
...@@ -86,9 +84,7 @@ type UpdatePlaylistCommand struct { ...@@ -86,9 +84,7 @@ type UpdatePlaylistCommand struct {
type CreatePlaylistCommand struct { type CreatePlaylistCommand struct {
Name string `json:"name" binding:"Required"` Name string `json:"name" binding:"Required"`
Type string `json:"type"`
Interval string `json:"interval"` Interval string `json:"interval"`
Data []int64 `json:"data"`
Items []PlaylistItemDTO `json:"items"` Items []PlaylistItemDTO `json:"items"`
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
...@@ -121,8 +117,3 @@ type GetPlaylistItemsByIdQuery struct { ...@@ -121,8 +117,3 @@ type GetPlaylistItemsByIdQuery struct {
PlaylistId int64 PlaylistId int64
Result *[]PlaylistItem Result *[]PlaylistItem
} }
type GetPlaylistDashboardsQuery struct {
DashboardIds []int64
Result *PlaylistDashboards
}
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
func init() { func init() {
bus.AddHandler("sql", SaveDashboard) bus.AddHandler("sql", SaveDashboard)
bus.AddHandler("sql", GetDashboard) bus.AddHandler("sql", GetDashboard)
bus.AddHandler("sql", GetDashboards)
bus.AddHandler("sql", DeleteDashboard) bus.AddHandler("sql", DeleteDashboard)
bus.AddHandler("sql", SearchDashboards) bus.AddHandler("sql", SearchDashboards)
bus.AddHandler("sql", GetDashboardTags) bus.AddHandler("sql", GetDashboardTags)
...@@ -223,3 +224,20 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { ...@@ -223,3 +224,20 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
return nil return nil
}) })
} }
func GetDashboards(query *m.GetDashboardsQuery) error {
if len(query.DashboardIds) == 0 {
return m.ErrCommandValidationFailed
}
var dashboards = make([]m.Dashboard, 0)
err := x.In("id", query.DashboardIds).Find(&dashboards)
query.Result = &dashboards
if err != nil {
return err
}
return nil
}
...@@ -15,7 +15,6 @@ func init() { ...@@ -15,7 +15,6 @@ func init() {
bus.AddHandler("sql", DeletePlaylist) bus.AddHandler("sql", DeletePlaylist)
bus.AddHandler("sql", SearchPlaylists) bus.AddHandler("sql", SearchPlaylists)
bus.AddHandler("sql", GetPlaylist) bus.AddHandler("sql", GetPlaylist)
bus.AddHandler("sql", GetPlaylistDashboards)
bus.AddHandler("sql", GetPlaylistItem) bus.AddHandler("sql", GetPlaylistItem)
} }
...@@ -162,20 +161,3 @@ func GetPlaylistItem(query *m.GetPlaylistItemsByIdQuery) error { ...@@ -162,20 +161,3 @@ func GetPlaylistItem(query *m.GetPlaylistItemsByIdQuery) error {
return err 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
}
package sqlstore
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
)
func TestPlaylistDataAccess(t *testing.T) {
Convey("Testing Playlist data access", t, func() {
InitTestDB(t)
Convey("Can create playlist", func() {
items := []m.PlaylistItemDTO{
{Title: "graphite", Value: "graphite", Type: "dashboard_by_tag"},
{Title: "Backend response times", Value: "3", Type: "dashboard_by_id"},
}
cmd := m.CreatePlaylistCommand{Name: "NYC office", Interval: "10m", OrgId: 1, Items: items}
err := CreatePlaylist(&cmd)
So(err, ShouldBeNil)
Convey("can update playlist", func() {
items := []m.PlaylistItemDTO{
{Title: "influxdb", Value: "influxdb", Type: "dashboard_by_tag"},
{Title: "Backend response times", Value: "2", Type: "dashboard_by_id"},
}
query := m.UpdatePlaylistCommand{Name: "NYC office ", OrgId: 1, Id: 1, Interval: "10s", Items: items}
err = UpdatePlaylist(&query)
So(err, ShouldBeNil)
Convey("can remove playlist", func() {
query := m.DeletePlaylistCommand{Id: 1}
err = DeletePlaylist(&query)
So(err, ShouldBeNil)
})
})
})
})
}
...@@ -24,8 +24,16 @@ ...@@ -24,8 +24,16 @@
<i class="fa" ng-class="{'fa-star-o': !dashboardMeta.isStarred, 'fa-star': dashboardMeta.isStarred}" style="color: orange;"></i> <i class="fa" ng-class="{'fa-star-o': !dashboardMeta.isStarred, 'fa-star': dashboardMeta.isStarred}" style="color: orange;"></i>
</a> </a>
</li> </li>
<li ng-show="dashboardMeta.canShare"> <li ng-show="dashboardMeta.canShare" class="dropdown">
<a class="pointer" ng-click="shareDashboard()" bs-tooltip="'Share dashboard'" data-placement="bottom"><i class="fa fa-share-square-o"></i></a> <a class="pointer" ng-click="hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
<ul class="dropdown-menu">
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="shareDashboard(0)">
<i class="fa fa-link"></i>
Link to Dashboard</a></li>
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="shareDashboard(1)">
<i class="gf-icon gf-icon-snap-multi"></i>
Snapshot sharing</a></li>
</ul>
</li> </li>
<li ng-show="dashboardMeta.canSave"> <li ng-show="dashboardMeta.canSave">
<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a> <a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a>
......
...@@ -42,10 +42,13 @@ export class DashNavCtrl { ...@@ -42,10 +42,13 @@ export class DashNavCtrl {
} }
}; };
$scope.shareDashboard = function() { $scope.shareDashboard = function(tabIndex) {
var modalScope = $scope.$new();
modalScope.tabIndex = tabIndex;
$scope.appEvent('show-modal', { $scope.appEvent('show-modal', {
src: './app/features/dashboard/partials/shareModal.html', src: './app/features/dashboard/partials/shareModal.html',
scope: $scope.$new(), scope: modalScope
}); });
}; };
......
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
<script type="text/ng-template" id="shareLink.html"> <script type="text/ng-template" id="shareLink.html">
<div class="share-modal-big-icon"> <div class="share-modal-big-icon">
<i class="fa fa-external-link"></i> <i class="fa fa-link"></i>
</div> </div>
<div ng-include src="'shareLinkOptions.html'"></div> <div ng-include src="'shareLinkOptions.html'"></div>
......
...@@ -12,7 +12,7 @@ function (angular, _, require, config) { ...@@ -12,7 +12,7 @@ function (angular, _, require, config) {
module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv, linkSrv) { module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv, linkSrv) {
$scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' }; $scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
$scope.editor = { index: 0 }; $scope.editor = { index: $scope.tabIndex || 0};
$scope.init = function() { $scope.init = function() {
$scope.modeSharePanel = $scope.panel ? true : false; $scope.modeSharePanel = $scope.panel ? true : false;
......
...@@ -123,7 +123,7 @@ function (angular, $, _) { ...@@ -123,7 +123,7 @@ function (angular, $, _) {
} }
var menuTemplate; var menuTemplate;
if ($(e.target).hasClass('fa-external-link')) { if ($(e.target).hasClass('fa-link')) {
menuTemplate = createExternalLinkMenu($scope); menuTemplate = createExternalLinkMenu($scope);
} else { } else {
menuTemplate = createMenuTemplate($scope); menuTemplate = createMenuTemplate($scope);
......
define([ define([
'./playlists_ctrl', './playlists_ctrl',
'./playlist_search',
'./playlist_srv', './playlist_srv',
'./playlist_edit_ctrl', './playlist_edit_ctrl',
'./playlist_routes' './playlist_routes'
......
<navbar title="Playlists" title-url="playlists" icon="fa fa-fw fa-list" subnav="true"> <navbar title="Playlists" title-url="playlists" icon="fa fa-fw fa-list" subnav="true">
<ul class="nav"> <ul class="nav">
<li ng-class="{active: isNew()}" ng-show="isNew()"><a href="datasources/create">New</a></li> <li ng-class="{active: ctrl.isNew()}" ng-show="ctrl.isNew()"><a href="datasources/create">New</a></li>
<li class="active" ng-show="!isNew()"><a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a></li> <li class="active" ng-show="!ctrl.isNew()"><a href="playlists/edit/{{ctrl.playlist.id}}">{{ctrl.playlist.name}}</a></li>
</ul> </ul>
</navbar> </navbar>
<div class="page-container" ng-form="playlistEditForm"> <div class="page-container" ng-form="playlistEditForm">
<div class="page"> <div class="page">
<h2 ng-show="isNew()">New playlist</h2> <h2 ng-show="ctrl.isNew()">New playlist</h2>
<h2 ng-show="!isNew()">Edit playlist</h2> <h2 ng-show="!ctrl.isNew()">Edit playlist</h2>
<h4>Name and interval</h4> <h4>Name and interval</h4>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
Name Name
</li> </li>
<li> <li>
<input type="text" required ng-model="playlist.name" class="input-xlarge tight-form-input"> <input type="text" required ng-model="ctrl.playlist.name" class="input-xlarge tight-form-input">
</li> </li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
Interval Interval
</li> </li>
<li> <li>
<input type="text" required ng-model="playlist.interval" placeholder="5m" class="input-xlarge tight-form-input"> <input type="text" required ng-model="ctrl.playlist.interval" placeholder="5m" class="input-xlarge tight-form-input">
</li> </li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
...@@ -39,66 +39,72 @@ ...@@ -39,66 +39,72 @@
</div> </div>
<br> <br>
<h4>Add dashboards</h4>
<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="dashboard search term"
ng-trim="true"
ng-change="search()">
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="span5 pull-left"> <div class="span5 pull-left">
<h5>Search results ({{filteredPlaylistItems.length}})</h5> <h5>Add dashboards</h5>
<div style="">
<playlist-search class="playlist-search-container" search-started="ctrl.searchStarted(promise)"></playlist-search>
</div>
</div>
</div>
<div class="row">
<div class="span5 pull-left" ng-if="ctrl.filteredDashboards.length > 0">
<h5>Search results ({{ctrl.filteredDashboards.length}})</h5>
<table class="grafana-options-table"> <table class="grafana-options-table">
<tr ng-repeat="playlistItem in filteredPlaylistItems"> <tr ng-repeat="playlistItem in ctrl.filteredDashboards">
<td style="white-space: nowrap;"> <td style="white-space: nowrap;">
{{playlistItem.title}} {{playlistItem.title}}
</td> </td>
<td style="text-align: center"> <td style="text-align: center">
<button class="btn btn-inverse btn-mini pull-right" ng-click="addPlaylistItem(playlistItem)"> <button class="btn btn-inverse btn-mini pull-right" ng-click="ctrl.addPlaylistItem(playlistItem)">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
Add to playlist Add to playlist
</button> </button>
</td> </td>
</tr> </tr>
<tr ng-if="isSearchResultsEmpty()">
<td colspan="2">
<i class="fa fa-warning"></i> Search results empty
</td>
</tr>
</table> </table>
</div> </div>
<div class="playlist-search-results-container" ng-if="ctrl.filteredTags.length > 0">
<div class="row">
<div class="span6 offset1">
<div ng-repeat="tag in ctrl.filteredTags" class="pointer" style="width: 180px; float: left;"
ng-class="{'selected': $index === selectedIndex }"
ng-click="ctrl.addTagPlaylistItem(tag, $event)">
<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
<i class="fa fa-tag"></i>
<span>{{tag.term}} &nbsp;({{tag.count}})</span>
</a>
</div>
</div>
</div>
</div>
<div class="span5 pull-left"> <div class="span5 pull-left">
<h5>Added dashboards</h5> <h5>Added dashboards</h5>
<table class="grafana-options-table"> <table class="grafana-options-table">
<tr ng-repeat="playlistItem in playlistItems"> <tr ng-repeat="playlistItem in ctrl.playlistItems">
<td style="white-space: nowrap;"> <td style="white-space: nowrap;" ng-if="playlistItem.type === 'dashboard_by_id'">
{{playlistItem.title}} {{playlistItem.title}}
</td> </td>
<td style="white-space: nowrap;" ng-if="playlistItem.type === 'dashboard_by_tag'">
<a class="search-result-tag label label-tag" tag-color-from-name="playlistItem.title">
<i class="fa fa-tag"></i>
<span>{{playlistItem.title}}</span>
</a>
</td>
<td style="text-align: right"> <td style="text-align: right">
<button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="movePlaylistItemUp(playlistItem)"> <button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="ctrl.movePlaylistItemUp(playlistItem)">
<i class="fa fa-arrow-up"></i> <i class="fa fa-arrow-up"></i>
</button> </button>
<button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="movePlaylistItemDown(playlistItem)"> <button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="ctrl.movePlaylistItemDown(playlistItem)">
<i class="fa fa-arrow-down"></i> <i class="fa fa-arrow-down"></i>
</button> </button>
<button class="btn btn-inverse btn-mini" ng-click="removePlaylistItem(playlistItem)"> <button class="btn btn-inverse btn-mini" ng-click="ctrl.removePlaylistItem(playlistItem)">
<i class="fa fa-remove"></i> <i class="fa fa-remove"></i>
</button> </button>
</td> </td>
...@@ -113,11 +119,11 @@ ...@@ -113,11 +119,11 @@
<!-- <div class="tight-form"> --> <!-- <div class="tight-form"> -->
<button type="button" <button type="button"
class="btn btn-success" class="btn btn-success"
ng-disabled="playlistEditForm.$invalid || isPlaylistEmpty()" ng-disabled="ctrl.playlistEditForm.$invalid || ctrl.isPlaylistEmpty()"
ng-click="savePlaylist(playlist, playlistItems)">Save</button> ng-click="ctrl.savePlaylist(ctrl.playlist, ctrl.playlistItems)">Save</button>
<button type="button" <button type="button"
class="btn btn-inverse" class="btn btn-inverse"
ng-click="backToList()">Cancel</button> ng-click="ctrl.backToList()">Cancel</button>
<!-- </div> --> <!-- </div> -->
</div> </div>
......
<div class="playlist-search-field-wrapper">
<span style="position: relative;">
<input type="text" placeholder="Find dashboards by name" tabindex="1"
ng-keydown="ctrl.keyDown($event)" ng-model="ctrl.query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.searchDashboards()" />
</span>
<div class="playlist-search-switches">
<i class="fa fa-filter"></i>
<a class="pointer" href="javascript:void 0;" ng-click="ctrl.showStarred()" tabindex="2">
<i class="fa fa-remove" ng-show="ctrl.query.starred"></i>
starred
</a> |
<a class="pointer" href="javascript:void 0;" ng-click="ctrl.getTags()" tabindex="3">
<i class="fa fa-remove" ng-show="ctrl.tagsMode"></i>
tags
</a>
<span ng-if="ctrl.query.tag.length">
|
<span ng-repeat="tagName in ctrl.query.tag">
<a ng-click="ctrl.removeTag(tagName, $event)" tag-color-from-name="ctrl.tagName" class="label label-tag">
<i class="fa fa-remove"></i>
{{tagName}}
</a>
</span>
</span>
</div>
</div>
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
<th style="width: 25px"></th> <th style="width: 25px"></th>
</thead> </thead>
<tr ng-repeat="playlist in playlists"> <tr ng-repeat="playlist in ctrl.playlists">
<td> <td>
<a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a> <a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a>
</td> </td>
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
</a> </a>
</td> </td>
<td class="text-right"> <td class="text-right">
<a ng-click="removePlaylist(playlist)" class="btn btn-danger btn-mini"> <a ng-click="ctrl.removePlaylist(playlist)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i> <i class="fa fa-remove"></i>
</a> </a>
</td> </td>
......
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 = {
interval: '10m',
};
$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 = {limit: 10};
if ($scope.searchQuery) {
query.query = $scope.searchQuery;
}
$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.movePlaylistItem(playlistItem, -1);
};
$scope.movePlaylistItemDown = function(playlistItem) {
$scope.movePlaylistItem(playlistItem, 1);
};
$scope.init();
});
});
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import coreModule from '../../core/core_module';
import config from 'app/core/config';
export class PlaylistEditCtrl {
filteredDashboards: any = [];
filteredTags: any = [];
searchQuery: string = '';
loading: boolean = false;
playlist: any = {
interval: '10m',
};
playlistItems: any = [];
dashboardresult: any = [];
tagresult: any = [];
/** @ngInject */
constructor(private $scope, private playlistSrv, private backendSrv, private $location, private $route) {
if ($route.current.params.id) {
var playlistId = $route.current.params.id;
backendSrv.get('/api/playlists/' + playlistId)
.then((result) => {
this.playlist = result;
});
backendSrv.get('/api/playlists/' + playlistId + '/items')
.then((result) => {
this.playlistItems = result;
});
}
}
filterFoundPlaylistItems() {
this.filteredDashboards = _.reject(this.dashboardresult, (playlistItem) => {
return _.findWhere(this.playlistItems, (listPlaylistItem) => {
return parseInt(listPlaylistItem.value) === playlistItem.id;
});
});
this.filteredTags = _.reject(this.tagresult, (tag) => {
return _.findWhere(this.playlistItems, (listPlaylistItem) => {
return listPlaylistItem.value === tag.term;
});
});
}
addPlaylistItem(playlistItem) {
playlistItem.value = playlistItem.id.toString();
playlistItem.type = 'dashboard_by_id';
playlistItem.order = this.playlistItems.length + 1;
this.playlistItems.push(playlistItem);
this.filterFoundPlaylistItems();
}
addTagPlaylistItem(tag) {
var playlistItem: any = {
value: tag.term,
type: 'dashboard_by_tag',
order: this.playlistItems.length + 1,
title: tag.term
};
this.playlistItems.push(playlistItem);
this.filterFoundPlaylistItems();
}
removePlaylistItem(playlistItem) {
_.remove(this.playlistItems, (listedPlaylistItem) => {
return playlistItem === listedPlaylistItem;
});
this.filterFoundPlaylistItems();
};
savePlaylist(playlist, playlistItems) {
var savePromise;
playlist.items = playlistItems;
savePromise = playlist.id
? this.backendSrv.put('/api/playlists/' + playlist.id, playlist)
: this.backendSrv.post('/api/playlists', playlist);
savePromise
.then(() => {
this.$scope.appEvent('alert-success', ['Playlist saved', '']);
this.$location.path('/playlists');
}, () => {
this.$scope.appEvent('alert-error', ['Unable to save playlist', '']);
});
}
isNew() {
return !this.playlist.id;
}
isPlaylistEmpty() {
return !this.playlistItems.length;
}
backToList() {
this.$location.path('/playlists');
}
searchStarted(promise) {
promise.then((data) => {
this.dashboardresult = data.dashboardResult;
this.tagresult = data.tagResult;
this.filterFoundPlaylistItems();
});
}
movePlaylistItem(playlistItem, offset) {
var currentPosition = this.playlistItems.indexOf(playlistItem);
var newPosition = currentPosition + offset;
if (newPosition >= 0 && newPosition < this.playlistItems.length) {
this.playlistItems.splice(currentPosition, 1);
this.playlistItems.splice(newPosition, 0, playlistItem);
}
}
movePlaylistItemUp(playlistItem) {
this.movePlaylistItem(playlistItem, -1);
}
movePlaylistItemDown(playlistItem) {
this.movePlaylistItem(playlistItem, 1);
}
}
coreModule.controller('PlaylistEditCtrl', PlaylistEditCtrl);
define([ define([
'angular', 'angular',
'app/core/config', 'app/core/config',
'lodash' 'lodash'
...@@ -12,14 +12,17 @@ function (angular) { ...@@ -12,14 +12,17 @@ function (angular) {
$routeProvider $routeProvider
.when('/playlists', { .when('/playlists', {
templateUrl: 'app/features/playlist/partials/playlists.html', templateUrl: 'app/features/playlist/partials/playlists.html',
controllerAs: 'ctrl',
controller : 'PlaylistsCtrl' controller : 'PlaylistsCtrl'
}) })
.when('/playlists/create', { .when('/playlists/create', {
templateUrl: 'app/features/playlist/partials/playlist.html', templateUrl: 'app/features/playlist/partials/playlist.html',
controllerAs: 'ctrl',
controller : 'PlaylistEditCtrl' controller : 'PlaylistEditCtrl'
}) })
.when('/playlists/edit/:id', { .when('/playlists/edit/:id', {
templateUrl: 'app/features/playlist/partials/playlist.html', templateUrl: 'app/features/playlist/partials/playlist.html',
controllerAs: 'ctrl',
controller : 'PlaylistEditCtrl' controller : 'PlaylistEditCtrl'
}) })
.when('/playlists/play/:id', { .when('/playlists/play/:id', {
......
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import config from 'app/core/config';
import _ from 'lodash';
import $ from 'jquery';
import coreModule from '../../core/core_module';
export class PlaylistSearchCtrl {
query: any;
tagsMode: boolean;
searchStarted: any;
/** @ngInject */
constructor(private $scope, private $location, private $timeout, private backendSrv, private contextSrv) {
this.query = { query: '', tag: [], starred: false };
$timeout(() => {
this.query.query = '';
this.searchDashboards();
}, 100);
}
searchDashboards() {
this.tagsMode = false;
var prom: any = {};
prom.promise = this.backendSrv.search(this.query).then((result) => {
return {
dashboardResult: result,
tagResult: []
};
});
this.searchStarted(prom);
}
showStarred() {
this.query.starred = !this.query.starred;
this.searchDashboards();
}
queryHasNoFilters() {
return this.query.query === '' && this.query.starred === false && this.query.tag.length === 0;
}
filterByTag(tag, evt) {
this.query.tag.push(tag);
this.searchDashboards();
if (evt) {
evt.stopPropagation();
evt.preventDefault();
}
}
getTags() {
var prom: any = {};
prom.promise = this.backendSrv.get('/api/dashboards/tags').then((result) => {
return {
dashboardResult: [],
tagResult: result
};
});
this.searchStarted(prom);
}
}
export function playlistSearchDirective() {
return {
restrict: 'E',
templateUrl: 'app/features/playlist/partials/playlist_search.html',
controller: PlaylistSearchCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
searchStarted: '&'
},
};
}
coreModule.directive('playlistSearch', playlistSearchDirective);
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.removePlaylistConfirmed = function(playlist) {
_.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.removePlaylist = function(playlist) {
$scope.appEvent('confirm-modal', {
title: 'Confirm delete playlist',
text: 'Are you sure you want to delete playlist ' + playlist.name + '?',
yesText: "Delete",
icon: "fa-warning",
onConfirm: function() {
$scope.removePlaylistConfirmed(playlist);
}
});
};
});
});
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import coreModule from '../../core/core_module';
export class PlaylistsCtrl {
playlists: any;
/** @ngInject */
constructor(private $scope, private $location, private backendSrv) {
backendSrv.get('/api/playlists')
.then((result) => {
this.playlists = result;
});
}
removePlaylistConfirmed(playlist) {
_.remove(this.playlists, { id: playlist.id });
this.backendSrv.delete('/api/playlists/' + playlist.id)
.then(() => {
this.$scope.appEvent('alert-success', ['Playlist deleted', '']);
}, () => {
this.$scope.appEvent('alert-error', ['Unable to delete playlist', '']);
this.playlists.push(playlist);
});
}
removePlaylist(playlist) {
this.$scope.appEvent('confirm-modal', {
title: 'Confirm delete playlist',
text: 'Are you sure you want to delete playlist ' + playlist.name + '?',
yesText: "Delete",
icon: "fa-warning",
onConfirm: () => {
this.removePlaylistConfirmed(playlist);
}
});
}
}
coreModule.controller('PlaylistsCtrl', PlaylistsCtrl);
import '../playlist_edit_ctrl';
import {describe, beforeEach, it, 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);
});
});
});
});
});
import '../playlist_edit_ctrl';
import {describe, beforeEach, it, expect} from 'test/lib/common';
import {PlaylistEditCtrl} from '../playlist_edit_ctrl';
describe('PlaylistEditCtrl', () => {
var ctx: any;
beforeEach(() => {
ctx = new PlaylistEditCtrl(null, null, null, null, { current: { params: {} } });
ctx.dashboardresult = [
{ id: 2, title: 'dashboard: 2' },
{ id: 3, title: 'dashboard: 3' }
];
ctx.tagresult = [
{ term: 'graphite', count: 1 },
{ term: 'nyc', count: 2 }
];
});
describe('searchresult returns 2 dashboards, ', () => {
it('found dashboard should be 2', () => {
expect(ctx.dashboardresult.length).to.be(2);
});
it('filtred result should be 2', () => {
ctx.filterFoundPlaylistItems();
expect(ctx.filteredDashboards.length).to.be(2);
expect(ctx.filteredTags.length).to.be(2);
});
describe('adds one dashboard to playlist, ', () => {
beforeEach(() => {
ctx.addPlaylistItem({ id: 2, title: 'dashboard: 2' });
ctx.addTagPlaylistItem({ term: 'graphite' });
ctx.filterFoundPlaylistItems();
});
it('playlistitems should be increased by one', () => {
expect(ctx.playlistItems.length).to.be(2);
});
it('filtred playlistitems should be reduced by one', () => {
expect(ctx.filteredDashboards.length).to.be(1);
expect(ctx.filteredTags.length).to.be(1);
});
it('found dashboard should be 2', () => {
expect(ctx.dashboardresult.length).to.be(2);
});
describe('removes one dashboard from playlist, ', () => {
beforeEach(() => {
ctx.removePlaylistItem(ctx.playlistItems[0]);
ctx.removePlaylistItem(ctx.playlistItems[0]);
ctx.filterFoundPlaylistItems();
});
it('playlistitems should be increased by one', () => {
expect(ctx.playlistItems.length).to.be(0);
});
it('found dashboard should be 2', () => {
expect(ctx.dashboardresult.length).to.be(2);
expect(ctx.filteredDashboards.length).to.be(2);
expect(ctx.filteredTags.length).to.be(2);
expect(ctx.tagresult.length).to.be(2);
});
});
});
});
});
...@@ -276,7 +276,7 @@ function (angular, _, dateMath) { ...@@ -276,7 +276,7 @@ function (angular, _, dateMath) {
query.downsample = interval + "-" + target.downsampleAggregator; query.downsample = interval + "-" + target.downsampleAggregator;
if (target.downsampleFillPolicy !== "none") { if (target.downsampleFillPolicy && target.downsampleFillPolicy !== "none") {
query.downsample += "-" + target.downsampleFillPolicy; query.downsample += "-" + target.downsampleFillPolicy;
} }
} }
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
@import "bootstrap-tagsinput.less"; @import "bootstrap-tagsinput.less";
@import "tables_lists.less"; @import "tables_lists.less";
@import "search.less"; @import "search.less";
@import "playlist.less";
@import "panel.less"; @import "panel.less";
@import "forms.less"; @import "forms.less";
@import "tightform.less"; @import "tightform.less";
......
.playlist-search-container {
margin: 15px;
z-index: 1000;
position: relative;
width: 700px;
box-shadow: 0px 0px 55px 0px black;
background-color: @grafanaPanelBackground;
.label-tag {
margin-left: 6px;
font-size: 11px;
padding: 2px 6px;
}
}
.playlist-search-switches {
position: relative;
top: -39px;
left: 260px;
}
.playlist-search-field-wrapper {
input {
width: 100%;
padding: 8px 8px;
height: 100%;
box-sizing: border-box;
}
button {
margin: 0 4px 0 0;
}
> span {
display: block;
overflow: hidden;
}
}
.playlist-search-results-container {
min-height: 100px;
overflow: auto;
display: block;
line-height: 28px;
.search-item:hover, .search-item.selected {
background-color: @grafanaListHighlight;
}
.selected {
.search-result-tag {
opacity: 0.70;
color: white;
}
}
.fa-star, .fa-star-o {
padding-left: 13px;
}
.fa-star {
color: @orange;
}
.search-result-link {
color: @grafanaListMainLinkColor;
.fa {
padding-right: 10px;
}
}
.search-item {
display: block;
padding: 3px 10px;
white-space: nowrap;
background-color: @grafanaListBackground;
margin-bottom: 4px;
.search-result-icon:before {
content: "\f009";
}
&.search-item-dash-home .search-result-icon:before {
content: "\f015";
}
}
.search-result-tags {
float: right;
}
.search-result-actions {
float: right;
padding-left: 20px;
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment