فهرست منبع

New TV Mode, dashboard toolbar update (layout change & new cycle view mode button) (#13025)

* wip: design update for navbar with kiosk mode button

* feat: progress on new view mode button

* css: view state refactorings

* feat: kiosk modes & playlist support

* feature: cycle tv mode feature, renamed view modes to TV, and Kiosk

* fix: updated the alert notification message

* fix: removed unused parameter

* fix: correct the css class set for tv mode

* some minor improvements to playlist
Torkel Ödegaard 7 سال پیش
والد
کامیت
154fbe2413

+ 56 - 36
public/app/core/components/grafana_app.ts

@@ -69,6 +69,30 @@ export class GrafanaCtrl {
   }
 }
 
+function setViewModeBodyClass(body, mode, sidemenuOpen: boolean) {
+  body.removeClass('view-mode--tv');
+  body.removeClass('view-mode--kiosk');
+  body.removeClass('view-mode--inactive');
+
+  switch (mode) {
+    case 'tv': {
+      body.removeClass('sidemenu-open');
+      body.addClass('view-mode--tv');
+      break;
+    }
+    // 1 & true for legacy states
+    case 1:
+    case true: {
+      body.removeClass('sidemenu-open');
+      body.addClass('view-mode--kiosk');
+      break;
+    }
+    default: {
+      body.toggleClass('sidemenu-open', sidemenuOpen);
+    }
+  }
+}
+
 /** @ngInject */
 export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope, $location) {
   return {
@@ -98,7 +122,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
       });
 
       scope.$watch(() => playlistSrv.isPlaying, function(newValue) {
-        elem.toggleClass('playlist-active', newValue === true);
+        elem.toggleClass('view-mode--playlist', newValue === true);
       });
 
       // check if we are in server side render
@@ -127,17 +151,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
         $('#tooltip, .tooltip').remove();
 
         // check for kiosk url param
-        if (data.params.kiosk) {
-          appEvents.emit('toggle-kiosk-mode');
-        }
-
-        // check for 'inactive' url param for clean looks like kiosk, but with title
-        if (data.params.inactive) {
-          body.addClass('user-activity-low');
-
-          // for some reason, with this class it looks cleanest
-          body.addClass('sidemenu-open');
-        }
+        setViewModeBodyClass(body, data.params.kiosk, sidemenuOpen);
 
         // close all drops
         for (const drop of Drop.drops) {
@@ -146,15 +160,37 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
       });
 
       // handle kiosk mode
-      appEvents.on('toggle-kiosk-mode', () => {
-        body.toggleClass('page-kiosk-mode');
+      appEvents.on('toggle-kiosk-mode', options => {
+        const search = $location.search();
+
+        if (options && options.exit) {
+          search.kiosk = 1;
+        }
+
+        switch (search.kiosk) {
+          case 'tv': {
+            search.kiosk = 1;
+            appEvents.emit('alert-success', ['Press ESC to exit Kiosk mode']);
+            break;
+          }
+          case 1:
+          case true: {
+            delete search.kiosk;
+            break;
+          }
+          default: {
+            search.kiosk = 'tv';
+          }
+        }
+
+        $location.search(search);
+        setViewModeBodyClass(body, search.kiosk, sidemenuOpen);
       });
 
       // handle in active view state class
       let lastActivity = new Date().getTime();
       let activeUser = true;
-      const inActiveTimeLimit = 60 * 1000;
-      let sidemenuHidden = false;
+      const inActiveTimeLimit = 60 * 5000;
 
       function checkForInActiveUser() {
         if (!activeUser) {
@@ -167,15 +203,8 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
 
         if (new Date().getTime() - lastActivity > inActiveTimeLimit) {
           activeUser = false;
-          body.addClass('user-activity-low');
-          // hide sidemenu
-          if (sidemenuOpen) {
-            sidemenuHidden = true;
-            body.removeClass('sidemenu-open');
-            $timeout(function() {
-              $rootScope.$broadcast('render');
-            }, 100);
-          }
+          body.addClass('view-mode--inactive');
+          body.removeClass('sidemenu-open');
         }
       }
 
@@ -183,17 +212,8 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
         lastActivity = new Date().getTime();
         if (!activeUser) {
           activeUser = true;
-          body.removeClass('user-activity-low');
-
-          // restore sidemenu
-          if (sidemenuHidden) {
-            sidemenuHidden = false;
-            body.addClass('sidemenu-open');
-            appEvents.emit('toggle-inactive-mode');
-            $timeout(function() {
-              $rootScope.$broadcast('render');
-            }, 100);
-          }
+          body.removeClass('view-mode--inactive');
+          body.toggleClass('sidemenu-open', sidemenuOpen);
         }
       }
 

+ 0 - 38
public/app/core/components/scroll/scroll.ts

@@ -1,7 +1,6 @@
 import $ from 'jquery';
 import baron from 'baron';
 import coreModule from 'app/core/core_module';
-import appEvents from 'app/core/app_events';
 
 const scrollBarHTML = `
 <div class="baron__track">
@@ -39,43 +38,6 @@ export function geminiScrollbar() {
 
       const scrollbar = baron(scrollParams);
 
-      let lastPos = 0;
-
-      appEvents.on(
-        'dash-scroll',
-        evt => {
-          if (evt.restore) {
-            elem[0].scrollTop = lastPos;
-            return;
-          }
-
-          lastPos = elem[0].scrollTop;
-
-          if (evt.animate) {
-            elem.animate({ scrollTop: evt.pos }, 500);
-          } else {
-            elem[0].scrollTop = evt.pos;
-          }
-        },
-        scope
-      );
-
-      // force updating dashboard width
-      appEvents.on('toggle-sidemenu', forceUpdate, scope);
-      appEvents.on('toggle-sidemenu-hidden', forceUpdate, scope);
-      appEvents.on('toggle-view-mode', forceUpdate, scope);
-      appEvents.on('toggle-kiosk-mode', forceUpdate, scope);
-      appEvents.on('toggle-inactive-mode', forceUpdate, scope);
-
-      function forceUpdate() {
-        scrollbar.scroll();
-      }
-
-      scope.$on('$routeChangeSuccess', () => {
-        lastPos = 0;
-        elem[0].scrollTop = 0;
-      });
-
       scope.$on('$destroy', () => {
         scrollbar.dispose();
       });

+ 18 - 8
public/app/core/services/keybindingSrv.ts

@@ -77,15 +77,15 @@ export class KeybindingSrv {
 
     appEvents.emit('hide-modal');
 
-    if (!this.modalOpen) {
-      if (this.timepickerOpen) {
-        this.$rootScope.appEvent('closeTimepicker');
-        this.timepickerOpen = false;
-      } else {
-        this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
-      }
-    } else {
+    if (this.modalOpen) {
       this.modalOpen = false;
+      return;
+    }
+
+    if (this.timepickerOpen) {
+      this.$rootScope.appEvent('closeTimepicker');
+      this.timepickerOpen = false;
+      return;
     }
 
     // close settings view
@@ -93,6 +93,16 @@ export class KeybindingSrv {
     if (search.editview) {
       delete search.editview;
       this.$location.search(search);
+      return;
+    }
+
+    if (search.fullscreen) {
+      this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
+      return;
+    }
+
+    if (search.kiosk) {
+      this.$rootScope.appEvent('toggle-kiosk-mode', { exit: true });
     }
   }
 

+ 13 - 5
public/app/features/dashboard/dashboard_model.ts

@@ -842,12 +842,20 @@ export class DashboardModel {
       })
     );
 
-    // Consider navbar and submenu controls, padding and margin
-    let visibleHeight = window.innerHeight - 55 - 20;
+    const navbarHeight = 55;
+    const margin = 20;
+    const submenuHeight = 50;
 
-    // Remove submenu if visible
-    if (this.meta.submenuEnabled) {
-      visibleHeight -= 50;
+    let visibleHeight = viewHeight - navbarHeight - margin;
+
+    // Remove submenu height if visible
+    if (this.meta.submenuEnabled && !this.meta.kiosk) {
+      visibleHeight -= submenuHeight;
+    }
+
+    // add back navbar height
+    if (this.meta.kiosk === 'b') {
+      visibleHeight += 55;
     }
 
     const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));

+ 10 - 4
public/app/features/dashboard/dashnav/dashnav.html

@@ -8,14 +8,14 @@
 		</a>
 	</div>
 
+	<div class="navbar__spacer"></div>
+
 	<div class="navbar-buttons navbar-buttons--playlist" ng-if="ctrl.playlistSrv.isPlaying">
 		<a class="navbar-button navbar-button--tight" ng-click="ctrl.playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
 		<a class="navbar-button navbar-button--tight" ng-click="ctrl.playlistSrv.stop()"><i class="fa fa-stop"></i></a>
 		<a class="navbar-button navbar-button--tight" ng-click="ctrl.playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
 	</div>
 
-	<div class="navbar__spacer"></div>
-
 	<div class="navbar-buttons navbar-buttons--actions">
 		<button class="btn navbar-button navbar-button--add-panel" ng-show="::ctrl.dashboard.meta.canSave" bs-tooltip="'Add panel'" data-placement="bottom" ng-click="ctrl.addPanel()">
 			<i class="gicon gicon-add-panel"></i>
@@ -25,11 +25,11 @@
 			<i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}"></i>
 		</button>
 
-		<button class="btn navbar-button navbar-button--share" ng-show="::ctrl.dashboard.meta.canShare" ng-click="ctrl.shareDashboard(0)" bs-tooltip="'Share dashboard'" data-placement="bottom">
+    <button class="btn navbar-button navbar-button--share" ng-show="::ctrl.dashboard.meta.canShare" ng-click="ctrl.shareDashboard(0)" bs-tooltip="'Share dashboard'" data-placement="bottom">
 			<i class="fa fa-share-square-o"></i></a>
 		</button>
 
-		<button class="btn navbar-button navbar-button--save" ng-show="ctrl.dashboard.meta.canSave" ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom">
+    <button class="btn navbar-button navbar-button--save" ng-show="ctrl.dashboard.meta.canSave" ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom">
 			<i class="fa fa-save"></i>
 		</button>
 
@@ -42,6 +42,12 @@
 		</button>
 	</div>
 
+	<div class="navbar-buttons navbar-buttons--tv">
+    <button class="btn navbar-button navbar-button--tv" ng-click="ctrl.toggleViewMode()" bs-tooltip="'Cycle view mode'" data-placement="bottom">
+      <i class="fa fa-desktop"></i>
+    </button>
+  </div>
+
 	<gf-time-picker class="gf-timepicker-nav" dashboard="ctrl.dashboard" ng-if="!ctrl.dashboard.timepicker.hidden"></gf-time-picker>
 
 	<div class="navbar-buttons navbar-buttons--close">

+ 4 - 0
public/app/features/dashboard/dashnav/dashnav.ts

@@ -31,6 +31,10 @@ export class DashNavCtrl {
     this.$location.search(search);
   }
 
+  toggleViewMode() {
+    appEvents.emit('toggle-kiosk-mode');
+  }
+
   close() {
     const search = this.$location.search();
     if (search.editview) {

+ 11 - 13
public/app/features/dashboard/timepicker/timepicker.html

@@ -1,18 +1,8 @@
-<div class="navbar-buttons navbar-buttons--zoom">
-	<button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(-1)'>
+<div class="navbar-buttons">
+  <button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(-1)' ng-if="ctrl.isAbsolute">
 		<i class="fa fa-chevron-left"></i>
 	</button>
 
-	<button class="btn navbar-button" bs-tooltip="'Time range zoom out <br> CTRL+Z'" data-placement="bottom" ng-click='ctrl.zoom(2)'>
-		<i class="fa fa-search-minus"></i>
-	</button>
-
-	<button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(1)'>
-		<i class="fa fa-chevron-right"></i>
-	</button>
-</div>
-
-<div class="navbar-buttons">
 	<button bs-tooltip="ctrl.tooltip" data-placement="bottom" ng-click="ctrl.openDropdown()" class="btn navbar-button gf-timepicker-nav-btn">
 		<i class="fa fa-clock-o"></i>
 		<span ng-bind="ctrl.rangeString"></span>
@@ -20,7 +10,15 @@
 		<span ng-show="ctrl.dashboard.refresh" class="text-warning">&nbsp; Refresh every {{ctrl.dashboard.refresh}}</span>
 	</button>
 
-	<button class="btn navbar-button navbar-button--refresh" ng-click="ctrl.timeSrv.refreshDashboard()">
+  <button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(1)' ng-if="ctrl.isAbsolute">
+		<i class="fa fa-chevron-right"></i>
+	</button>
+
+  <button class="btn navbar-button navbar-button--zoom" bs-tooltip="'Time range zoom out <br> CTRL+Z'" data-placement="bottom" ng-click='ctrl.zoom(2)'>
+		<i class="fa fa-search-minus"></i>
+	</button>
+
+  <button class="btn navbar-button navbar-button--refresh" ng-click="ctrl.timeSrv.refreshDashboard()">
 		<i class="fa fa-refresh"></i>
 	</button>
 </div>

+ 2 - 0
public/app/features/dashboard/timepicker/timepicker.ts

@@ -23,6 +23,7 @@ export class TimePickerCtrl {
   isUtc: boolean;
   firstDayOfWeek: number;
   isOpen: boolean;
+  isAbsolute: boolean;
 
   /** @ngInject */
   constructor(private $scope, private $rootScope, private timeSrv) {
@@ -65,6 +66,7 @@ export class TimePickerCtrl {
     this.tooltip = this.dashboard.formatDate(time.from) + ' <br>to<br>';
     this.tooltip += this.dashboard.formatDate(time.to);
     this.timeRaw = timeRaw;
+    this.isAbsolute = moment.isMoment(this.timeRaw.to);
   }
 
   zoom(factor) {

+ 62 - 74
public/app/features/playlist/partials/playlist.html

@@ -1,9 +1,9 @@
 <page-header model="ctrl.navModel"></page-header>
 
-<div class="page-container page-body" ng-form="playlistEditForm">
+<div class="page-container page-body" ng-form="ctrl.playlistEditForm">
 
-  <h3 class="page-sub-heading" ng-hide="ctrl.isNew">Edit Playlist</h3>
-  <h3 class="page-sub-heading" ng-show="ctrl.isNew">New Playlist</h3>
+	<h3 class="page-sub-heading" ng-hide="ctrl.isNew">Edit Playlist</h3>
+	<h3 class="page-sub-heading" ng-show="ctrl.isNew">New Playlist</h3>
 
 	<p class="playlist-description">A playlist rotates through a pre-selected list of Dashboards. A Playlist can be a great way to build situational awareness, or just show off your metrics to your team or visitors.</p>
 
@@ -20,79 +20,71 @@
 
 	<div class="gf-form-group">
 		<h3 class="page-headering">Dashboards</h3>
-	</div>
 
-	<div class="row">
-		<div class="col-lg-6">
-			<div class="playlist-search-containerwrapper">
-				<div class="max-width-32">
-					<h5 class="page-headering playlist-column-header">Available</h5>
-					<div style="">
-						<playlist-search class="playlist-search-container" search-started="ctrl.searchStarted(promise)"></playlist-search>
-					</div>
-				</div>
-			</div>
+		<table class="filter-table playlist-available-list">
+			<tr ng-repeat="playlistItem in ctrl.playlistItems">
+				<td ng-if="playlistItem.type === 'dashboard_by_id'">
+					<i class="icon-gf icon-gf-dashboard"></i>&nbsp;&nbsp;{{playlistItem.title}}
+				</td>
+				<td 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>
 
-			<div ng-if="ctrl.filteredDashboards.length > 0">
-				<table class="filter-table playlist-available-list">
-					<tr ng-repeat="playlistItem in ctrl.filteredDashboards">
-						<td>
-							<i class="icon-gf icon-gf-dashboard"></i>
-							&nbsp;&nbsp;{{playlistItem.title}}
-							<i class="fa fa-star" ng-show="playlistItem.isStarred"></i>
-						</td>
-						<td class="add-dashboard">
-							<button class="btn btn-inverse btn-mini pull-right" ng-click="ctrl.addPlaylistItem(playlistItem)">
-								<i class="fa fa-plus"></i>
-								Add to playlist
-							</button>
-						</td>
-					</tr>
-				</table>
-			</div>
-			<div class="playlist-search-results-container" ng-if="ctrl.filteredTags.length > 0;">
-				<table class="filter-table playlist-available-list">
-					<tr ng-repeat="tag in ctrl.filteredTags">
-						<td>
-							<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>
-						</td>
-						<td class="add-dashboard">
-							<button class="btn btn-inverse btn-mini pull-right" ng-click="ctrl.addTagPlaylistItem(tag)">
-								<i class="fa fa-plus"></i>
-								Add to playlist
-							</button>
-						</td>
-					</tr>
-				</table>
-			</div>
-		</div>
+				<td class="selected-playlistitem-settings">
+					<button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="ctrl.movePlaylistItemUp(playlistItem)">
+						<i class="fa fa-arrow-up"></i>
+					</button>
+					<button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="ctrl.movePlaylistItemDown(playlistItem)">
+						<i class="fa fa-arrow-down"></i>
+					</button>
+					<button class="btn btn-inverse btn-mini" ng-click="ctrl.removePlaylistItem(playlistItem)">
+						<i class="fa fa-remove"></i>
+					</button>
+				</td>
+			</tr>
+			<tr ng-if="ctrl.playlistItems.length === 0">
+				<td><em>Playlist is empty, add dashboards below.</em></td>
+			</tr>
+		</table>
+	</div>
 
-		<div class="col-lg-6">
-			<h5 class="page headering playlist-column-header">Selected</h5>
+	<div class="gf-form-group">
+		<h3 class="page-headering">Add dashboards</h3>
+		<playlist-search class="playlist-search-container" search-started="ctrl.searchStarted(promise)"></playlist-search>
+
+		<div ng-if="ctrl.filteredDashboards.length > 0">
 			<table class="filter-table playlist-available-list">
-				<tr ng-repeat="playlistItem in ctrl.playlistItems">
-					<td ng-if="playlistItem.type === 'dashboard_by_id'">
-						<i class="icon-gf icon-gf-dashboard"></i>&nbsp;&nbsp;{{playlistItem.title}}
+				<tr ng-repeat="playlistItem in ctrl.filteredDashboards">
+					<td>
+						<i class="icon-gf icon-gf-dashboard"></i>
+						&nbsp;&nbsp;{{playlistItem.title}}
+						<i class="fa fa-star" ng-show="playlistItem.isStarred"></i>
+					</td>
+					<td class="add-dashboard">
+						<button class="btn btn-inverse btn-mini pull-right" ng-click="ctrl.addPlaylistItem(playlistItem)">
+							<i class="fa fa-plus"></i>
+							Add to playlist
+						</button>
 					</td>
-					<td ng-if="playlistItem.type === 'dashboard_by_tag'">
-						<a class="search-result-tag label label-tag" tag-color-from-name="playlistItem.title">
+				</tr>
+			</table>
+		</div>
+		<div class="playlist-search-results-container" ng-if="ctrl.filteredTags.length > 0;">
+			<table class="filter-table playlist-available-list">
+				<tr ng-repeat="tag in ctrl.filteredTags">
+					<td>
+						<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
 							<i class="fa fa-tag"></i>
-							<span>{{playlistItem.title}}</span>
+							<span>{{tag.term}} &nbsp;({{tag.count}})</span>
 						</a>
 					</td>
-
-					<td class="selected-playlistitem-settings">
-						<button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="ctrl.movePlaylistItemUp(playlistItem)">
-							<i class="fa fa-arrow-up"></i>
-						</button>
-						<button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="ctrl.movePlaylistItemDown(playlistItem)">
-							<i class="fa fa-arrow-down"></i>
-						</button>
-						<button class="btn btn-inverse btn-mini" ng-click="ctrl.removePlaylistItem(playlistItem)">
-							<i class="fa fa-remove"></i>
+					<td class="add-dashboard">
+						<button class="btn btn-inverse btn-mini pull-right" ng-click="ctrl.addTagPlaylistItem(tag)">
+							<i class="fa fa-plus"></i>
+							Add to playlist
 						</button>
 					</td>
 				</tr>
@@ -103,12 +95,8 @@
 	<div class="clearfix"></div>
 
 	<div class="gf-form-button-row">
-		<a class="btn btn-success " ng-show="ctrl.isNew"
-			ng-disabled="ctrl.playlistEditForm.$invalid || ctrl.isPlaylistEmpty()"
-			ng-click="ctrl.savePlaylist(ctrl.playlist, ctrl.playlistItems)">Create new playlist</a>
-		<a class="btn btn-success" ng-show="!ctrl.isNew()"
-			ng-disabled="ctrl.playlistEditForm.$invalid || ctrl.isPlaylistEmpty()"
-			ng-click="ctrl.savePlaylist(ctrl.playlist, ctrl.playlistItems)">Save</a>
+		<a class="btn btn-success" ng-show="ctrl.isNew" ng-disabled="ctrl.playlistEditForm.$invalid || ctrl.isPlaylistEmpty()" ng-click="ctrl.savePlaylist(ctrl.playlist, ctrl.playlistItems)">Create</a>
+		<a class="btn btn-success" ng-show="!ctrl.isNew" ng-disabled="ctrl.playlistEditForm.$invalid || ctrl.isPlaylistEmpty()" ng-click="ctrl.savePlaylist(ctrl.playlist, ctrl.playlistItems)">Save</a>
 		<a class="btn-text" ng-click="ctrl.backToList()">Cancel</a>
 	</div>
 </div>

+ 30 - 28
public/app/features/playlist/partials/playlists.html

@@ -10,38 +10,42 @@
       </a>
     </div>
 
-    <table class="filter-table">
+    <table class="filter-table filter-table--hover">
       <thead>
-        <th>
-          <strong>Name</strong>
-        </th>
-        <th>
-          <strong>Start url</strong>
-        </th>
+        <th><strong>Name</strong></th>
+        <th style="width: 100px"></th>
         <th style="width: 78px"></th>
-        <th style="width: 78px"></th>
-        <th style="width: 25px"></th>
       </thead>
       <tr ng-repeat="playlist in ctrl.playlists">
-        <td>
+        <td class="link-td">
           <a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a>
         </td>
-        <td>
-          <a href="playlists/play/{{playlist.id}}">playlists/play/{{playlist.id}}</a>
-        </td>
-        <td class="text-center">
-          <a href="playlists/play/{{playlist.id}}" class="btn btn-inverse btn-small">
-            <i class="fa fa-play"></i>
-            Play
-          </a>
+        <td class="dropdown">
+          <button class="btn btn-inverse btn-small" data-toggle="dropdown">
+            Start playlist
+            <i class="fa fa-caret-down"></i>
+          </button>
+          <ul class="dropdown-menu" role="menu">
+            <li>
+              <a href="{{playlist.startUrl}}">
+                <i class="fa fa-play"></i> In Normal mode</span>
+              </a>
+              <a href="{{playlist.startUrl}}?kiosk=tv">
+                <i class="fa fa-play"></i> In TV mode</span>
+              </a>
+              <a href="{{playlist.startUrl}}?kiosk=tv&autofitpanels">
+                <i class="fa fa-play"></i> In TV mode <span class="muted">(with auto fit panels)</span>
+              </a>
+              <a href="{{playlist.startUrl}}?kiosk">
+                <i class="fa fa-play"></i> In Kiosk mode</span>
+              </a>
+              <a ng-href="{{playlist.startUrl}}?kiosk&autofitpanels">
+                <i class="fa fa-play"></i> In Kiosk mode <span class="muted">(with auto fit panels)</span>
+              </a>
+            </li>
+          </ul>
         </td>
-        <td class="text-right">
-          <a href="playlists/edit/{{playlist.id}}" class="btn btn-inverse btn-small">
-            <i class="fa fa-edit"></i>
-            Edit
-          </a>
-        </td>
-        <td class="text-right">
+        <td  class="text-right">
           <a ng-click="ctrl.removePlaylist(playlist)" class="btn btn-danger btn-small">
             <i class="fa fa-remove"></i>
           </a>
@@ -49,18 +53,16 @@
       </tr>
     </table>
   </div>
-
   <div ng-if="ctrl.playlists.length === 0">
     <empty-list-cta model="{
       title: 'There are no playlists created yet',
       buttonIcon: 'fa fa-plus',
       buttonLink: 'playlists/create',
       buttonTitle: ' Create Playlist',
-      proTip: 'You can run the playlist in Kiosk Mode.',
+      proTip: 'You can use playlists to remove control TVs',
       proTipLink: 'http://docs.grafana.org/reference/playlist/',
       proTipLinkTitle: 'Learn more',
       proTipTarget: '_blank'
     }" />
   </div>
-
 </div>

+ 1 - 12
public/app/features/playlist/playlist_edit_ctrl.ts

@@ -19,29 +19,18 @@ export class PlaylistEditCtrl {
   /** @ngInject */
   constructor(private $scope, private backendSrv, private $location, $route, navModelSrv) {
     this.navModel = navModelSrv.getNav('dashboards', 'playlists', 0);
-    this.isNew = $route.current.params.id;
+    this.isNew = !$route.current.params.id;
 
     if ($route.current.params.id) {
       const playlistId = $route.current.params.id;
 
       backendSrv.get('/api/playlists/' + playlistId).then(result => {
         this.playlist = result;
-        this.navModel.node = {
-          text: result.name,
-          icon: this.navModel.node.icon,
-        };
-        this.navModel.breadcrumbs.push(this.navModel.node);
       });
 
       backendSrv.get('/api/playlists/' + playlistId + '/items').then(result => {
         this.playlistItems = result;
       });
-    } else {
-      this.navModel.node = {
-        text: 'New playlist',
-        icon: this.navModel.node.icon,
-      };
-      this.navModel.breadcrumbs.push(this.navModel.node);
     }
   }
 

+ 1 - 3
public/app/features/playlist/playlist_routes.ts

@@ -19,9 +19,7 @@ function grafanaRoutes($routeProvider) {
       controller: 'PlaylistEditCtrl',
     })
     .when('/playlists/play/:id', {
-      templateUrl: 'public/app/features/playlist/partials/playlists.html',
-      controllerAs: 'ctrl',
-      controller: 'PlaylistsCtrl',
+      template: '',
       resolve: {
         init: function(playlistSrv, $route) {
           const playlistId = $route.current.params.id;

+ 1 - 1
public/app/features/playlist/playlist_search.ts

@@ -8,7 +8,7 @@ export class PlaylistSearchCtrl {
 
   /** @ngInject */
   constructor($timeout, private backendSrv) {
-    this.query = { query: '', tag: [], starred: false, limit: 30 };
+    this.query = { query: '', tag: [], starred: false, limit: 20 };
 
     $timeout(() => {
       this.query.query = '';

+ 15 - 25
public/app/features/playlist/playlist_srv.ts

@@ -1,6 +1,8 @@
 import coreModule from '../../core/core_module';
 import kbn from 'app/core/utils/kbn';
 import appEvents from 'app/core/app_events';
+import _ from 'lodash';
+import { toUrlParams } from 'app/core/utils/url';
 
 class PlaylistSrv {
   private cancelPromise: any;
@@ -11,42 +13,27 @@ class PlaylistSrv {
   public isPlaying: boolean;
 
   /** @ngInject */
-  constructor(private $location: any, private $timeout: any, private backendSrv: any, private $routeParams: any) {}
+  constructor(private $location: any, private $timeout: any, private backendSrv: any) {}
 
   next() {
     this.$timeout.cancel(this.cancelPromise);
 
     const playedAllDashboards = this.index > this.dashboards.length - 1;
-
     if (playedAllDashboards) {
-      window.location.href = this.getUrlWithKioskMode();
+      window.location.href = this.startUrl;
       return;
     }
 
     const dash = this.dashboards[this.index];
-    this.$location.url('dashboard/' + dash.uri);
+    const queryParams = this.$location.search();
+    const filteredParams = _.pickBy(queryParams, value => value !== null);
+
+    this.$location.url('dashboard/' + dash.uri + '?' + toUrlParams(filteredParams));
 
     this.index++;
     this.cancelPromise = this.$timeout(() => this.next(), this.interval);
   }
 
-  getUrlWithKioskMode() {
-    const inKioskMode = document.body.classList.contains('page-kiosk-mode');
-
-    // check if should add kiosk query param
-    if (inKioskMode && this.startUrl.indexOf('kiosk') === -1) {
-      return this.startUrl + '?kiosk=true';
-    }
-
-    // check if should remove kiosk query param
-    if (!inKioskMode) {
-      return this.startUrl.split('?')[0];
-    }
-
-    // already has kiosk query param, just return startUrl
-    return this.startUrl;
-  }
-
   prev() {
     this.index = Math.max(this.index - 2, 0);
     this.next();
@@ -59,10 +46,6 @@ class PlaylistSrv {
     this.index = 0;
     this.isPlaying = true;
 
-    if (this.$routeParams.kiosk) {
-      appEvents.emit('toggle-kiosk-mode');
-    }
-
     this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
       this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => {
         this.dashboards = dashboards;
@@ -73,6 +56,13 @@ class PlaylistSrv {
   }
 
   stop() {
+    if (this.isPlaying) {
+      const queryParams = this.$location.search();
+      if (queryParams.kiosk) {
+        appEvents.emit('toggle-kiosk-mode', { exit: true });
+      }
+    }
+
     this.index = 0;
     this.isPlaying = false;
 

+ 4 - 1
public/app/features/playlist/playlists_ctrl.ts

@@ -10,7 +10,10 @@ export class PlaylistsCtrl {
     this.navModel = navModelSrv.getNav('dashboards', 'playlists', 0);
 
     backendSrv.get('/api/playlists').then(result => {
-      this.playlists = result;
+      this.playlists = result.map(item => {
+        item.startUrl = `playlists/play/${item.id}`;
+        return item;
+      });
     });
   }
 

+ 2 - 3
public/app/routes/dashboard_loaders.ts

@@ -40,9 +40,8 @@ export class LoadDashboardCtrl {
         }
       }
 
-      if ($routeParams.autofitpanels) {
-        result.meta.autofitpanels = true;
-      }
+      result.meta.autofitpanels = $routeParams.autofitpanels;
+      result.meta.kiosk = $routeParams.kiosk;
 
       $scope.initDashboard(result, $scope);
     });

+ 9 - 2
public/sass/components/_navbar.scss

@@ -23,6 +23,7 @@
     @include navbar-alt-look();
   }
 
+  .navbar-buttons--tv,
   .navbar-button--add-panel,
   .navbar-button--star,
   .navbar-button--save,
@@ -45,6 +46,7 @@
 
   .navbar-button--add-panel,
   .navbar-button--star,
+  .navbar-button--tv,
   .navbar-page-btn .fa-caret-down {
     display: none;
   }
@@ -106,6 +108,10 @@
     display: none;
     margin-right: 0;
   }
+
+  &--zoom {
+    margin-right: 0;
+  }
 }
 
 .navbar__spacer {
@@ -119,6 +125,7 @@
   font-weight: $btn-font-weight;
   padding: 6px 11px;
   line-height: 16px;
+  height: 30px;
   color: $text-muted;
   border: 1px solid $navbar-button-border;
   margin-right: 3px;
@@ -133,7 +140,7 @@
   }
 
   &--add-panel {
-    padding: 3px 10px;
+    padding: 2px 10px;
 
     .gicon {
       font-size: 22px;
@@ -146,7 +153,7 @@
     .fa {
       font-size: 14px;
       position: relative;
-      top: 2px;
+      top: 1px;
     }
   }
 

+ 33 - 26
public/sass/components/_view_states.scss

@@ -1,42 +1,49 @@
-.page-kiosk-mode {
-  .sidemenu,
-  .navbar {
-    display: none;
-  }
-  .scroll-canvas--dashboard {
-    height: 100%;
-  }
-}
-
-.playlist-active,
-.user-activity-low {
+.view-mode--inactive {
   .react-resizable-handle,
   .add-row-panel-hint,
   .dash-row-menu-container,
-  .navbar-button--refresh,
-  .navbar-buttons--zoom,
   .navbar-buttons--actions,
-  .panel-menu-container,
   .panel-info-corner--info,
   .panel-info-corner--links {
-    opacity: 0;
-  }
-
-  .navbar {
-    box-shadow: none;
-    background: transparent;
+    display: none;
   }
 
   .navbar-page-btn {
-    border-color: transparent;
-    background: transparent;
-    transform: translate3d(-40px, 0, 0);
+    transform: translate3d(-36px, 0, 0);
     i {
       opacity: 0;
     }
   }
 
-  .gf-timepicker-nav-btn {
-    transform: translate3d(40px, 0, 0);
+  .navbar-button--zoom,
+  .navbar-button--refresh {
+    display: none;
+  }
+}
+
+.view-mode--playlist {
+  @extend .view-mode--inactive;
+}
+
+.view-mode--tv {
+  @extend .view-mode--inactive;
+
+  .submenu-controls {
+    display: none;
+  }
+}
+
+.view-mode--kiosk {
+  @extend .view-mode--tv;
+
+  .sidemenu,
+  .navbar {
+    display: none;
+  }
+  .scroll-canvas--dashboard {
+    height: 100%;
+  }
+  .submenu-controls {
+    display: none;
   }
 }