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 1cba6ed1d7ca5f1..642b4f5d048c29c 100644 --- a/src/core_plugins/kibana/public/dashboard/components/panel/panel.html +++ b/src/core_plugins/kibana/public/dashboard/components/panel/panel.html @@ -1,16 +1,16 @@ -
+
{{::savedObj.title}} 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 000000000000000..d554d0399475f15 --- /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 ed1736349991db2..20220dbca43c4fa 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} @@ -99,6 +105,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 48af4f7ff519913..348785f8d265c99 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) { @@ -165,6 +179,7 @@ app.directive('dashboardGrid', function ($compile, Notifier) { `; panel.$el = $compile(panelHtml)($scope); 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 new file mode 100644 index 000000000000000..7944c547669097c --- /dev/null +++ b/src/core_plugins/kibana/public/dashboard/get_top_nav_config.js @@ -0,0 +1,84 @@ +import { DashboardViewMode } from './dashboard_view_mode'; +import { createTopNavExecuteConfig, createTopNavTemplateConfig } from 'ui/kbn_top_nav/kbn_top_nav_config'; + +/** + * @param {DashboardMode} dashboardMode. + * @param kbnUrl - used to change the url. + * @param {(DashboardMode) => void} 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(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 []; + } +} + +function getEditConfig(modeChange) { + return createTopNavExecuteConfig( + 'edit', + 'Switch to edit mode', + 'dashboardEditMode', + () => { modeChange(DashboardViewMode.EDIT); }); +} + +function getViewConfig(modeChange) { + return createTopNavExecuteConfig( + 'preview', + 'Switch to preview mode', + 'dashboardViewOnlyMode', + () => { modeChange(DashboardViewMode.VIEW); }); +} + +function getNewConfig(kbnUrl) { + return createTopNavExecuteConfig( + 'new', + 'New Dashboard', + 'dashboardNewButton', + () => { kbnUrl.change('/dashboard', {}); }); +} + +function getAddConfig() { + return createTopNavTemplateConfig( + 'add', + 'Add a panel to the dashboard', + 'dashboardAddPanelButton', + require('plugins/kibana/dashboard/partials/pick_visualization.html')); +} + +function getSaveConfig() { + return createTopNavTemplateConfig( + 'save', + 'Save Dashboard', + 'dashboardSaveButton', + require('plugins/kibana/dashboard/partials/save_dashboard.html')); +} + +function getOpenConfig() { + return createTopNavTemplateConfig( + 'open', + 'Open Saved Dashboard', + 'dashboardOpenButton', + require('plugins/kibana/dashboard/partials/load_dashboard.html')); +} + +function getShareConfig() { + return createTopNavTemplateConfig( + 'share', + 'Share Dashboard', + 'dashboardShareButton', + require('plugins/kibana/dashboard/partials/share.html')); +} + +function getOptionsConfig() { + return createTopNavTemplateConfig( + 'options', + 'Options', + 'dashboardOptionsButton', + require('plugins/kibana/dashboard/partials/options.html')); +} diff --git a/src/core_plugins/kibana/public/dashboard/index.html b/src/core_plugins/kibana/public/dashboard/index.html index 767fcfcb7a6bf9d..8d51ada0a73ec6a 100644 --- a/src/core_plugins/kibana/public/dashboard/index.html +++ b/src/core_plugins/kibana/public/dashboard/index.html @@ -10,7 +10,7 @@ >
@@ -54,7 +54,7 @@ -
+

Ready to get started?

Click the Add button in the menu bar above to add a visualization to the dashboard.
If you haven't setup a visualization yet visit the "Visualize" tab to create your first visualization.

diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index 83b9bba32fb4bfa..6d13650dada9ebf 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -17,7 +17,10 @@ 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'; + require('ui/saved_objects/saved_object_registry').register(savedDashboardRegister); const app = uiModules.get('app/dashboard', [ @@ -104,37 +107,12 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, $scope.$watch('state.options.darkTheme', setDarkTheme); - $scope.topNavMenu = [{ - key: 'new', - description: 'New Dashboard', - run: function () { kbnUrl.change('/dashboard', {}); }, - testId: 'dashboardNewButton', - }, { - key: 'add', - description: 'Add a panel to the dashboard', - template: require('plugins/kibana/dashboard/partials/pick_visualization.html'), - testId: 'dashboardAddPanelButton', - }, { - key: 'save', - description: 'Save Dashboard', - template: require('plugins/kibana/dashboard/partials/save_dashboard.html'), - testId: 'dashboardSaveButton', - }, { - key: 'open', - description: 'Open Saved Dashboard', - template: require('plugins/kibana/dashboard/partials/load_dashboard.html'), - testId: 'dashboardOpenButton', - }, { - key: 'share', - description: 'Share Dashboard', - template: require('plugins/kibana/dashboard/partials/share.html'), - testId: 'dashboardShareButton', - }, { - key: 'options', - description: 'Options', - template: require('plugins/kibana/dashboard/partials/options.html'), - testId: 'dashboardOptionsButton', - }]; + const changeViewMode = (newMode) => { + $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'); @@ -217,6 +195,11 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, // update data when filters fire fetch event $scope.$listen(queryFilter, 'fetch', $scope.refresh); + $scope.getDashTitle = function () { + const isEditMode = $scope.dashboardViewMode === DashboardViewMode.EDIT; + return isEditMode ? 'Editing ' + dash.lastSavedTitle : dash.lastSavedTitle; + }; + $scope.newDashboard = function () { kbnUrl.change('/dashboard', {}); }; @@ -291,6 +274,10 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, }; init(); + + $scope.showEditHelpText = () => { + 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 a4b61f1e09ea76d..7dd02286fbdaae8 100644 --- a/src/core_plugins/kibana/public/dashboard/styles/main.less +++ b/src/core_plugins/kibana/public/dashboard/styles/main.less @@ -34,10 +34,12 @@ dashboard-grid { } .gs-w { - border: 2px dashed transparent; + + .panel { + border: 2px dashed transparent; + } &:hover { - border-color: @kibanaGray4; dashboard-panel { .visualize-show-spy { @@ -46,6 +48,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 8d3174f2ae35417..68531dce6671911 100644 --- a/src/ui/public/kbn_top_nav/kbn_top_nav.js +++ b/src/ui/public/kbn_top_nav/kbn_top_nav.js @@ -9,27 +9,11 @@ * * Menu items/templates are passed to the kbnTopNav via the config attribute * and should be defined as an array of objects. Each object represents a menu - * item and should have the following properties: + * item and should be of type kbnTopNavConfig. * - * @param {Array|KbnTopNavController} config - * @param {string} config[].key - * - the uniq key for this menu item. - * @param {string} [config[].label] - * - optional, string that will be displayed for the menu button. - * Defaults to the key - * @param {string} [config[].description] - * - optional, used for the screen-reader description of this menu - * item, defaults to "Toggle ${key} view" for templated menu items - * and just "${key}" for programatic menu items - * @param {boolean} [config[].hideButton] - * - optional, set to true to prevent a menu item from being created. - * This allow injecting templates into the navbar that don't have - * an associated template - * @param {function} [config[].run] - * - optional, function to call when the menu item is clicked, defaults - * to toggling the template + * @param {Array|KbnTopNavController} config * - * Programatic control of the navbar can be acheived one of two ways + * Programmatic control of the navbar can be achieved one of two ways */ import _ from 'lodash'; @@ -98,15 +82,24 @@ 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); + /** + * Dashboard makes some dynamic changes to the top nav so we need to watch this + * variable. Since the scope isn't isolate, I have to watch a variable that is defined on + * the parent scope. This is pretty ugly but without isolate scope, I don't see a better + * way to achieve dynamic updates. + */ + $scope.$watch('topNavMenu', function () { + 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); + }); return $scope.kbnTopNav; }, diff --git a/src/ui/public/kbn_top_nav/kbn_top_nav_config.js b/src/ui/public/kbn_top_nav/kbn_top_nav_config.js new file mode 100644 index 000000000000000..1f3bde2d1bdf846 --- /dev/null +++ b/src/ui/public/kbn_top_nav/kbn_top_nav_config.js @@ -0,0 +1,40 @@ +/** + * A configuration object for a top nav component. + * @typedef {Object} KbnTopNavConfig + * @type Object + * @property {string} key - A display string which will be shown in the top nav for this option. + * @property {string} [description] - optional, used for the screen-reader description of this + * menu. Defaults to "Toggle ${key} view" for templated menu items and just "${key}" for + * programmatic menu items + * @property {string} testId - for testing purposes, can be used to retrieve this item. + * @property {Object} [template] - an html template that will be shown when this item is clicked. + * If template is not given then run should be supplied. + * @property {function} [run] - an optional function that will be run when the nav item is clicked. + * Either this or template parameter should be specified. + * @param {boolean} [hideButton] - optional, set to true to prevent a menu item from being created. + * This allow injecting templates into the navbar that don't have an associated template + */ + +/** + * + * @param {string} key + * @param {string} description + * @param {string} testId + * @param {Object} template + * @returns {KbnTopNavConfig} + */ +export function createTopNavTemplateConfig(key, description, testId, template) { + return { key, description, testId, template }; +} + +/** + * + * @param {string} key + * @param {string} description + * @param {string} testId + * @param {function} run + * @returns {KbnTopNavConfig} + */ +export function createTopNavExecuteConfig(key, description, testId, run) { + return { key, description, testId, run }; +}