diff --git a/src/core_plugins/kibana/public/dashboard/components/panel/panel.html b/src/core_plugins/kibana/public/dashboard/components/panel/panel.html index c38cf45a2bd31..911da9f23ef57 100644 --- a/src/core_plugins/kibana/public/dashboard/components/panel/panel.html +++ b/src/core_plugins/kibana/public/dashboard/components/panel/panel.html @@ -1,4 +1,4 @@ -
+
{{::savedObj.title}} @@ -7,13 +7,13 @@ - + - + - +
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js b/src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js new file mode 100644 index 0000000000000..7143851e7189c --- /dev/null +++ b/src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js @@ -0,0 +1,39 @@ + +import stateMonitorFactory from 'ui/state_management/state_monitor_factory'; + +export class DashboardAppStateManager { + constructor(stateDefaults, AppState) { + let temporaryStateMonitor; + let persistedStateMonitor; + + let temporaryAppState; + let persistedAppState; + + this.appState = new AppState(stateDefaults); + this.uiState = this.appState.makeStateful('uiState'); + this.$appStatus = {}; + + // watch for state changes and update the appStatus.dirty value + this.stateMonitor = stateMonitorFactory.create($state, stateDefaults); + this.stateMonitor.onChange((status) => { + $appStatus.dirty = status.dirty; + }); + + this.viewMode = currentViewMode; + } + + onViewModeChanged(newMode) { + this.viewMode = newMode; + + } + + destroy() { + this.stateMonitor.destroy(); + } +} + + +function resetState(dashboardStateManager, newAppState) { + dashboardStateManager.appState = newAppState; + dashboardStateManager.uiState = newAppState.makeStateful('uiState'); +} diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_view_mode.js b/src/core_plugins/kibana/public/dashboard/dashboard_view_mode.js new file mode 100644 index 0000000000000..d554d0399475f --- /dev/null +++ b/src/core_plugins/kibana/public/dashboard/dashboard_view_mode.js @@ -0,0 +1,13 @@ +/** + * A dashboard mode. + * @typedef {string} DashboardMode + */ + +/** + * Dashboard view modes. + * @type {{EDIT: DashboardMode, VIEW: DashboardMode}} + */ +export const DashboardViewMode = { + EDIT: 'edit', + VIEW: 'view' +}; diff --git a/src/core_plugins/kibana/public/dashboard/directives/dashboard_panel.js b/src/core_plugins/kibana/public/dashboard/directives/dashboard_panel.js index a974683872cc0..98aafd5470e75 100644 --- a/src/core_plugins/kibana/public/dashboard/directives/dashboard_panel.js +++ b/src/core_plugins/kibana/public/dashboard/directives/dashboard_panel.js @@ -5,6 +5,7 @@ import { loadPanelProvider } from 'plugins/kibana/dashboard/components/panel/lib import FilterManagerProvider from 'ui/filter_manager'; import uiModules from 'ui/modules'; import panelTemplate from 'plugins/kibana/dashboard/components/panel/panel.html'; +import { DashboardViewMode } from 'plugins/kibana/dashboard/dashboard_view_mode'; uiModules .get('app/dashboard') @@ -33,6 +34,11 @@ uiModules restrict: 'E', template: panelTemplate, scope: { + /** + * What view mode the dashboard is currently in - edit or view only. + * @type {DashboardViewMode} + */ + dashboardViewMode: '=', /** * Whether or not the dashboard this panel is contained on is in 'full screen mode'. * @type {boolean} @@ -115,6 +121,14 @@ uiModules $scope.editUrl = '#management/kibana/objects/' + service.name + '/' + id + '?notFound=' + e.savedObjectType; }); + + /** + * Determines whether or not to show edit controls. + * @returns {boolean} + */ + $scope.isViewOnlyMode = () => { + return $scope.dashboardViewMode === DashboardViewMode.VIEW || $scope.isFullScreenMode; + }; } }; }); diff --git a/src/core_plugins/kibana/public/dashboard/directives/grid.js b/src/core_plugins/kibana/public/dashboard/directives/grid.js index c7ec6a3b625f7..00cb4ed8b5ccf 100644 --- a/src/core_plugins/kibana/public/dashboard/directives/grid.js +++ b/src/core_plugins/kibana/public/dashboard/directives/grid.js @@ -4,6 +4,7 @@ import Binder from 'ui/binder'; import 'gridster'; import uiModules from 'ui/modules'; import { PanelUtils } from 'plugins/kibana/dashboard/components/panel/lib/panel_utils'; +import { DashboardViewMode } from 'plugins/kibana/dashboard/dashboard_view_mode'; const app = uiModules.get('app/dashboard'); @@ -69,13 +70,26 @@ app.directive('dashboardGrid', function ($compile, Notifier) { } }).data('gridster'); + function setResizeCapability() { + if ($scope.dashboardViewMode === DashboardViewMode.VIEW) { + gridster.disable_resize(); + } else { + gridster.enable_resize(); + } + } + // This is necessary to enable text selection within gridster elements // http://stackoverflow.com/questions/21561027/text-not-selectable-from-editable-div-which-is-draggable binder.jqOn($el, 'mousedown', function () { gridster.disable().disable_resize(); }); binder.jqOn($el, 'mouseup', function enableResize() { - gridster.enable().enable_resize(); + gridster.enable(); + setResizeCapability(); + }); + + $scope.$watch('dashboardViewMode', () => { + setResizeCapability(); }); $scope.$watchCollection('state.panels', function (panels) { @@ -168,6 +182,7 @@ app.directive('dashboardGrid', function ($compile, Notifier) { is-full-screen-mode="!chrome.getVisible()" state="state" is-expanded="false" + dashboard-view-mode="dashboardViewMode" toggle-expand="toggleExpandPanel(${panel.panelIndex})" parent-ui-state="uiState"> `; diff --git a/src/core_plugins/kibana/public/dashboard/get_top_nav_config.js b/src/core_plugins/kibana/public/dashboard/get_top_nav_config.js index d98a15ddaed04..d9789143b33e0 100644 --- a/src/core_plugins/kibana/public/dashboard/get_top_nav_config.js +++ b/src/core_plugins/kibana/public/dashboard/get_top_nav_config.js @@ -1,22 +1,52 @@ +import { DashboardViewMode } from './dashboard_view_mode'; /** + * @param {DashboardMode} dashboardMode. * @param kbnUrl - used to change the url. - * @return {Array} - Returns an array of objects for a top nav configuration. - * Note that order matters and the top nav will be displayed in the same order. + * @param {function} modeChange - a function to trigger a dashboard mode change. + * @return {Array} - Returns an array of objects for a top nav configuration, based on the + * mode. */ -export function getTopNavConfig(kbnUrl) { - return [ - getNewConfig(kbnUrl), - getAddConfig(), - getSaveConfig(), - getOpenConfig(), - getShareConfig(), - getOptionsConfig()]; +export function getTopNavConfig(dashboardMode, kbnUrl, modeChange) { + switch (dashboardMode) { + case DashboardViewMode.VIEW: + return [getNewConfig(kbnUrl), getOpenConfig(), getShareConfig(), getEditConfig(modeChange)]; + case DashboardViewMode.EDIT: + return [getNewConfig(kbnUrl), getOpenConfig(), getAddConfig(), getSaveConfig(), getOptionsConfig(), getViewConfig(modeChange)]; + default: + return []; + } +} + +/** + * @returns {kbnTopNavConfig} + */ +function getEditConfig(modeChange) { + return { + key: 'edit', + description: 'Switch to edit mode', + testId: 'dashboardEditMode', + run: () => { + modeChange(DashboardViewMode.EDIT); + } + }; +} + +/** + * @returns {kbnTopNavConfig} + */ +function getViewConfig(modeChange) { + return { + key: 'stop editing', + description: 'Stop editing and switch to view only mode', + testId: 'dashboardViewOnlyMode', + run: () => { + modeChange(DashboardViewMode.VIEW); + } + }; } /** - * - * @param kbnUrl * @returns {kbnTopNavConfig} */ function getNewConfig(kbnUrl) { diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index 0e98af0004318..5bd46f09218a7 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -17,6 +17,7 @@ import uiRoutes from 'ui/routes'; import uiModules from 'ui/modules'; import indexTemplate from 'plugins/kibana/dashboard/index.html'; import { savedDashboardRegister } from 'plugins/kibana/dashboard/services/saved_dashboard_register'; +import { DashboardViewMode } from './dashboard_view_mode'; import { getTopNavConfig } from './get_top_nav_config'; import { createPanelState } from 'plugins/kibana/dashboard/components/panel/lib/panel_state'; import { DashboardConstants } from './dashboard_constants'; @@ -56,7 +57,7 @@ uiRoutes } }); -app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, kbnUrl) { +app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, kbnUrl, safeConfirm) { return { restrict: 'E', controllerAs: 'dashboardApp', @@ -107,7 +108,21 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, $scope.$watch('state.options.darkTheme', setDarkTheme); - $scope.topNavMenu = getTopNavConfig(kbnUrl); + const changeViewMode = (newMode) => { + if ($appStatus.dirty && newMode === DashboardViewMode.VIEW) { + safeConfirm('You have unsaved changes to your dashboard that will be lost if you continue without saving.' + + '\n\nDo you wish to continue?') + .then(() => { + kbnUrl.change('/dashboard/{{id}}', { id: dash.id }); + }).catch(); + } else { + $scope.dashboardViewMode = newMode; + $scope.topNavMenu = getTopNavConfig(newMode, kbnUrl, changeViewMode); + } + }; + + // Brand new dashboards are defaulted to edit mode, existing ones default to view mode. + changeViewMode(dash.id ? DashboardViewMode.VIEW : DashboardViewMode.EDIT); $scope.refresh = _.bindKey(courier, 'fetch'); @@ -202,7 +217,9 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, $scope.$listen(queryFilter, 'fetch', $scope.refresh); $scope.getDashTitle = function () { - return dash.lastSavedTitle || `${dash.title} (unsaved)`; + const displayTitle = dash.lastSavedTitle || `${dash.title} (unsaved)`; + const isEditMode = $scope.dashboardViewMode === DashboardViewMode.EDIT; + return isEditMode ? 'Editing ' + displayTitle : displayTitle; }; $scope.newDashboard = function () { @@ -292,7 +309,7 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, init(); $scope.showEditHelpText = () => { - return !$scope.state.panels.length; + return !$scope.state.panels.length && $scope.dashboardViewMode === DashboardViewMode.EDIT; }; } }; diff --git a/src/core_plugins/kibana/public/dashboard/styles/main.less b/src/core_plugins/kibana/public/dashboard/styles/main.less index f7a480a657457..9a156b3c36fbf 100644 --- a/src/core_plugins/kibana/public/dashboard/styles/main.less +++ b/src/core_plugins/kibana/public/dashboard/styles/main.less @@ -34,14 +34,16 @@ dashboard-grid { } .gs-w { - border: 2px dashed transparent; + + .panel { + border: 2px dashed transparent; + } .panel .panel-heading .btn-group { display: none; } &:hover { - border-color: @kibanaGray4; dashboard-panel { .visualize-show-spy { @@ -50,6 +52,9 @@ dashboard-grid { .panel .panel-heading .btn-group { display: block !important; } + .panel--edit-mode { + border-color: @kibanaGray4; + } } } diff --git a/src/ui/public/kbn_top_nav/kbn_top_nav.js b/src/ui/public/kbn_top_nav/kbn_top_nav.js index 2d4cd3ef91476..02eabdc48e30a 100644 --- a/src/ui/public/kbn_top_nav/kbn_top_nav.js +++ b/src/ui/public/kbn_top_nav/kbn_top_nav.js @@ -101,15 +101,21 @@ module.directive('kbnTopNav', function (Private) { const extensions = getNavbarExtensions($attrs.name); - let controls = _.get($scope, $attrs.config, []); - if (controls instanceof KbnTopNavController) { - controls.addItems(extensions); - } else { - controls = controls.concat(extensions); - } - - $scope.kbnTopNav = new KbnTopNavController(controls); - $scope.kbnTopNav._link($scope, $element); + let initialized = false; + $scope.$watch(() => _.get($scope, $attrs.config, []), function (newValue, oldValue) { + if (initialized && _.isEqual(oldValue, newValue)) return; + + initialized = true; + let controls = newValue; + if (controls instanceof KbnTopNavController) { + controls.addItems(extensions); + } else { + controls = controls.concat(extensions); + } + + $scope.kbnTopNav = new KbnTopNavController(controls); + $scope.kbnTopNav._link($scope, $element); + }); return $scope.kbnTopNav; },