Pārlūkot izejas kodu

Resurrected nikita-graf's work and added playlistType for future use

utkarshcmu 10 gadi atpakaļ
vecāks
revīzija
bcaaedf2ff

+ 13 - 0
pkg/api/api.go

@@ -47,6 +47,9 @@ func Register(r *macaron.Macaron) {
 	r.Get("/dashboard/*", reqSignedIn, Index)
 	r.Get("/dashboard-solo/*", reqSignedIn, Index)
 
+	r.Get("/playlists/", reqSignedIn, Index)
+	r.Get("/playlists/*", reqSignedIn, Index)
+
 	// sign up
 	r.Get("/signup", Index)
 	r.Get("/api/user/signup/options", wrap(GetSignUpOptions))
@@ -169,6 +172,16 @@ func Register(r *macaron.Macaron) {
 			r.Get("/tags", GetDashboardTags)
 		})
 
+		// Playlist
+		r.Group("/playlists", func() {
+			r.Get("/", SearchPlaylists)
+			r.Get("/:id", ValidateOrgPlaylist, GetPlaylist)
+			r.Get("/:id/dashboards", ValidateOrgPlaylist, GetPlaylistDashboards)
+			r.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, DeletePlaylist)
+			r.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistQuery{}), ValidateOrgPlaylist, UpdatePlaylist)
+			r.Post("/", reqEditorRole, bind(m.CreatePlaylistQuery{}), CreatePlaylist)
+		})
+
 		// Search
 		r.Get("/search/", Search)
 

+ 6 - 0
pkg/api/index.go

@@ -53,6 +53,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 		Href: "/",
 	})
 
+	data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
+		Text: "Playlists",
+		Icon: "fa fa-fw fa-list",
+		Href: "/playlists",
+	})
+
 	if c.OrgRole == m.ROLE_ADMIN {
 		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
 			Text: "Data Sources",

+ 103 - 0
pkg/api/playlist.go

@@ -0,0 +1,103 @@
+package api
+
+import (
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/middleware"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func ValidateOrgPlaylist(c *middleware.Context) {
+	id := c.ParamsInt64(":id")
+	query := m.GetPlaylistByIdQuery{Id: id}
+	err := bus.Dispatch(&query)
+
+	if err != nil {
+		c.JsonApiErr(404, "Playlist not found", err)
+		return
+	}
+
+	if query.Result.OrgId != c.OrgId {
+		c.JsonApiErr(403, "You are not allowed to edit/view playlist", nil)
+		return
+	}
+}
+
+func SearchPlaylists(c *middleware.Context) {
+	query := c.Query("query")
+	limit := c.QueryInt("limit")
+
+	if limit == 0 {
+		limit = 1000
+	}
+
+	searchQuery := m.PlaylistQuery{
+		Title: query,
+		Limit: limit,
+		OrgId: c.OrgId,
+	}
+
+	err := bus.Dispatch(&searchQuery)
+	if err != nil {
+		c.JsonApiErr(500, "Search failed", err)
+		return
+	}
+
+	c.JSON(200, searchQuery.Result)
+}
+
+func GetPlaylist(c *middleware.Context) {
+	id := c.ParamsInt64(":id")
+	cmd := m.GetPlaylistByIdQuery{Id: id}
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		c.JsonApiErr(500, "Playlist not found", err)
+		return
+	}
+
+	c.JSON(200, cmd.Result)
+}
+
+func GetPlaylistDashboards(c *middleware.Context) {
+	id := c.ParamsInt64(":id")
+
+	query := m.GetPlaylistDashboardsQuery{Id: id}
+	if err := bus.Dispatch(&query); err != nil {
+		c.JsonApiErr(500, "Playlist not found", err)
+		return
+	}
+
+	c.JSON(200, query.Result)
+}
+
+func DeletePlaylist(c *middleware.Context) {
+	id := c.ParamsInt64(":id")
+
+	cmd := m.DeletePlaylistQuery{Id: id}
+	if err := bus.Dispatch(&cmd); err != nil {
+		c.JsonApiErr(500, "Failed to delete playlist", err)
+		return
+	}
+
+	c.JSON(200, "")
+}
+
+func CreatePlaylist(c *middleware.Context, query m.CreatePlaylistQuery) {
+	query.OrgId = c.OrgId
+	err := bus.Dispatch(&query)
+	if err != nil {
+		c.JsonApiErr(500, "Failed to create playlist", err)
+		return
+	}
+
+	c.JSON(200, query.Result)
+}
+
+func UpdatePlaylist(c *middleware.Context, query m.UpdatePlaylistQuery) {
+	err := bus.Dispatch(&query)
+	if err != nil {
+		c.JsonApiErr(500, "Failed to save playlist", err)
+		return
+	}
+
+	c.JSON(200, query.Result)
+}

+ 79 - 0
pkg/models/playlist.go

@@ -0,0 +1,79 @@
+package models
+
+import (
+	"errors"
+)
+
+// Typed errors
+var (
+	ErrPlaylistNotFound           = errors.New("Playlist not found")
+	ErrPlaylistWithSameNameExists = errors.New("A playlist with the same name already exists")
+)
+
+// Playlist model
+type Playlist struct {
+	Id       int64  `json:"id"`
+	Title    string `json:"title"`
+	Type     string `json:"type"`
+	Timespan string `json:"timespan"`
+	Data     []int  `json:"data"`
+	OrgId    int64  `json:"-"`
+}
+
+type PlaylistDashboard struct {
+	Id    int64  `json:"id"`
+	Slug  string `json:"slug"`
+	Title string `json:"title"`
+}
+
+func (this PlaylistDashboard) TableName() string {
+	return "dashboard"
+}
+
+type Playlists []*Playlist
+type PlaylistDashboards []*PlaylistDashboard
+
+//
+// COMMANDS
+//
+type PlaylistQuery struct {
+	Title string
+	Limit int
+	OrgId int64
+
+	Result Playlists
+}
+
+type UpdatePlaylistQuery struct {
+	Id       int64
+	Title    string
+	Type     string
+	Timespan string
+	Data     []int
+
+	Result *Playlist
+}
+
+type CreatePlaylistQuery struct {
+	Title    string
+	Type     string
+	Timespan string
+	Data     []int
+	OrgId    int64
+
+	Result *Playlist
+}
+
+type GetPlaylistByIdQuery struct {
+	Id     int64
+	Result *Playlist
+}
+
+type GetPlaylistDashboardsQuery struct {
+	Id     int64
+	Result *PlaylistDashboards
+}
+
+type DeletePlaylistQuery struct {
+	Id int64
+}

+ 1 - 0
pkg/services/sqlstore/migrations/migrations.go

@@ -19,6 +19,7 @@ func AddMigrations(mg *Migrator) {
 	addDashboardSnapshotMigrations(mg)
 	addQuotaMigration(mg)
 	addPluginBundleMigration(mg)
+	addPlaylistMigrations(mg)
 }
 
 func addMigrationLogMigrations(mg *Migrator) {

+ 20 - 0
pkg/services/sqlstore/migrations/playlist_mig.go

@@ -0,0 +1,20 @@
+package migrations
+
+import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+
+func addPlaylistMigrations(mg *Migrator) {
+	playlistV1 := Table{
+		Name: "playlist",
+		Columns: []*Column{
+			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "data", Type: DB_Text, Nullable: false},
+			{Name: "timespan", Type: DB_NVarchar, Length: 255, Nullable: false},
+			{Name: "org_id", Type: DB_BigInt, Nullable: false},
+		},
+	}
+
+	// create table
+	mg.AddMigration("create playlist table v1", NewAddTableMigration(playlistV1))
+}

+ 125 - 0
pkg/services/sqlstore/playlist.go

@@ -0,0 +1,125 @@
+package sqlstore
+
+import (
+	"github.com/go-xorm/xorm"
+
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func init() {
+	bus.AddHandler("sql", CreatePlaylist)
+	bus.AddHandler("sql", UpdatePlaylist)
+	bus.AddHandler("sql", DeletePlaylist)
+	bus.AddHandler("sql", SearchPlaylists)
+	bus.AddHandler("sql", GetPlaylist)
+	bus.AddHandler("sql", GetPlaylistDashboards)
+}
+
+func CreatePlaylist(query *m.CreatePlaylistQuery) error {
+	var err error
+
+	playlist := m.Playlist{
+		Title:    query.Title,
+		Type:     query.Type,
+		Data:     query.Data,
+		Timespan: query.Timespan,
+		OrgId:    query.OrgId,
+	}
+
+	_, err = x.Insert(&playlist)
+
+	query.Result = &playlist
+	return err
+}
+
+func UpdatePlaylist(query *m.UpdatePlaylistQuery) error {
+	var err error
+	x.Logger.SetLevel(5)
+	playlist := m.Playlist{
+		Id:       query.Id,
+		Title:    query.Title,
+		Type:     query.Type,
+		Data:     query.Data,
+		Timespan: query.Timespan,
+	}
+
+	existingPlaylist := x.Where("id = ?", query.Id).Find(m.Playlist{})
+
+	if existingPlaylist == nil {
+		return m.ErrPlaylistNotFound
+	}
+
+	_, err = x.Id(query.Id).Cols("id", "title", "data", "timespan").Update(&playlist)
+
+	query.Result = &playlist
+	return err
+}
+
+func GetPlaylist(query *m.GetPlaylistByIdQuery) error {
+	if query.Id == 0 {
+		return m.ErrCommandValidationFailed
+	}
+
+	playlist := m.Playlist{}
+	_, err := x.Id(query.Id).Get(&playlist)
+	query.Result = &playlist
+
+	return err
+}
+
+func DeletePlaylist(query *m.DeletePlaylistQuery) error {
+	if query.Id == 0 {
+		return m.ErrCommandValidationFailed
+	}
+
+	return inTransaction(func(sess *xorm.Session) error {
+		var rawSql = "DELETE FROM playlist WHERE id = ?"
+		_, err := sess.Exec(rawSql, query.Id)
+		return err
+	})
+}
+
+func SearchPlaylists(query *m.PlaylistQuery) error {
+	var playlists = make(m.Playlists, 0)
+
+	sess := x.Limit(query.Limit)
+
+	if query.Title != "" {
+		sess.Where("title LIKE ?", query.Title)
+	}
+
+	sess.Where("org_id = ?", query.OrgId)
+	err := sess.Find(&playlists)
+	query.Result = playlists
+
+	return err
+}
+
+func GetPlaylistDashboards(query *m.GetPlaylistDashboardsQuery) error {
+	if query.Id == 0 {
+		return m.ErrCommandValidationFailed
+	}
+
+	var dashboards = make(m.PlaylistDashboards, 0)
+	var playlist = m.Playlist{}
+
+	hasPlaylist, err := x.Id(query.Id).Get(&playlist)
+	query.Result = &dashboards
+
+	if err != nil {
+		return err
+	}
+
+	if !hasPlaylist || len(playlist.Data) == 0 {
+		return nil
+	}
+
+	err = x.In("id", playlist.Data).Find(&dashboards)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 1 - 0
public/app/features/all.js

@@ -4,6 +4,7 @@ define([
   './annotations/annotationsSrv',
   './templating/templateSrv',
   './dashboard/all',
+  './playlist/all',
   './panel/all',
   './profile/profileCtrl',
   './profile/changePasswordCtrl',

+ 5 - 0
public/app/features/playlist/all.js

@@ -0,0 +1,5 @@
+define([
+  './playlistsCtrl',
+  './playlistEditCtrl',
+  './playlistRoutes'
+], function () {});

+ 5 - 0
public/app/features/playlist/partials/playlist-remove.html

@@ -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>

+ 115 - 0
public/app/features/playlist/partials/playlist.html

@@ -0,0 +1,115 @@
+<topnav icon="fa fa-fw fa-list" title="Playlists"></topnav>
+
+<div class="page-container" ng-form="playlistEditForm">
+  <div class="page">
+    <div class="row" 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-xxlarge tight-form-input last">
+            </li>
+          </ul>
+          <div class="clearfix"></div>
+        </div>
+        <div class="tight-form">
+          <ul class="tight-form-list">
+            <li class="tight-form-item" style="width: 100px">
+              <strong>Timespan</strong>
+            </li>
+            <li>
+              <input type="text" required ng-model="playlist.timespan" class="input-xxlarge tight-form-input last">
+            </li>
+          </ul>
+          <div class="clearfix"></div>
+        </div>
+      </div>
+
+      <br>
+
+      <div style="display: inline-block">
+        <div class="tight-form">
+          <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 title"
+                     ng-trim="true"
+                     ng-change="search()">
+            </li>
+          </ul>
+          <div class="clearfix"></div>
+        </div>
+      </div>
+    </div>
+    <div class="span6">
+      <h5>Playlist dashboards</h5>
+    </div>
+  </div>
+
+  <div class="row">
+    <div class="span6">
+      <table class="grafana-options-table">
+        <tr ng-repeat="dashboard in filteredDashboards">
+          <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-if="isSearchResultsEmpty() && !isSearchQueryEmpty()">
+          <td colspan="2">
+            <i class="fa fa-warning"></i> No dashboards found
+          </td>
+        </tr>
+        <tr ng-if="isSearchQueryEmpty() && isPlaylistEmpty()">
+          <td colspan="2">
+            <i class="fa fa-warning"></i> Playlist empty
+          </td>
+        </tr>
+      </table>
+    </div>
+    <div class="span6">
+      <table class="grafana-options-table">
+        <tr ng-repeat="dashboard in dashboards">
+          <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>
+      </table>
+    </div>
+  </div>
+
+  <br>
+  <br>
+
+  <div class="pull-left">
+    <div class="tight-form">
+      <button type="button"
+              class="btn btn-success"
+              ng-disabled="playlistEditForm.$invalid || isPlaylistEmpty()"
+              ng-click="savePlaylist(playlist, dashboards)">Save</button>
+      <button type="button"
+              class="btn btn-default"
+              ng-click="backToList()">Cancel</button>
+    </div>
+  </div>
+
+  <div class="clearfix"></div>
+</div>

+ 43 - 0
public/app/features/playlist/partials/playlists.html

@@ -0,0 +1,43 @@
+<topnav icon="fa fa-fw fa-list" title="Playlists"></topnav>
+
+<div class="page-container">
+  <div class="page">
+    <button type="submit" class="btn btn-success" ng-click="createPlaylist()">Create playlist</button>
+
+    <br>
+    <br>
+
+    <div ng-if="playlists.length === 0">
+      <em>No saved playlists</em>
+    </div>
+
+    <table class="grafana-options-table" ng-if="playlists.length > 0">
+      <tr>
+        <td><strong>Title</strong></td>
+        <td><strong>Url</strong></td>
+        <td></td>
+        <td></td>
+      </tr>
+      <tr ng-repeat="playlist in playlists">
+        <td style="width:1%">
+          {{playlist.title}}
+        </td>
+        <td style="width:90%">
+          <a href="{{ playlistUrl(playlist) }}">{{ playlistUrl(playlist) }}</a>
+        </td>
+        <td style="width: 1%">
+          <a href="playlists/edit/{{playlist.id}}" class="btn btn-inverse btn-mini">
+            <i class="fa fa-edit"></i>
+            Edit
+          </a>
+        </td>
+        <td style="width: 1%">
+          <a ng-click="removePlaylist(playlist)" class="btn btn-danger btn-mini">
+            <i class="fa fa-remove"></i>
+          </a>
+        </td>
+      </tr>
+    </table>
+
+  </div>
+</div>

+ 111 - 0
public/app/features/playlist/playlistEditCtrl.js

@@ -0,0 +1,111 @@
+define([
+  'angular',
+  'app/core/config',
+  'lodash'
+],
+function (angular, config, _) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('PlaylistEditCtrl', function(
+    playlist,
+    dashboards,
+    $scope,
+    playlistSrv,
+    backendSrv,
+    $location
+  ) {
+    $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.foundDashboards = results;
+          $scope.filterFoundDashboards();
+        })
+        .finally(function() {
+          $scope.loading = false;
+        });
+    };
+
+    $scope.filterFoundDashboards = function() {
+      $scope.filteredDashboards = _.reject($scope.foundDashboards, function(dashboard) {
+        return _.findWhere(dashboards, function(listDashboard) {
+          return listDashboard.id === dashboard.id;
+        });
+      });
+    };
+
+    $scope.addDashboard = function(dashboard) {
+      dashboards.push(dashboard);
+      $scope.filterFoundDashboards();
+    };
+
+    $scope.removeDashboard = function(dashboard) {
+      _.remove(dashboards, function(listedDashboard) {
+        return dashboard === listedDashboard;
+      });
+      $scope.filterFoundDashboards();
+    };
+
+    $scope.savePlaylist = function(playlist, dashboards) {
+      var savePromise;
+
+      playlist.data = dashboards.map(function(dashboard) {
+        return dashboard.id;
+      });
+
+      // Hardcoding playlist type for this iteration
+      playlist.type = "dashboards";
+
+      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-success', ['Unable to save playlist', '']);
+        });
+    };
+
+    $scope.isPlaylistEmpty = function() {
+      return !dashboards.length;
+    };
+
+    $scope.isSearchResultsEmpty = function() {
+      return !$scope.foundDashboards.length;
+    };
+
+    $scope.isSearchQueryEmpty = function() {
+      return $scope.searchQuery === '';
+    };
+
+    $scope.backToList = function() {
+      $location.path('/playlists');
+    };
+
+    $scope.isLoading = function() {
+      return $scope.loading;
+    };
+
+    $scope.playlist = playlist;
+    $scope.dashboards = dashboards;
+    $scope.timespan = config.playlist_timespan;
+    $scope.filteredDashboards = [];
+    $scope.foundDashboards = [];
+    $scope.searchQuery = '';
+    $scope.loading = false;
+    $scope.search();
+  });
+});

+ 73 - 0
public/app/features/playlist/playlistRoutes.js

@@ -0,0 +1,73 @@
+define([
+  'angular',
+  'app/core/config',
+  'lodash'
+],
+function (angular, config, _) {
+  'use strict';
+
+  var module = angular.module('grafana.routes');
+
+  module.config(function($routeProvider) {
+    $routeProvider
+      .when('/playlists', {
+        templateUrl: 'app/features/playlist/partials/playlists.html',
+        controller : 'PlaylistsCtrl',
+        resolve: {
+          playlists: function (backendSrv) {
+            return backendSrv.get('/api/playlists');
+          }
+        }
+      })
+      .when('/playlists/create', {
+        templateUrl: 'app/features/playlist/partials/playlist.html',
+        controller : 'PlaylistEditCtrl',
+        resolve: {
+          playlist: function() {
+            return {
+              timespan: '1m'
+            };
+          },
+          dashboards: function() {
+            return [];
+          }
+        }
+      })
+      .when('/playlists/edit/:id', {
+        templateUrl: 'app/features/playlist/partials/playlist.html',
+        controller : 'PlaylistEditCtrl',
+        resolve: {
+          playlist: function(backendSrv, $route) {
+            var playlistId = $route.current.params.id;
+
+            return backendSrv.get('/api/playlists/' + playlistId);
+          },
+          dashboards: function(backendSrv, $route) {
+            var playlistId = $route.current.params.id;
+
+            return backendSrv.get('/api/playlists/' + playlistId + '/dashboards');
+          }
+        }
+      })
+      .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) {
+                    _.each(dashboards, function(dashboard) {
+                      dashboard.uri = 'db/' + dashboard.slug;
+                    });
+                    playlistSrv.start(dashboards, playlist.timespan);
+                  });
+              });
+          }
+        }
+      });
+  });
+});

+ 49 - 0
public/app/features/playlist/playlistsCtrl.js

@@ -0,0 +1,49 @@
+define([
+  'angular',
+  'lodash'
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('PlaylistsCtrl', function(
+    playlists,
+    $scope,
+    $location,
+    backendSrv
+  ) {
+    $scope.playlists = playlists;
+
+    $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(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', '']);
+            playlists.push(playlist);
+          });
+      };
+
+      $scope.appEvent('show-modal', {
+        src: './app/features/playlist/partials/playlist-remove.html',
+        scope: modalScope
+      });
+    };
+
+    $scope.createPlaylist = function() {
+      $location.path('/playlists/create');
+    };
+  });
+});