From aafbe72822b947740b6a63b2f3d13acd28f0c0e8 Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Tue, 5 Sep 2017 16:34:02 -0400
Subject: [PATCH 01/16] Initial check-in to replace gridster with
react-grid-layout and reactify panels
---
package.json | 4 +-
.../dashboard/__tests__/dashboard_panels.js | 126 --------
.../public/dashboard/__tests__/panel.js | 79 -----
.../kibana/public/dashboard/dashboard.html | 17 +-
.../kibana/public/dashboard/dashboard.js | 37 ++-
.../public/dashboard/dashboard_constants.js | 2 +
.../public/dashboard/dashboard_state.js | 4 +-
.../kibana/public/dashboard/grid.js | 273 ------------------
.../__snapshots__/dashboard_grid.test.js.snap | 85 ++++++
.../public/dashboard/grid/dashboard_grid.js | 139 +++++++++
.../dashboard/grid/dashboard_grid.test.js | 105 +++++++
.../dashboard_panel.test.js.snap | 202 +++++++++++++
.../dashboard/panel/__tests__/panel_state.js | 47 +++
.../dashboard/panel/__tests__/panel_utils.js | 17 ++
.../public/dashboard/panel/dashboard_panel.js | 111 +++++++
.../dashboard/panel/dashboard_panel.test.js | 46 +++
.../kibana/public/dashboard/panel/index.js | 3 +-
.../kibana/public/dashboard/panel/panel.html | 80 -----
.../kibana/public/dashboard/panel/panel.js | 111 -------
.../public/dashboard/panel/panel_header.js | 69 +++++
.../public/dashboard/panel/panel_menu_item.js | 27 ++
.../dashboard/panel/panel_options_menu.js | 90 ++++++
.../public/dashboard/panel/panel_state.js | 83 +++++-
.../public/dashboard/panel/panel_utils.js | 20 +-
.../kibana/public/dashboard/styles/index.less | 94 ++++--
.../embeddable/search_embeddable_handler.js | 6 +
.../visualize_embeddable_handler.js | 5 +-
src/jest/config.json | 2 +
.../public/embeddable/embeddable_handler.js | 2 +
src/ui/public/styles/dark-theme.less | 4 +-
test/functional/apps/dashboard/_dashboard.js | 44 +--
test/functional/apps/dashboard/_view_edit.js | 39 +--
.../functional/page_objects/dashboard_page.js | 38 +--
ui_framework/dist/ui_framework.css | 3 +-
.../src/components/popover/_popover.scss | 3 +-
webpackShims/gridster.js | 3 -
36 files changed, 1217 insertions(+), 803 deletions(-)
delete mode 100644 src/core_plugins/kibana/public/dashboard/__tests__/dashboard_panels.js
delete mode 100644 src/core_plugins/kibana/public/dashboard/__tests__/panel.js
delete mode 100644 src/core_plugins/kibana/public/dashboard/grid.js
create mode 100644 src/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap
create mode 100644 src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
create mode 100644 src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js
create mode 100644 src/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.js.snap
create mode 100644 src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_state.js
create mode 100644 src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_utils.js
create mode 100644 src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
create mode 100644 src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js
delete mode 100644 src/core_plugins/kibana/public/dashboard/panel/panel.html
delete mode 100644 src/core_plugins/kibana/public/dashboard/panel/panel.js
create mode 100644 src/core_plugins/kibana/public/dashboard/panel/panel_header.js
create mode 100644 src/core_plugins/kibana/public/dashboard/panel/panel_menu_item.js
create mode 100644 src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js
delete mode 100644 webpackShims/gridster.js
diff --git a/package.json b/package.json
index 38d7d3bd840cc..176ceaa311c98 100644
--- a/package.json
+++ b/package.json
@@ -127,7 +127,6 @@
"glob": "5.0.13",
"glob-all": "3.0.1",
"good-squeeze": "2.1.0",
- "gridster": "0.5.6",
"h2o2": "5.1.1",
"handlebars": "4.0.5",
"hapi": "14.2.0",
@@ -171,6 +170,7 @@
"react-anything-sortable": "1.6.1",
"react-color": "2.11.7",
"react-dom": "15.6.1",
+ "react-grid-layout": "0.14.7",
"react-input-autosize": "1.1.0",
"react-input-range": "1.2.1",
"react-markdown": "2.4.2",
@@ -178,6 +178,8 @@
"react-router": "2.0.0",
"react-router-redux": "4.0.4",
"react-select": "1.0.0-rc.5",
+
+ "react-sizeme": "2.3.4",
"react-sortable": "1.1.0",
"react-test-renderer": "15.6.1",
"react-toggle": "3.0.1",
diff --git a/src/core_plugins/kibana/public/dashboard/__tests__/dashboard_panels.js b/src/core_plugins/kibana/public/dashboard/__tests__/dashboard_panels.js
deleted file mode 100644
index 109e9c34e9624..0000000000000
--- a/src/core_plugins/kibana/public/dashboard/__tests__/dashboard_panels.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import angular from 'angular';
-import expect from 'expect.js';
-import ngMock from 'ng_mock';
-import 'plugins/kibana/dashboard/saved_dashboard/saved_dashboard';
-import { DashboardContainerAPI } from '../dashboard_container_api';
-import { DashboardState } from '../dashboard_state';
-import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from 'plugins/kibana/dashboard/panel/panel_state';
-
-describe('dashboard panels', function () {
- let $scope;
- let $el;
- let AppState;
-
- function compile(dashboard) {
- ngMock.inject(($injector, $rootScope, $controller, $compile, $route) => {
- AppState = $injector.get('AppState');
- $scope = $rootScope.$new();
- $route.current = {
- locals: {
- dash: dashboard
- },
- params: {}
- };
-
- const dashboardState = new DashboardState(dashboard, AppState, false);
- $scope.containerApi = new DashboardContainerAPI(dashboardState);
- $el = angular.element(`
-
-
- `);
- $compile($el)($scope);
- $scope.$digest();
- });
- }
-
- function findPanelWithVisualizationId(id) {
- return $scope.panels.find((panel) => { return panel.id === id; });
- }
-
- beforeEach(() => {
- ngMock.module('kibana');
- });
-
- afterEach(() => {
- $scope.$destroy();
- $el.remove();
- });
-
- it('loads with no vizualizations', function () {
- ngMock.inject((SavedDashboard) => {
- const dash = new SavedDashboard();
- dash.init();
- compile(dash);
- });
- expect($scope.panels.length).to.be(0);
- });
-
- it('loads one vizualization', function () {
- ngMock.inject((SavedDashboard) => {
- const dash = new SavedDashboard();
- dash.init();
- dash.panelsJSON = `[{"col":3,"id":"foo1","row":1,"size_x":2,"size_y":2,"type":"visualization"}]`;
- compile(dash);
- });
- expect($scope.panels.length).to.be(1);
- });
-
- it('loads vizualizations in correct order', function () {
- ngMock.inject((SavedDashboard) => {
- const dash = new SavedDashboard();
- dash.init();
- dash.panelsJSON = `[
- {"col":3,"id":"foo1","row":1,"size_x":2,"size_y":2,"type":"visualization"},
- {"col":5,"id":"foo2","row":1,"size_x":2,"size_y":2,"type":"visualization"},
- {"col":9,"id":"foo3","row":1,"size_x":2,"size_y":2,"type":"visualization"},
- {"col":11,"id":"foo4","row":1,"size_x":2,"size_y":2,"type":"visualization"},
- {"col":1,"id":"foo5","row":1,"size_x":2,"size_y":2,"type":"visualization"},
- {"col":7,"id":"foo6","row":1,"size_x":2,"size_y":2,"type":"visualization"},
- {"col":4,"id":"foo7","row":6,"size_x":3,"size_y":2,"type":"visualization"},
- {"col":1,"id":"foo8","row":8,"size_x":3,"size_y":2,"type":"visualization"},
- {"col":10,"id":"foo9","row":8,"size_x":3,"size_y":2,"type":"visualization"},
- {"col":10,"id":"foo10","row":6,"size_x":3,"size_y":2,"type":"visualization"},
- {"col":4,"id":"foo11","row":8,"size_x":3,"size_y":2,"type":"visualization"},
- {"col":7,"id":"foo12","row":8,"size_x":3,"size_y":2,"type":"visualization"},
- {"col":1,"id":"foo13","row":6,"size_x":3,"size_y":2,"type":"visualization"},
- {"col":7,"id":"foo14","row":6,"size_x":3,"size_y":2,"type":"visualization"},
- {"col":5,"id":"foo15","row":3,"size_x":6,"size_y":3,"type":"visualization"},
- {"col":1,"id":"foo17","row":3,"size_x":4,"size_y":3,"type":"visualization"}]`;
- compile(dash);
- });
- expect($scope.panels.length).to.be(16);
- const foo8Panel = findPanelWithVisualizationId('foo8');
- expect(foo8Panel).to.not.be(null);
- expect(foo8Panel.row).to.be(8);
- expect(foo8Panel.col).to.be(1);
- });
-
- it('initializes visualizations with the default size', function () {
- ngMock.inject((SavedDashboard) => {
- const dash = new SavedDashboard();
- dash.init();
- dash.panelsJSON = `[
- {"col":3,"id":"foo1","row":1,"type":"visualization"},
- {"col":5,"id":"foo2","row":1,"size_x":5,"size_y":9,"type":"visualization"}]`;
- compile(dash);
- });
- expect($scope.panels.length).to.be(2);
- const foo1Panel = findPanelWithVisualizationId('foo1');
- expect(foo1Panel).to.not.be(null);
- expect(foo1Panel.size_x).to.be(DEFAULT_PANEL_WIDTH);
- expect(foo1Panel.size_y).to.be(DEFAULT_PANEL_HEIGHT);
-
- const foo2Panel = findPanelWithVisualizationId('foo2');
- expect(foo2Panel).to.not.be(null);
- expect(foo2Panel.size_x).to.be(5);
- expect(foo2Panel.size_y).to.be(9);
- });
-});
diff --git a/src/core_plugins/kibana/public/dashboard/__tests__/panel.js b/src/core_plugins/kibana/public/dashboard/__tests__/panel.js
deleted file mode 100644
index 35b807ff0cf54..0000000000000
--- a/src/core_plugins/kibana/public/dashboard/__tests__/panel.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import expect from 'expect.js';
-import ngMock from 'ng_mock';
-import Promise from 'bluebird';
-import sinon from 'sinon';
-import noDigestPromise from 'test_utils/no_digest_promises';
-import { DashboardContainerAPI } from '../dashboard_container_api';
-import { DashboardState } from '../dashboard_state';
-import { SavedObjectsClient } from 'ui/saved_objects';
-
-describe('dashboard panel', function () {
- let $scope;
- let $el;
- let parentScope;
- let savedDashboard;
- let AppState;
-
- noDigestPromise.activateForSuite();
-
- function init(mockDocResponse) {
- ngMock.module('kibana');
- ngMock.inject(($rootScope, $compile, Private, $injector) => {
- const SavedDashboard = $injector.get('SavedDashboard');
- AppState = $injector.get('AppState');
- savedDashboard = new SavedDashboard();
- sinon.stub(SavedObjectsClient.prototype, 'get').returns(Promise.resolve(mockDocResponse));
- parentScope = $rootScope.$new();
- parentScope.saveState = sinon.stub();
- const dashboardState = new DashboardState(savedDashboard, AppState, false);
- parentScope.containerApi = new DashboardContainerAPI(dashboardState);
- parentScope.getVisClickHandler = sinon.stub();
- parentScope.getVisBrushHandler = sinon.stub();
- parentScope.registerPanelIndexPattern = sinon.stub();
- parentScope.panel = {
- col: 3,
- id: 'foo1',
- row: 1,
- size_x: 2,
- size_y: 2,
- type: 'visualization'
- };
- $el = $compile(`
-
- `)(parentScope);
- $scope = $el.isolateScope();
- parentScope.$digest();
- });
- }
-
- afterEach(() => {
- SavedObjectsClient.prototype.get.restore();
- $scope.$destroy();
- $el.remove();
- });
-
- it('should not visualize the visualization if it does not exist', function () {
- init({ found: false });
- return $scope.renderPromise.then(() => {
- expect($scope.error).to.be('Could not locate that visualization (id: foo1)');
- parentScope.$digest();
- const content = $el.find('.panel-content');
- expect(content.children().length).to.be(0);
- });
- });
-
- it('should try to visualize the visualization if found', function () {
- init({ id: 'foo1', type: 'visualization', _version: 2, attributes: {} });
- return $scope.renderPromise.then(() => {
- expect($scope.error).not.to.be.ok();
- parentScope.$digest();
- const content = $el.find('.panel-content');
- expect(content.children().length).to.be.greaterThan(0);
- });
- });
-});
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard.html b/src/core_plugins/kibana/public/dashboard/dashboard.html
index 4586d851d2c33..07573ee07e654 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard.html
+++ b/src/core_plugins/kibana/public/dashboard/dashboard.html
@@ -79,13 +79,14 @@
is-full-screen-mode="!chrome.getVisible()"
is-expanded="true"
dashboard-view-mode="dashboardViewMode"
- container-api="containerApi"
- toggle-expand="toggleExpandPanel(expandedPanel.panelIndex)"
+ get-embeddable-handler="getEmbeddableHandler"
+ get-container-api="getContainerApi"
+ on-toggle-expanded="minimizeExpandedPanel"
>
-
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard.js b/src/core_plugins/kibana/public/dashboard/dashboard.js
index 0d52e6cae2165..ea55029f12c2a 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard.js
+++ b/src/core_plugins/kibana/public/dashboard/dashboard.js
@@ -4,8 +4,6 @@ import { uiModules } from 'ui/modules';
import uiRoutes from 'ui/routes';
import chrome from 'ui/chrome';
-import 'plugins/kibana/dashboard/grid';
-import 'plugins/kibana/dashboard/panel/panel';
import 'ui/query_bar';
import { SavedObjectNotFound } from 'ui/errors';
@@ -28,16 +26,36 @@ import { keyCodes } from 'ui_framework/services';
import { DashboardContainerAPI } from './dashboard_container_api';
import * as filterActions from 'ui/doc_table/actions/filter';
import { FilterManagerProvider } from 'ui/filter_manager';
+import { EmbeddableHandlersRegistryProvider } from 'ui/embeddable/embeddable_handlers_registry';
+
+import {
+ DashboardGrid
+} from './grid/dashboard_grid';
+
+import {
+ DashboardPanel
+} from './panel';
const app = uiModules.get('app/dashboard', [
'elasticsearch',
'ngRoute',
+ 'react',
'kibana/courier',
'kibana/config',
'kibana/notify',
'kibana/typeahead',
]);
+_.once(() => {
+ app.directive('dashboardGrid', function (reactDirective) {
+ return reactDirective(DashboardGrid);
+ });
+
+ app.directive('dashboardPanel', function (reactDirective) {
+ return reactDirective(DashboardPanel);
+ });
+})();
+
uiRoutes
.when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {
template: dashboardTemplate,
@@ -95,6 +113,8 @@ app.directive('dashboardApp', function ($injector) {
const docTitle = Private(DocTitleProvider);
const notify = new Notifier({ location: 'Dashboard' });
$scope.queryDocLinks = documentationLinks.query;
+ const embeddableHandlers = Private(EmbeddableHandlersRegistryProvider);
+ $scope.getEmbeddableHandler = panelType => embeddableHandlers.byName[panelType];
const dash = $scope.dash = $route.current.locals.dash;
if (dash.id) {
@@ -110,6 +130,7 @@ app.directive('dashboardApp', function ($injector) {
dashboardState.saveState();
}
);
+ $scope.getContainerApi = () => $scope.containerApi;
// The 'previouslyStored' check is so we only update the time filter on dashboard open, not during
// normal cross app navigation.
@@ -181,13 +202,13 @@ app.directive('dashboardApp', function ($injector) {
!dashboardConfig.getHideWriteControls()
);
- $scope.toggleExpandPanel = (panelIndex) => {
- if ($scope.expandedPanel && $scope.expandedPanel.panelIndex === panelIndex) {
- $scope.expandedPanel = null;
- } else {
- $scope.expandedPanel =
+ $scope.minimizeExpandedPanel = () => {
+ $scope.expandedPanel = null;
+ };
+
+ $scope.expandPanel = (panelIndex) => {
+ $scope.expandedPanel =
dashboardState.getPanels().find((panel) => panel.panelIndex === panelIndex);
- }
};
$scope.updateQueryAndFetch = function (query) {
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_constants.js b/src/core_plugins/kibana/public/dashboard/dashboard_constants.js
index 16542f1edaf94..6bdb743804f25 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard_constants.js
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_constants.js
@@ -5,6 +5,8 @@ export const DashboardConstants = {
CREATE_NEW_DASHBOARD_URL: '/dashboard',
};
+export const DASHBOARD_GRID_COLUMN_COUNT = 12;
+
export function createDashboardEditUrl(id) {
return `/dashboard/${id}`;
}
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_state.js b/src/core_plugins/kibana/public/dashboard/dashboard_state.js
index e8e90a6f71011..0302b356ebe24 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard_state.js
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_state.js
@@ -6,7 +6,7 @@ import { PanelUtils } from './panel/panel_utils';
import moment from 'moment';
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
-import { createPanelState, getPersistedStateId } from 'plugins/kibana/dashboard/panel/panel_state';
+import { createPanelState, getPersistedStateId } from 'plugins/kibana/dashboard/panel';
function getStateDefaults(dashboard, hideWriteControls) {
return {
@@ -298,7 +298,7 @@ export class DashboardState {
*/
addNewPanel(id, type) {
const maxPanelIndex = PanelUtils.getMaxPanelIndex(this.getPanels());
- this.getPanels().push(createPanelState(id, type, maxPanelIndex));
+ this.getPanels().push(createPanelState(id, type, maxPanelIndex, this.getPanels()));
}
removePanel(panelIndex) {
diff --git a/src/core_plugins/kibana/public/dashboard/grid.js b/src/core_plugins/kibana/public/dashboard/grid.js
deleted file mode 100644
index 54fd3fab1ab79..0000000000000
--- a/src/core_plugins/kibana/public/dashboard/grid.js
+++ /dev/null
@@ -1,273 +0,0 @@
-import _ from 'lodash';
-import $ from 'jquery';
-import { Binder } from 'ui/binder';
-import chrome from 'ui/chrome';
-import 'gridster';
-import { uiModules } from 'ui/modules';
-import { DashboardViewMode } from 'plugins/kibana/dashboard/dashboard_view_mode';
-import { PanelUtils } from 'plugins/kibana/dashboard/panel/panel_utils';
-
-const app = uiModules.get('app/dashboard');
-
-app.directive('dashboardGrid', function ($compile, Notifier) {
- return {
- restrict: 'E',
- scope: {
- /**
- * What view mode the dashboard is currently in - edit or view only.
- * @type {DashboardViewMode}
- */
- dashboardViewMode: '=',
- /**
- * Trigger after a panel has been removed from the grid.
- */
- onPanelRemoved: '=',
- /**
- * Contains information about this panel.
- * @type {Array}
- */
- panels: '=',
- /**
- * Call when changes should be propagated to the url and thus saved in state.
- * @type {function}
- */
- saveState: '=',
- /**
- * Expand or collapse a panel, so it either takes up the whole screen or goes back to its
- * natural size.
- * @type {function}
- */
- toggleExpand: '=',
- /**
- * @type {DashboardContainerApi}
- */
- containerApi: '=',
- },
- link: function ($scope, $el) {
- const notify = new Notifier();
- const $container = $el;
- $el = $('').appendTo($container);
-
- const $window = $(window);
- const binder = new Binder($scope);
-
- let gridster; // defined in init()
-
- // number of columns to render
- const COLS = 12;
- // number of pixed between each column/row
- const SPACER = 0;
- // pixels used by all of the spacers (gridster puts have a spacer on the ends)
- const spacerSize = SPACER * COLS;
-
- // debounced layout function is safe to call as much as possible
- const safeLayout = _.debounce(layout, 200);
- /**
- * Mapping of panelIndex to the angular element in the grid.
- */
- const panelElementMapping = {};
-
- // Tell gridster to remove the panel, and cleanup our metadata
- function removePanelFromGrid(panelIndex, silent) {
- const panelElement = panelElementMapping[panelIndex];
- // remove from grister 'silently' (don't reorganize after)
- gridster.remove_widget(panelElement, silent);
- delete panelElementMapping[panelIndex];
- }
-
- $scope.removePanel = (panelIndex) => {
- removePanelFromGrid(panelIndex);
- $scope.onPanelRemoved(panelIndex);
- };
-
- $scope.findPanelByPanelIndex = PanelUtils.findPanelByPanelIndex;
- $scope.isFullScreenMode = !chrome.getVisible();
-
- function init() {
- $el.addClass('gridster');
-
- gridster = $el.gridster({
- max_cols: COLS,
- min_cols: COLS,
- autogenerate_stylesheet: false,
- resize: {
- enabled: true,
- stop: readGridsterChangeHandler
- },
- draggable: {
- handle: '[data-dashboard-panel-drag-handle]',
- stop: readGridsterChangeHandler
- }
- }).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();
- setResizeCapability();
- });
-
- $scope.$watch('dashboardViewMode', () => {
- setResizeCapability();
- });
-
- $scope.$watchCollection('panels', function (panels) {
- const currentPanels = gridster.$widgets.toArray().map(
- el => {
- const panel = PanelUtils.findPanelByPanelIndex(el.panelIndex, $scope.panels);
- if (panel) {
- // A panel may have had its state updated, refresh gridster with the latest values.
- const panelElement = panelElementMapping[panel.panelIndex];
- PanelUtils.refreshElementSizeAndPosition(panel, panelElement);
- return panel;
- } else {
- return { panelIndex: el.panelIndex };
- }
- }
- );
-
- // Panels in the grid that are missing from the panels array. This can happen if the url is modified, and a
- // panel is manually removed.
- const removed = _.difference(currentPanels, panels);
- // Panels that have been added.
- const added = _.difference(panels, currentPanels);
-
- removed.forEach(panel => $scope.removePanel(panel.panelIndex));
-
- if (added.length) {
- // See issue https://github.com/elastic/kibana/issues/2138 and the
- // subsequent fix for why we need to sort here. Short story is that
- // gridster can fail to render widgets in the correct order, depending
- // on the specific order of the panels.
- // See https://github.com/ducksboard/gridster.js/issues/147
- // for some additional back story.
- added.sort((a, b) => {
- if (a.row === b.row) {
- return a.col - b.col;
- } else {
- return a.row - b.row;
- }
- });
- added.forEach(addPanel);
- }
-
- if (added.length || removed.length) {
- $scope.saveState();
- }
- layout();
- });
-
- $scope.$on('$destroy', function () {
- safeLayout.cancel();
- $window.off('resize', safeLayout);
-
- if (!gridster) return;
- gridster.$widgets.each(function (i, widget) {
- const panelElement = panelElementMapping[widget.panelIndex];
- // stop any animations
- panelElement.stop();
- removePanelFromGrid(widget.panelIndex, true);
- });
- });
-
- safeLayout();
- $window.on('resize', safeLayout);
- $scope.$on('ready:vis', safeLayout);
- $scope.$on('globalNav:update', safeLayout);
- $scope.$on('reLayout', safeLayout);
- }
-
- // tell gridster to add the panel, and create additional meatadata like $scope
- function addPanel(panel) {
- PanelUtils.initializeDefaults(panel);
- const panelHtml = `
-
-
- `;
- const panelElement = $compile(panelHtml)($scope);
- panelElementMapping[panel.panelIndex] = panelElement;
- // Store the panelIndex on the widget so it can be used to retrieve the panelElement
- // from the mapping.
- panelElement[0].panelIndex = panel.panelIndex;
-
- // tell gridster to use the widget
- gridster.add_widget(panelElement, panel.size_x, panel.size_y, panel.col, panel.row);
-
- // Gridster may change the position of the widget when adding it, make sure the panel
- // contains the latest info.
- PanelUtils.refreshSizeAndPosition(panel, panelElement);
- }
-
- // When gridster tell us it made a change, update each of the panel objects
- function readGridsterChangeHandler() {
- // ensure that our panel objects keep their size in sync
- gridster.$widgets.each(function (i, widget) {
- const panel = PanelUtils.findPanelByPanelIndex(widget.panelIndex, $scope.panels);
- const panelElement = panelElementMapping[panel.panelIndex];
- PanelUtils.refreshSizeAndPosition(panel, panelElement);
- });
-
- $scope.saveState();
- }
-
- // calculate the position and sizing of the gridster el, and the columns within it
- // then tell gridster to "reflow" -- which is definitely not supported.
- // we may need to consider using a different library
- function reflowGridster() {
- if ($container.hasClass('ng-hide')) {
- return;
- }
-
- // https://github.com/gcphost/gridster-responsive/blob/97fe43d4b312b409696b1d702e1afb6fbd3bba71/jquery.gridster.js#L1208-L1235
- const g = gridster;
-
- g.options.widget_margins = [SPACER / 2, SPACER / 2];
- g.options.widget_base_dimensions = [($container.width() - spacerSize) / COLS, 100];
- g.min_widget_width = (g.options.widget_margins[0] * 2) + g.options.widget_base_dimensions[0];
- g.min_widget_height = (g.options.widget_margins[1] * 2) + g.options.widget_base_dimensions[1];
-
- g.$widgets.each(function (i, widget) {
- g.resize_widget($(widget));
- });
-
- g.generate_grid_and_stylesheet();
- g.generate_stylesheet({ namespace: '.gridster' });
-
- g.get_widgets_from_DOM();
- // We can't call this method if the gridmap is empty. This was found
- // when the user double clicked the "New Dashboard" icon. See
- // https://github.com/elastic/kibana4/issues/390
- if (gridster.gridmap.length > 0) g.set_dom_grid_height();
- g.drag_api.set_limits(COLS * g.min_widget_width);
- }
-
- function layout() {
- const complete = notify.event('reflow dashboard');
- reflowGridster();
- readGridsterChangeHandler();
- complete();
- }
-
- init();
- }
- };
-});
diff --git a/src/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap b/src/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap
new file mode 100644
index 0000000000000..e0c505e73cc69
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap
@@ -0,0 +1,85 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders DashboardGrid 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders DashboardGrid with no visualizations 1`] = `
+
+`;
diff --git a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
new file mode 100644
index 0000000000000..f0a24610aac34
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
@@ -0,0 +1,139 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import _ from 'lodash';
+import ReactGridLayout from 'react-grid-layout';
+import { PanelUtils } from '../panel/panel_utils';
+import { DashboardViewMode } from '../dashboard_view_mode';
+import { DashboardPanel } from '../panel/dashboard_panel';
+import { DASHBOARD_GRID_COLUMN_COUNT } from '../dashboard_constants';
+import sizeMe from 'react-sizeme';
+
+const config = { monitorWidth: true };
+
+function ResponsiveGrid({ size, isViewMode, buildLayoutFromPanels, onLayoutChange, children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+// Using sizeMe sets up the grid to be re-rendered automatically not only when the window size changes, but also
+// when the container size changes, so it works for Full Screen mode switches.
+const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid);
+
+
+export class DashboardGrid extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ layout: this.buildLayoutFromPanels()
+ };
+ }
+
+ buildLayoutFromPanels() {
+ return _.map(this.props.panels, panel => {
+ if (panel.size_x || panel.size_y || panel.col || panel.row) {
+ PanelUtils.convertOldPanelData(panel);
+ }
+ return panel.gridData;
+ });
+ }
+
+ onLayoutChange = (layout) => {
+ const { panels, getContainerApi } = this.props;
+
+ const containerApi = getContainerApi();
+
+ layout.forEach(panelLayout => {
+ const panelUpdated = _.find(panels, panel => panel.panelIndex.toString() === panelLayout.i);
+ panelUpdated.gridData = {
+ x: panelLayout.x,
+ y: panelLayout.y,
+ w: panelLayout.w,
+ h: panelLayout.h,
+ i: panelLayout.i,
+ version: panelLayout.version
+ };
+ panelUpdated.gridData.version = 6.0;
+ containerApi.updatePanel(panelUpdated.panelIndex, panelUpdated);
+ });
+ };
+
+ generateDOM() {
+ const {
+ panels,
+ onPanelRemoved,
+ expandPanel,
+ isFullScreenMode,
+ getEmbeddableHandler,
+ getContainerApi,
+ dashboardViewMode
+ } = this.props;
+
+ // Part of our unofficial API - need to render in a consistent order for plugins.
+ const panelsInOrder = panels.slice(0);
+ panelsInOrder.sort((panelA, panelB) => {
+ if (panelA.gridData.y === panelB.gridData.y) {
+ return panelA.gridData.x - panelB.gridData.x;
+ } else {
+ return panelA.gridData.y - panelB.gridData.y;
+ }
+ });
+
+ return _.map(panelsInOrder, panel => {
+ return (
+
+
+
+ );
+ });
+ }
+
+ render() {
+ const { dashboardViewMode } = this.props;
+ const isViewMode = dashboardViewMode === DashboardViewMode.EDIT;
+ return (
+
+ {this.generateDOM()}
+
+ );
+ }
+}
+
+DashboardGrid.propTypes = {
+ isFullScreenMode: PropTypes.bool.isRequired,
+ panels: PropTypes.array.isRequired,
+ getContainerApi: PropTypes.func.isRequired,
+ getEmbeddableHandler: PropTypes.func.isRequired,
+ dashboardViewMode: PropTypes.oneOf([DashboardViewMode.EDIT, DashboardViewMode.VIEW]).isRequired,
+ expandPanel: PropTypes.func.isRequired,
+ onPanelRemoved: PropTypes.func.isRequired,
+};
+
diff --git a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js
new file mode 100644
index 0000000000000..7ad93bd582954
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js
@@ -0,0 +1,105 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { DashboardViewMode } from '../dashboard_view_mode';
+import { PanelUtils } from '../panel/panel_utils';
+
+import { DashboardGrid } from './dashboard_grid';
+import { DashboardPanel } from '../panel/dashboard_panel';
+
+const getContainerApi = () => {
+ return {
+ addFilter: () => {},
+ getAppState: () => {},
+ createChildUistate: () => {},
+ registerPanelIndexPattern: () => {},
+ updatePanel: () => {}
+ };
+};
+
+const embeddableHandlerMock = {
+ getEditPath: () => {},
+ getTitleFor: () => {},
+ render: jest.fn()
+};
+
+function getProps(props = {}) {
+ const defaultTestProps = {
+ dashboardViewMode: DashboardViewMode.EDIT,
+ isFullScreenMode: false,
+ panels: [{
+ gridData: { x: 0, y: 0, w: 6, h: 6, i: 1 },
+ panelIndex: '1',
+ type: 'visualization',
+ id: '123'
+ },{
+ gridData: { x: 6, y: 6, w: 6, h: 6, i: 2 },
+ panelIndex: '2',
+ type: 'visualization',
+ id: '456'
+ }],
+ getEmbeddableHandler: () => embeddableHandlerMock,
+ isExpanded: false,
+ getContainerApi,
+ expandPanel: () => {},
+ onPanelRemoved: () => {}
+ };
+ return Object.assign(defaultTestProps, props);
+}
+
+test('renders DashboardGrid', () => {
+ const component = shallow( );
+ expect(component).toMatchSnapshot();
+ const panelElements = component.find(DashboardPanel);
+ expect(panelElements.length).toBe(2);
+});
+
+test('renders DashboardGrid with no visualizations', () => {
+ const component = shallow( );
+ expect(component).toMatchSnapshot();
+});
+
+function createOldPanelData(col, id, row, sizeX, sizeY, panelIndex) {
+ return { col, id, row, size_x: sizeX, size_y: sizeY, type: 'visualization', panelIndex };
+}
+
+function findPanelWithId(panelElements, id) {
+ for (let i = 0; i < panelElements.length; i++) {
+ if (panelElements.at(i).props().panel.id === id) {
+ return panelElements.at(i);
+ }
+ }
+ return null;
+}
+
+test('Loads old panel data in the right order', () => {
+ const panelData = [
+ createOldPanelData(3, 'foo1', 1, 2, 2, 1),
+ createOldPanelData(5, 'foo2', 1, 2, 2, 2),
+ createOldPanelData(9, 'foo3', 1, 2, 2, 3),
+ createOldPanelData(11, 'foo4', 1, 2, 2, 4),
+ createOldPanelData(1, 'foo5', 1, 2, 2, 5),
+ createOldPanelData(7, 'foo6', 1, 2, 2, 6),
+ createOldPanelData(4, 'foo7', 6, 3, 2, 7),
+ createOldPanelData(1, 'foo8', 8, 3, 2, 8),
+ createOldPanelData(10, 'foo9', 8, 3, 2, 9),
+ createOldPanelData(10, 'foo10', 6, 3, 2, 10),
+ createOldPanelData(4, 'foo11', 8, 3, 2, 11),
+ createOldPanelData(7, 'foo12', 8, 3, 2, 12),
+ createOldPanelData(1, 'foo13', 6, 3, 2, 13),
+ createOldPanelData(7, 'foo14', 6, 3, 2, 14),
+ createOldPanelData(5, 'foo15', 3, 6, 3, 15),
+ createOldPanelData(1, 'foo17', 3, 4, 3, 16)
+ ];
+ panelData.forEach(oldPanel => PanelUtils.convertOldPanelData(oldPanel));
+ const props = getProps({ panels: panelData });
+
+ const component = shallow( );
+ const panelElements = component.find(DashboardPanel);
+ expect(panelElements.length).toBe(16);
+
+ const foo8PanelElement = findPanelWithId(panelElements, 'foo8');
+ const panel = foo8PanelElement.props().panel;
+ expect(panel.row).toBe(undefined);
+ expect(panel.gridData.y).toBe(7);
+ expect(panel.gridData.x).toBe(0);
+});
diff --git a/src/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.js.snap b/src/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.js.snap
new file mode 100644
index 0000000000000..b2ab4da0548d3
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.js.snap
@@ -0,0 +1,202 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DashboardPanel matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ }
+ className="dashboardPanelPopOver"
+ closePopover={[Function]}
+ isOpen={false}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit Visualization
+
+
+
+
+
+
+
+
+
+ Full screen
+
+
+
+
+
+
+
+
+
+ Delete from dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_state.js b/src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_state.js
new file mode 100644
index 0000000000000..29665ddd0550c
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_state.js
@@ -0,0 +1,47 @@
+import expect from 'expect.js';
+
+import { createPanelState } from '../panel_state';
+
+function createPanelWithDimensions(x, y, w, h) {
+ return {
+ gridData: {
+ x, y, w, h
+ }
+ };
+}
+
+describe('Panel state', function () {
+ it('finds a spot on the right', function () {
+ // Default setup after a single panel, of default size, is on the grid
+ const panels = [createPanelWithDimensions(0, 0, 6, 6)];
+
+ const panel = createPanelState('1', 'a type', '1', panels);
+ expect(panel.gridData.x).to.equal(6);
+ expect(panel.gridData.y).to.equal(0);
+ });
+
+ it('finds a spot on the right when the panel is taller than any other panel on the grid', function () {
+ // Should be a little empty spot on the right.
+ const panels = [
+ createPanelWithDimensions(0, 0, 6, 9),
+ createPanelWithDimensions(6, 0, 6, 6),
+ ];
+
+ const panel = createPanelState('1', 'a type', '1', panels);
+ expect(panel.gridData.x).to.equal(6);
+ expect(panel.gridData.y).to.equal(6);
+ });
+
+ it('finds an empty spot in the middle of the grid', function () {
+ const panels = [
+ createPanelWithDimensions(0, 0, 12, 1),
+ createPanelWithDimensions(0, 1, 1, 6),
+ createPanelWithDimensions(10, 1, 1, 6),
+ createPanelWithDimensions(0, 11, 12, 1),
+ ];
+
+ const panel = createPanelState('1', 'a type', '1', panels);
+ expect(panel.gridData.x).to.equal(1);
+ expect(panel.gridData.y).to.equal(1);
+ });
+});
diff --git a/src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_utils.js b/src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_utils.js
new file mode 100644
index 0000000000000..f8ebf46eed645
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_utils.js
@@ -0,0 +1,17 @@
+import { PanelUtils } from '../panel_utils';
+import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../panel_state';
+
+describe('PanelUtils', function () {
+ it('convertOldPanelData gives supplies width and height when missing', () => {
+ const panelData = [
+ { col: 3, id: 'foo1', row: 1, type: 'visualization', panelIndex: 1 },
+ { col: 3, id: 'foo2', row: 1, size_x: 3, size_y: 2, type: 'visualization', panelIndex: 2 }
+ ];
+ panelData.forEach(oldPanel => PanelUtils.convertOldPanelData(oldPanel));
+ expect(panelData[0].gridData.w = DEFAULT_PANEL_WIDTH);
+ expect(panelData[0].gridData.h = DEFAULT_PANEL_HEIGHT);
+
+ expect(panelData[1].gridData.w = 3);
+ expect(panelData[1].gridData.h = 2);
+ });
+});
diff --git a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
new file mode 100644
index 0000000000000..9430047da62d9
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
@@ -0,0 +1,111 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import { DashboardViewMode } from '../dashboard_view_mode';
+import { PanelHeader } from './panel_header';
+
+export class DashboardPanel extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {};
+ this.embeddable = null;
+ this.embeddableHandler = null;
+ this.parentNode = null;
+ }
+
+ async componentWillMount() {
+ const { getEmbeddableHandler, panel, getContainerApi } = this.props;
+
+ this.containerApi = getContainerApi();
+ this.embeddableHandler = getEmbeddableHandler(panel.type);
+ const editUrl = await this.embeddableHandler.getEditPath(panel.id);
+ const title = await this.embeddableHandler.getTitleFor(panel.id);
+
+ this.setState({ editUrl, title });
+
+ this.destroyEmbeddable = await this.embeddableHandler.render(
+ this.panelElement,
+ panel,
+ this.containerApi);
+ }
+
+ isViewOnlyMode() {
+ return this.props.dashboardViewMode === DashboardViewMode.VIEW || this.props.isFullScreenMode;
+ }
+
+ getParentNode() {
+ if (!this.parentNode) {
+ this.parentNode = ReactDOM.findDOMNode(this).parentNode;
+ }
+ return this.parentNode;
+ }
+
+ toggleExpandedPanel = () => this.props.onToggleExpanded(this.props.panel.panelIndex);
+ deletePanel = () => {
+ this.props.onDeletePanel(this.props.panel.panelIndex);
+ };
+ onEditPanel = () => window.location = this.state.editUrl;
+
+ /**
+ * Setting the zIndex on onFocus and onBlur allows popups, like the panel menu, to appear above other panels in the
+ * grid.
+ */
+ onFocus = () => {
+ this.getParentNode().style.zIndex = 1;
+ };
+ onBlur = () => {
+ this.getParentNode().style.zIndex = 'auto';
+ };
+
+ componentWillUnmount() {
+ this.destroyEmbeddable();
+ }
+
+ render() {
+ const { title } = this.state;
+ const { dashboardViewMode, isFullScreenMode, isExpanded } = this.props;
+ const classes = classNames('panel panel-default', this.props.className, {
+ 'panel--edit-mode': !this.isViewOnlyMode()
+ });
+ return (
+
+
+
+
this.panelElement = panelElement}
+ />
+
+
+ );
+ }
+}
+
+DashboardPanel.propTypes = {
+ dashboardViewMode: PropTypes.oneOf([DashboardViewMode.EDIT, DashboardViewMode.VIEW]).isRequired,
+ isFullScreenMode: PropTypes.bool.isRequired,
+ panel: PropTypes.object.isRequired,
+ getEmbeddableHandler: PropTypes.func.isRequired,
+ isExpanded: PropTypes.bool.isRequired,
+ getContainerApi: PropTypes.func.isRequired,
+ onToggleExpanded: PropTypes.func.isRequired,
+ onDeletePanel: PropTypes.func
+};
diff --git a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js
new file mode 100644
index 0000000000000..38a67c4f9fc91
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { DashboardViewMode } from '../dashboard_view_mode';
+import { DashboardPanel } from './dashboard_panel';
+
+const containerApiMock = {
+ addFilter: () => {},
+ getAppState: () => {},
+ createChildUistate: () => {},
+ registerPanelIndexPattern: () => {},
+ updatePanel: () => {}
+};
+
+const embeddableHandlerMock = {
+ getEditPath: () => {},
+ getTitleFor: () => {},
+ render: jest.fn()
+};
+
+function getProps(props = {}) {
+ const defaultTestProps = {
+ dashboardViewMode: DashboardViewMode.EDIT,
+ isFullScreenMode: false,
+ panel: {
+ gridData: { x: 0, y: 0, w: 6, h: 6, i: 1 },
+ panelIndex: '1',
+ type: 'visualization',
+ id: 'foo1'
+ },
+ getEmbeddableHandler: () => embeddableHandlerMock,
+ isExpanded: false,
+ getContainerApi: () => containerApiMock,
+ onToggleExpanded: () => {},
+ onDeletePanel: () => {}
+ };
+ return Object.assign(props, defaultTestProps);
+}
+
+test('DashboardPanel matches snapshot', () => {
+ const component = mount(
);
+ expect(component).toMatchSnapshot();
+});
+
+test('and calls render', () => {
+ expect(embeddableHandlerMock.render.mock.calls.length).toBe(1);
+});
diff --git a/src/core_plugins/kibana/public/dashboard/panel/index.js b/src/core_plugins/kibana/public/dashboard/panel/index.js
index 01563f8b57527..a83e0b5bae450 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/index.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/index.js
@@ -1 +1,2 @@
-import './panel';
+export { DashboardPanel } from './dashboard_panel';
+export { createPanelState, getPersistedStateId } from './panel_state';
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel.html b/src/core_plugins/kibana/public/dashboard/panel/panel.html
deleted file mode 100644
index d20d0fdde1bee..0000000000000
--- a/src/core_plugins/kibana/public/dashboard/panel/panel.html
+++ /dev/null
@@ -1,80 +0,0 @@
-
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel.js b/src/core_plugins/kibana/public/dashboard/panel/panel.js
deleted file mode 100644
index 2c2a53e1b643a..0000000000000
--- a/src/core_plugins/kibana/public/dashboard/panel/panel.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import 'ui/visualize';
-import 'ui/doc_table';
-import 'plugins/kibana/visualize/saved_visualizations';
-import 'plugins/kibana/discover/saved_searches';
-import { uiModules } from 'ui/modules';
-import panelTemplate from 'plugins/kibana/dashboard/panel/panel.html';
-import { savedObjectManagementRegistry } from 'plugins/kibana/management/saved_object_registry';
-import { DashboardViewMode } from '../dashboard_view_mode';
-import { EmbeddableHandlersRegistryProvider } from 'ui/embeddable/embeddable_handlers_registry';
-
-uiModules
-.get('app/dashboard')
-.directive('dashboardPanel', function (Notifier, Private, $injector) {
- const services = savedObjectManagementRegistry.all().map(function (serviceObj) {
- const service = $injector.get(serviceObj.service);
- return {
- type: service.type,
- name: serviceObj.service
- };
- });
-
- return {
- 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}
- */
- isFullScreenMode: '=',
- /**
- * Contains information about this panel.
- * @type {PanelState}
- */
- panel: '=',
- /**
- * Handles removing this panel from the grid.
- * @type {function}
- */
- remove: '&',
- /**
- * Expand or collapse the current panel, so it either takes up the whole screen or goes back to its
- * natural size.
- * @type {function}
- */
- toggleExpand: '&',
- /**
- * @type {boolean}
- */
- isExpanded: '=',
- /**
- * @type {DashboardContainerApi}
- */
- containerApi: '='
- },
- link: function ($scope, element) {
- if (!$scope.panel.id || !$scope.panel.type) return;
-
- $scope.isViewOnlyMode = () => {
- return $scope.dashboardViewMode === DashboardViewMode.VIEW || $scope.isFullScreenMode;
- };
-
- const panelId = $scope.panel.id;
-
- // TODO: This function contains too much internal panel knowledge. Logic should be pushed to embeddable handlers.
- const handleError = (error) => {
- $scope.error = error.message;
-
- // Dashboard listens for this broadcast, once for every visualization (pendingVisCount).
- // We need to broadcast even in the event of an error or it'll never fetch the data for
- // other visualizations.
- $scope.$root.$broadcast('ready:vis');
-
- // If the savedObjectType matches the panel type, this means the object itself has been deleted,
- // so we shouldn't even have an edit link. If they don't match, it means something else is wrong
- // with the object (but the object still exists), so we link to the object editor instead.
- const objectItselfDeleted = error.savedObjectType === $scope.panel.type;
- if (objectItselfDeleted) return;
-
- const type = $scope.panel.type;
- const service = services.find(service => service.type === type);
- if (!service) return;
-
- $scope.editUrl = '#management/kibana/objects/' + service.name + '/' + panelId + '?notFound=' + error.savedObjectType;
- };
-
- const embeddableHandlers = Private(EmbeddableHandlersRegistryProvider);
- const embeddableHandler = embeddableHandlers.byName[$scope.panel.type];
- if (!embeddableHandler) {
- handleError(new Error(`No embeddable handler for panel type ${$scope.panel.type} was found.`));
- return;
- }
- embeddableHandler.getEditPath(panelId).then(path => {
- $scope.editUrl = path;
- });
- embeddableHandler.getTitleFor(panelId).then(title => {
- $scope.title = title;
- });
- $scope.renderPromise = embeddableHandler.render(
- element.find('#embeddedPanel').get(0),
- $scope.panel,
- $scope.containerApi)
- .catch(handleError);
- }
- };
-});
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header.js b/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
new file mode 100644
index 0000000000000..aff4163126348
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import { KuiKeyboardAccessible } from 'ui_framework/components';
+import { PanelOptionsMenu } from './panel_options_menu';
+
+export class PanelHeader extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ getOptionsDropDown() {
+ return (
+
+ );
+ }
+
+ getExpandToggle() {
+ const { isExpanded } = this.props;
+ const classes = classNames('kuiIcon', null, { 'fa-expand': !isExpanded, 'fa-compress': isExpanded });
+ return (
+
+
+
+
+
+ );
+ }
+
+ render() {
+ return (
+
+
+ {this.props.title}
+
+
+
+ {this.props.isViewOnlyMode ? this.getExpandToggle() : this.getOptionsDropDown()}
+
+
+ );
+ }
+}
+
+PanelHeader.propTypes = {
+ title: PropTypes.string,
+ onEditPanel: PropTypes.func.isRequired,
+ onDeletePanel: PropTypes.func.isRequired,
+ onToggleExpand: PropTypes.func.isRequired,
+ isExpanded: PropTypes.bool.isRequired
+};
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_menu_item.js b/src/core_plugins/kibana/public/dashboard/panel/panel_menu_item.js
new file mode 100644
index 0000000000000..df80d5f8cd6ee
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_menu_item.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { KuiMenuItem } from 'ui_framework/components';
+
+export function PanelMenuItem({ iconClass, onClick, label, ...props }) {
+ const iconClasses = classNames('kuiButton__icon kuiIcon', iconClass);
+ return (
+
+
+ {label}
+
+ );
+}
+
+PanelMenuItem.propTypes = {
+ iconClass: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired,
+ label: PropTypes.string.isRequired
+};
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js b/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js
new file mode 100644
index 0000000000000..1596e0a90ae99
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js
@@ -0,0 +1,90 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { PanelMenuItem } from './panel_menu_item';
+import {
+ KuiPopover,
+ KuiMenu,
+ KuiKeyboardAccessible
+} from 'ui_framework/components';
+
+export class PanelOptionsMenu extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ showMenu: false
+ };
+ }
+
+ toggleMenu = () => {
+ this.setState({ showMenu: !this.state.showMenu });
+ };
+ closeMenu = () => this.setState({ showMenu: false });
+
+ getEditVisualizationMenuItem() {
+ return (
+
+ );
+ }
+
+ getDeleteMenuItem() {
+ return (
+
+ );
+ }
+
+ getToggleExpandMenuItem() {
+ return (
+
+ );
+ }
+
+ render() {
+ return (
+
+
+
+ )}
+ isOpen={this.state.showMenu}
+ anchorPosition="right"
+ closePopover={this.closeMenu}
+ >
+
+ {this.getEditVisualizationMenuItem()}
+ {this.getToggleExpandMenuItem()}
+ {this.props.isExpanded ? null : this.getDeleteMenuItem()}
+
+
+ );
+ }
+}
+
+PanelOptionsMenu.propTypes = {
+ onEditPanel: PropTypes.func.isRequired,
+ onToggleExpandPanel: PropTypes.func.isRequired,
+ isExpanded: PropTypes.bool.isRequired,
+ onDeletePanel: PropTypes.func, // Not available when the panel is expanded.
+};
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_state.js b/src/core_plugins/kibana/public/dashboard/panel/panel_state.js
index 25ece385e3d50..64f4ddcf20c8d 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_state.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_state.js
@@ -1,36 +1,101 @@
export const DEFAULT_PANEL_WIDTH = 6;
export const DEFAULT_PANEL_HEIGHT = 3;
+import { DASHBOARD_GRID_COLUMN_COUNT } from '../dashboard_constants';
+
/**
* Represents a panel on a grid. Keeps track of position in the grid and what visualization it
* contains.
*
* @typedef {Object} PanelState
* @property {number} id - Id of the visualization contained in the panel.
- * @property {Element} $el - A reference to the gridster widget holding this panel. Used to
- * update the size and column attributes. TODO: move out of panel state as this couples state to ui.
* @property {string} type - Type of the visualization in the panel.
* @property {number} panelIndex - Unique id to represent this panel in the grid. Note that this is
* NOT the index in the panels array. While it may initially represent that, it is not
* updated with changes in a dashboard, and is simply used as a unique identifier. The name
* remains as panelIndex for backward compatibility reasons - changing it can break reporting.
- * @property {number} size_x - Width of the panel.
- * @property {number} size_y - Height of the panel.
- * @property {number} col - Column index in the grid.
- * @property {number} row - Row index in the grid.
+ * @property {Object} gridData
+ * @property {number} gridData.w - Width of the panel.
+ * @property {number} gridData.h - Height of the panel.
+ * @property {number} gridData.x - Column position of the panel.
+ * @property {number} gridData.y - Row position of the panel.
*/
+// Look for the smallest y and x value where the default panel will fit.
+function findTopLeftMostOpenSpace(width, height, currentPanels) {
+ let maxY = -1;
+
+ for (let i = 0; i < currentPanels.length; i++) {
+ const panel = currentPanels[i];
+ maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY);
+ }
+
+ // Handle case of empty grid.
+ if (maxY < 0) {
+ return { x: 0, y: 0 };
+ }
+
+ const grid = new Array(maxY);
+ for (let y = 0; y < maxY; y++) {
+ grid[y] = new Array(DASHBOARD_GRID_COLUMN_COUNT).fill(0);
+ }
+
+ for (let i = 0; i < currentPanels.length; i++) {
+ const panel = currentPanels[i];
+ for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) {
+ for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) {
+ grid[y][x] = 1;
+ }
+ }
+ }
+
+ for (let y = 0; y < maxY; y++) {
+ for (let x = 0; x < DASHBOARD_GRID_COLUMN_COUNT; x++) {
+ if (grid[y][x] === 1) {
+ // Space is filled
+ continue;
+ } else {
+ for (let h = y; h < Math.min(y + height, maxY); h++) {
+ for (let w = x; w < Math.min(x + width, DASHBOARD_GRID_COLUMN_COUNT); w++) {
+ const spaceIsEmpty = grid[h][w] === 0;
+ const fitsPanelWidth = w === x + width - 1;
+ // If the panel is taller than any other panel in the current grid, it can still fit in the space, hence
+ // we check the minimum of maxY and the panel height.
+ const fitsPanelHeight = h === Math.min(y + height - 1, maxY - 1);
+
+ if (spaceIsEmpty && fitsPanelWidth && fitsPanelHeight) {
+ // Found space
+ return { x, y };
+ } else if (grid[h][w] === 1) {
+ // x, y spot doesn't work, break.
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ return { x: 0, y: Infinity };
+}
+
/**
* Creates and initializes a basic panel state.
* @param {number} id
* @param {string} type
* @param {number} panelIndex
+ * @param {Array} currentPanels
* @return {PanelState}
*/
-export function createPanelState(id, type, panelIndex) {
+export function createPanelState(id, type, panelIndex, currentPanels) {
+ const { x, y } = findTopLeftMostOpenSpace(DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT, currentPanels);
return {
- size_x: DEFAULT_PANEL_WIDTH,
- size_y: DEFAULT_PANEL_HEIGHT,
+ gridData: {
+ w: DEFAULT_PANEL_WIDTH,
+ h: DEFAULT_PANEL_HEIGHT,
+ x,
+ y,
+ i: panelIndex.toString()
+ },
panelIndex: panelIndex,
type: type,
id: id
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js b/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
index b2783c8c17d81..d27a5a98e5803 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
@@ -8,8 +8,9 @@ export class PanelUtils {
* @param {PanelState} panel
*/
static initializeDefaults(panel) {
- panel.size_x = panel.size_x || DEFAULT_PANEL_WIDTH;
- panel.size_y = panel.size_y || DEFAULT_PANEL_HEIGHT;
+ panel.gridData = panel.gridData || {};
+ panel.gridData.w = panel.gridData.w || DEFAULT_PANEL_WIDTH;
+ panel.gridData.h = panel.gridData.h || DEFAULT_PANEL_HEIGHT;
if (!panel.id) {
// In the interest of backwards comparability
@@ -23,6 +24,21 @@ export class PanelUtils {
}
}
+ static convertOldPanelData(panel) {
+ panel.gridData = {
+ x: panel.col - 1,
+ y: panel.row - 1,
+ w: panel.size_x || DEFAULT_PANEL_WIDTH,
+ h: panel.size_y || DEFAULT_PANEL_HEIGHT,
+ i: panel.panelIndex.toString(),
+ version: 6,
+ };
+ delete panel.size_x;
+ delete panel.size_y;
+ delete panel.row;
+ delete panel.col;
+ }
+
/**
* Ensures that the panel object has the latest size/pos info.
* @param {PanelState} panel
diff --git a/src/core_plugins/kibana/public/dashboard/styles/index.less b/src/core_plugins/kibana/public/dashboard/styles/index.less
index 39d67124f6a79..957107771809e 100644
--- a/src/core_plugins/kibana/public/dashboard/styles/index.less
+++ b/src/core_plugins/kibana/public/dashboard/styles/index.less
@@ -2,6 +2,9 @@
@import (reference) "~ui/styles/mixins";
@import "~ui/styles/local_search.less";
+@import "../../../../../../node_modules/react-grid-layout/css/styles.css";
+@import "../../../../../../node_modules/react-resizable/css/styles.css";
+
.fullScreenModePlaceholder {
text-align: center;
width: 100%;
@@ -10,6 +13,13 @@
position: absolute;
}
+/**
+ * 1. If we don't give the resizable handler a larger z index value the spy toggle will take over the mouse hover.
+ */
+.react-resizable-handle {
+ z-index: 10; /* 1 */
+}
+
.exitFullScreenMode {
height: 40px;
left: 0px;
@@ -52,7 +62,6 @@
*/
.exitFullScreenModeText {
- display: block;
background: @globalColorBlue;
color: tint(@globalColorBlue, 70%);
line-height: 40px;
@@ -77,7 +86,7 @@
width: 450px;
}
-dashboard-grid {
+.dashboard-grid {
display: block;
margin: 0;
padding: 5px;
@@ -93,24 +102,17 @@ dashboard-grid {
border-radius: 4px;
}
-.gridster {
- list-style-type: none;
- display: block;
+/**
+ * 1. Not entirely sure why but when the panel is within the grid, it requires height 100%. When it's an expanded
+ * panel, however, outside the grid, height: 100% will cause the panel not to expand properly.
+ * 2. We need this so the panel menu pop up shows up outside the boundaries of a panel.
+ */
+.react-grid-layout {
background-color: @dashboard-bg;
- margin: 0;
- padding: 0;
- .gs-resize-handle {
- background-position: 50% 50% !important;
- bottom: 0 !important;
- right: 0 !important;
- padding: 4px;
- height: 25px;
- width: 25px;
- }
-
- i.remove {
- cursor: pointer;
+ .dashboard-panel {
+ height: 100%; /* 1. */
+ overflow: visible; /* 2. */
}
.gs-w {
@@ -127,6 +129,10 @@ dashboard-grid {
.visualize-show-spy {
visibility: visible;
}
+
+ .panel-heading {
+ cursor: pointer;
+ }
}
.dashboard-container {
@@ -135,17 +141,22 @@ dashboard-grid {
flex-direction: column;
}
+dashboard-panel {
+ flex: 1;
+ display: flex;
+}
+
/**
* 1. Fix Firefox bug where a value of overflow: hidden will prevent scrolling in a panel where the spy panel does
* not have enough room.
* 2. react-select used in input control vis needs `visible` overflow to avoid clipping selection list
*/
-dashboard-panel {
+.dashboard-panel {
+ z-index: auto;
flex: 1;
display: flex;
flex-direction: column;
- height: 100%;
background: @dashboard-panel-bg;
color: @dashboard-panel-color;
padding: 0;
@@ -165,7 +176,7 @@ dashboard-panel {
justify-content: flex-start;
.panel-heading {
- padding: 0px 0px 0px 5px;
+ padding: 2px 10px 2px 5px;
flex: 0 0 auto;
white-space: nowrap;
display: flex;
@@ -174,6 +185,40 @@ dashboard-panel {
background-color: @white;
border: none;
+ /**
+ * 1. The popover aligns with the right side of this icon, so we want the right edge as far so the right as
+ * possible to make the arrows line up.
+ */
+ .dashboardPanelPopOver {
+ margin-right: -10px; /* 1. */
+ }
+
+ /**
+ * 1. Required to get the pop up component arrow to line up with the menu icon.
+ */
+ .panel-dropdown {
+ padding: 0 20px; /* 1. */
+ }
+
+ .kuiPopover__body {
+ z-index: 25;
+ }
+
+ .dashboardPanelMenuItem {
+ padding: 10px;
+ color: @text-color;
+
+ p {
+ display: inline;
+ padding: 0 0 0 5px;
+ }
+
+ &:hover {
+ color: @link-hover-color;
+ }
+
+ }
+
.panel-title {
font-size: inherit;
@@ -235,3 +280,10 @@ dashboard-panel {
overflow: auto; /* 1 */
}
}
+
+/**
+ * Allows popups inside each panel appear in front of other panels, if the boundaries are outside the panel itself.
+ */
+.react-grid-item:active {
+ z-index: 1; /* 1. */
+}
diff --git a/src/core_plugins/kibana/public/discover/embeddable/search_embeddable_handler.js b/src/core_plugins/kibana/public/discover/embeddable/search_embeddable_handler.js
index 1c12be3e8cab4..f816b13ec771d 100644
--- a/src/core_plugins/kibana/public/discover/embeddable/search_embeddable_handler.js
+++ b/src/core_plugins/kibana/public/discover/embeddable/search_embeddable_handler.js
@@ -76,6 +76,12 @@ export class SearchEmbeddableHandler extends EmbeddableHandler {
const searchInstance = this.$compile(searchTemplate)(searchScope);
const rootNode = angular.element(domNode);
rootNode.append(searchInstance);
+
+ return () => {
+ searchInstance.remove();
+ searchScope.savedObj.destroy();
+ searchScope.$destroy();
+ };
});
}
}
diff --git a/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_handler.js b/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_handler.js
index 23372e65d15a3..f5a15b215c2be 100644
--- a/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_handler.js
+++ b/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_handler.js
@@ -56,10 +56,11 @@ export class VisualizeEmbeddableHandler extends EmbeddableHandler {
const rootNode = angular.element(domNode);
rootNode.append(visualizationInstance);
- visualizationInstance.on('$destroy', function () {
+ return () => {
+ visualizationInstance.remove();
visualizeScope.savedObj.destroy();
visualizeScope.$destroy();
- });
+ };
});
}
}
diff --git a/src/jest/config.json b/src/jest/config.json
index 57360bdfd4bcc..5dcf367e9c1df 100644
--- a/src/jest/config.json
+++ b/src/jest/config.json
@@ -15,6 +15,8 @@
"moduleNameMapper": {
"^ui_framework/components": "
/ui_framework/components",
"^ui_framework/services": "/ui_framework/services",
+ "^ui/(.*)": "/src/ui/public/$1",
+ "^plugins/kibana/(.*)": "/src/core_plugins/kibana/public/$1",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/jest/file_mock.js",
"\\.(css|less|scss)$": "/src/jest/style_mock.js"
},
diff --git a/src/ui/public/embeddable/embeddable_handler.js b/src/ui/public/embeddable/embeddable_handler.js
index aee16ca8e6049..a60fec577d7a8 100644
--- a/src/ui/public/embeddable/embeddable_handler.js
+++ b/src/ui/public/embeddable/embeddable_handler.js
@@ -26,6 +26,8 @@ export class EmbeddableHandler {
* store per panel information.
* @property {ContainerApi} containerApi - an id to specify the object that this panel contains.
* @param {Promise.} A promise that resolves when the object is finished rendering.
+ * @return {Promise.} A promise that resolves to a function that should be used to destroy the
+ * rendered embeddable.
*/
render(/* domNode, panel, container */) {
throw new Error('Must implement render.');
diff --git a/src/ui/public/styles/dark-theme.less b/src/ui/public/styles/dark-theme.less
index dc281d164cbcb..4d5d7ad5c8073 100644
--- a/src/ui/public/styles/dark-theme.less
+++ b/src/ui/public/styles/dark-theme.less
@@ -528,11 +528,11 @@
background-color: @dashboard-bg;
}
- .gridster {
+ .react-grid-layout {
background-color: @dashboard-bg;
}
- dashboard-panel {
+ .dashboard-panel {
background: @dashboard-panel-bg;
color: @dashboard-panel-color;
diff --git a/test/functional/apps/dashboard/_dashboard.js b/test/functional/apps/dashboard/_dashboard.js
index bd1f24db015ec..b7b4c51550407 100644
--- a/test/functional/apps/dashboard/_dashboard.js
+++ b/test/functional/apps/dashboard/_dashboard.js
@@ -2,7 +2,6 @@ import expect from 'expect.js';
import {
DEFAULT_PANEL_WIDTH,
- DEFAULT_PANEL_HEIGHT,
} from '../../../../src/core_plugins/kibana/public/dashboard/panel/panel_state';
import {
@@ -76,29 +75,6 @@ export default function ({ getService, getPageObjects }) {
});
});
- it('should have all the expected initial sizes', function checkVisualizationSizes() {
- const width = DEFAULT_PANEL_WIDTH;
- const height = DEFAULT_PANEL_HEIGHT;
- const titles = PageObjects.dashboard.getTestVisualizationNames();
- const visObjects = [
- { dataCol: '1', dataRow: '1', dataSizeX: width, dataSizeY: height, title: titles[0] },
- { dataCol: width + 1, dataRow: '1', dataSizeX: width, dataSizeY: height, title: titles[1] },
- { dataCol: '1', dataRow: height + 1, dataSizeX: width, dataSizeY: height, title: titles[2] },
- { dataCol: width + 1, dataRow: height + 1, dataSizeX: width, dataSizeY: height, title: titles[3] },
- { dataCol: '1', dataRow: (height * 2) + 1, dataSizeX: width, dataSizeY: height, title: titles[4] },
- { dataCol: width + 1, dataRow: (height * 2) + 1, dataSizeX: width, dataSizeY: height, title: titles[5] },
- { dataCol: '1', dataRow: (height * 3) + 1, dataSizeX: width, dataSizeY: height, title: titles[6] }
- ];
- return retry.tryForTime(10000, function () {
- return PageObjects.dashboard.getPanelSizeData()
- .then(function (panelTitles) {
- log.info('visualization titles = ' + panelTitles);
- screenshots.take('Dashboard-visualization-sizes');
- expect(panelTitles).to.eql(visObjects);
- });
- });
- });
-
describe('filters', async function () {
it('are not selected by default', async function () {
const filters = await PageObjects.dashboard.getFilters(1000);
@@ -156,10 +132,16 @@ export default function ({ getService, getPageObjects }) {
it('for panel size parameters', async function () {
const currentUrl = await remote.getCurrentUrl();
- const newUrl = currentUrl.replace(`size_x:${DEFAULT_PANEL_WIDTH}`, `size_x:${DEFAULT_PANEL_WIDTH * 2}`);
+ const currentPanelDimensions = await PageObjects.dashboard.getPanelDimensions();
+ const newUrl = currentUrl.replace(`w:${DEFAULT_PANEL_WIDTH}`, `w:${DEFAULT_PANEL_WIDTH * 2}`);
await remote.get(newUrl.toString(), false);
- const allPanelInfo = await PageObjects.dashboard.getPanelSizeData();
- expect(allPanelInfo[0].dataSizeX).to.equal(`${DEFAULT_PANEL_WIDTH * 2}`);
+ await retry.try(async () => {
+ const newPanelDimensions = await PageObjects.dashboard.getPanelDimensions();
+ if (newPanelDimensions.length < 0) {
+ throw new Error('No panel dimensions...');
+ }
+ expect(newPanelDimensions[0].width).to.equal(currentPanelDimensions[0].width * 2);
+ });
});
});
@@ -303,11 +285,9 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.header.clickToastOK();
const visualizations = PageObjects.dashboard.getTestVisualizations();
- return retry.tryForTime(10000, async function () {
- const panelTitles = await PageObjects.dashboard.getPanelSizeData();
- log.info('visualization titles = ' + panelTitles.map(item => item.title));
- screenshots.take('Dashboard-visualization-sizes');
- expect(panelTitles.length).to.eql(visualizations.length + 1);
+ return retry.try(async () => {
+ const panelCount = await PageObjects.dashboard.getPanelCount();
+ expect(panelCount).to.eql(visualizations.length + 1);
});
});
diff --git a/test/functional/apps/dashboard/_view_edit.js b/test/functional/apps/dashboard/_view_edit.js
index 9d476691be90e..801096c71e59d 100644
--- a/test/functional/apps/dashboard/_view_edit.js
+++ b/test/functional/apps/dashboard/_view_edit.js
@@ -52,24 +52,20 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickDashboardByLinkText(dashboardName);
- const editLinkExists = await testSubjects.exists('dashboardPanelEditLink');
- const moveExists = await testSubjects.exists('dashboardPanelMoveIcon');
- const removeExists = await testSubjects.exists('dashboardPanelRemoveIcon');
-
- expect(editLinkExists).to.equal(false);
- expect(moveExists).to.equal(false);
- expect(removeExists).to.equal(false);
+ const panelToggleMenu = await testSubjects.exists('dashboardPanelToggleMenuIcon');
+ expect(panelToggleMenu).to.equal(false);
});
it('are shown in edit mode', async function () {
await PageObjects.dashboard.clickEdit();
+ const panelToggleMenu = await testSubjects.exists('dashboardPanelToggleMenuIcon');
+ expect(panelToggleMenu).to.equal(true);
+ await testSubjects.click('dashboardPanelToggleMenuIcon');
const editLinkExists = await testSubjects.exists('dashboardPanelEditLink');
- const moveExists = await testSubjects.exists('dashboardPanelMoveIcon');
const removeExists = await testSubjects.exists('dashboardPanelRemoveIcon');
expect(editLinkExists).to.equal(true);
- expect(moveExists).to.equal(true);
expect(removeExists).to.equal(true);
});
@@ -79,24 +75,20 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.header.clickToastOK();
await PageObjects.dashboard.toggleExpandPanel();
- const editLinkExists = await testSubjects.exists('dashboardPanelEditLink');
- const moveExists = await testSubjects.exists('dashboardPanelMoveIcon');
- const removeExists = await testSubjects.exists('dashboardPanelRemoveIcon');
-
- expect(editLinkExists).to.equal(false);
- expect(moveExists).to.equal(false);
- expect(removeExists).to.equal(false);
+ const panelToggleMenu = await testSubjects.exists('dashboardPanelToggleMenuIcon');
+ expect(panelToggleMenu).to.equal(false);
});
- it('in edit mode hides move and remove icons ', async function () {
+ it('in edit mode hides remove icons ', async function () {
await PageObjects.dashboard.clickEdit();
+ const panelToggleMenu = await testSubjects.exists('dashboardPanelToggleMenuIcon');
+ expect(panelToggleMenu).to.equal(true);
+ await testSubjects.click('dashboardPanelToggleMenuIcon');
const editLinkExists = await testSubjects.exists('dashboardPanelEditLink');
- const moveExists = await testSubjects.exists('dashboardPanelMoveIcon');
const removeExists = await testSubjects.exists('dashboardPanelRemoveIcon');
expect(editLinkExists).to.equal(true);
- expect(moveExists).to.equal(false);
expect(removeExists).to.equal(false);
await PageObjects.dashboard.toggleExpandPanel();
@@ -108,6 +100,7 @@ export default function ({ getService, getPageObjects }) {
describe('panel expand control', function () {
it('shown in edit mode', async function () {
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
+ await testSubjects.click('dashboardPanelToggleMenuIcon');
const expandExists = await testSubjects.exists('dashboardPanelExpandIcon');
expect(expandExists).to.equal(true);
});
@@ -212,8 +205,8 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.common.clickConfirmOnModal();
const visualizations = PageObjects.dashboard.getTestVisualizations();
- const panelTitles = await PageObjects.dashboard.getPanelSizeData();
- expect(panelTitles.length).to.eql(visualizations.length);
+ const panelCount = await PageObjects.dashboard.getPanelCount();
+ expect(panelCount).to.eql(visualizations.length);
});
it('when an existing vis is added', async function () {
@@ -224,8 +217,8 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.common.clickConfirmOnModal();
const visualizations = PageObjects.dashboard.getTestVisualizations();
- const panelTitles = await PageObjects.dashboard.getPanelSizeData();
- expect(panelTitles.length).to.eql(visualizations.length);
+ const panelCount = await PageObjects.dashboard.getPanelCount();
+ expect(panelCount).to.eql(visualizations.length);
});
});
diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js
index edd7bc102873c..a2cb020560bed 100644
--- a/test/functional/page_objects/dashboard_page.js
+++ b/test/functional/page_objects/dashboard_page.js
@@ -31,6 +31,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
async clickEditVisualization() {
log.debug('clickEditVisualization');
+ await testSubjects.click('dashboardPanelToggleMenuIcon');
await testSubjects.click('dashboardPanelEditLink');
await retry.try(async () => {
@@ -377,20 +378,25 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
return await testSubjects.findAll('dashboardPanel');
}
- async getPanelSizeData() {
- const titleObjects = await find.allByCssSelector('li.gs-w'); // These are gridster-defined elements and classes
- async function getTitles(chart) {
- const dataCol = await chart.getAttribute('data-col');
- const dataRow = await chart.getAttribute('data-row');
- const dataSizeX = await chart.getAttribute('data-sizex');
- const dataSizeY = await chart.getAttribute('data-sizey');
- const childElement = await testSubjects.findDescendant('dashboardPanelTitle', chart);
- const title = await childElement.getVisibleText();
- return { dataCol, dataRow, dataSizeX, dataSizeY, title };
+
+ async getPanelDimensions() {
+ const panels = await find.allByCssSelector('.react-grid-item'); // These are gridster-defined elements and classes
+ async function getPanelDimensions(panel) {
+ const size = await panel.getSize();
+ return {
+ width: size.width,
+ height: size.height
+ };
}
- const getTitlePromises = _.map(titleObjects, getTitles);
- return await Promise.all(getTitlePromises);
+ const getDimensionsPromises = _.map(panels, getPanelDimensions);
+ return await Promise.all(getDimensionsPromises);
+ }
+
+ async getPanelCount() {
+ log.debug('getPanelCount');
+ const panels = await find.allByCssSelector('.react-grid-item');
+ return panels.length;
}
getTestVisualizations() {
@@ -465,13 +471,9 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
log.debug('toggleExpandPanel');
const expandShown = await testSubjects.exists('dashboardPanelExpandIcon');
if (!expandShown) {
- const panelElements = await find.allByCssSelector('span.panel-title');
- log.debug('click title');
- await retry.try(() => panelElements[0].click()); // Click to simulate hover.
+ await testSubjects.click('dashboardPanelToggleMenuIcon');
}
- const expandButton = await testSubjects.find('dashboardPanelExpandIcon');
- log.debug('click expand icon');
- await retry.try(() => expandButton.click());
+ await testSubjects.click('dashboardPanelExpandIcon');
}
async getSharedItemsCount() {
diff --git a/ui_framework/dist/ui_framework.css b/ui_framework/dist/ui_framework.css
index a51004bc0cff5..8dfe46f9b4afe 100644
--- a/ui_framework/dist/ui_framework.css
+++ b/ui_framework/dist/ui_framework.css
@@ -2795,6 +2795,7 @@ main {
.kuiPopover.kuiPopover-isOpen .kuiPopover__body {
opacity: 1;
visibility: visible;
+ display: inline-block;
z-index: 1;
margin-top: 10px;
box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); }
@@ -2817,7 +2818,7 @@ main {
-webkit-transform-origin: center top;
transform-origin: center top;
opacity: 0;
- visibility: hidden;
+ display: none;
margin-top: 32px; }
.kuiPopover__body:before {
position: absolute;
diff --git a/ui_framework/src/components/popover/_popover.scss b/ui_framework/src/components/popover/_popover.scss
index 559f091821458..65d1e48809159 100644
--- a/ui_framework/src/components/popover/_popover.scss
+++ b/ui_framework/src/components/popover/_popover.scss
@@ -10,6 +10,7 @@
.kuiPopover__body {
opacity: 1;
visibility: visible;
+ display: inline-block;
z-index: 1;
margin-top: 10px;
box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1);
@@ -33,7 +34,7 @@
backface-visibility: hidden;
transform-origin: center top;
opacity: 0;
- visibility: hidden;
+ display: none;
margin-top: 32px;
// This fakes a border on the arrow.
diff --git a/webpackShims/gridster.js b/webpackShims/gridster.js
deleted file mode 100644
index 381358646f616..0000000000000
--- a/webpackShims/gridster.js
+++ /dev/null
@@ -1,3 +0,0 @@
-require('jquery');
-require('node_modules/gridster/dist/jquery.gridster.css');
-require('script!node_modules/gridster/dist/jquery.gridster');
From bee007f8046d86a9e51b957ca784dee6a7370214 Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 13 Sep 2017 20:17:35 -0400
Subject: [PATCH 02/16] # This is a combination of 3 commits. # This is the 1st
commit message: Add margin of error to test determining panel widths
# This is the commit message #2:
use real kibana version when creating panel data. Will make future conversions easier.
# This is the commit message #3:
Fix lint errors
---
.../dashboard/grid/dashboard_grid.test.js | 2 ++
.../public/dashboard/panel/panel_state.js | 2 ++
.../public/dashboard/panel/panel_utils.js | 29 ++-----------------
src/ui/public/chrome/api/apps.js | 5 ++++
test/functional/apps/dashboard/_dashboard.js | 6 +++-
5 files changed, 16 insertions(+), 28 deletions(-)
diff --git a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js
index 7ad93bd582954..88a3276f1be36 100644
--- a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js
+++ b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js
@@ -6,6 +6,8 @@ import { PanelUtils } from '../panel/panel_utils';
import { DashboardGrid } from './dashboard_grid';
import { DashboardPanel } from '../panel/dashboard_panel';
+jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0' }), { virtual: true });
+
const getContainerApi = () => {
return {
addFilter: () => {},
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_state.js b/src/core_plugins/kibana/public/dashboard/panel/panel_state.js
index 64f4ddcf20c8d..c5d0238d5a70f 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_state.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_state.js
@@ -2,6 +2,7 @@ export const DEFAULT_PANEL_WIDTH = 6;
export const DEFAULT_PANEL_HEIGHT = 3;
import { DASHBOARD_GRID_COLUMN_COUNT } from '../dashboard_constants';
+import chrome from 'ui/chrome';
/**
* Represents a panel on a grid. Keeps track of position in the grid and what visualization it
@@ -96,6 +97,7 @@ export function createPanelState(id, type, panelIndex, currentPanels) {
y,
i: panelIndex.toString()
},
+ version: chrome.getKibanaVersion(),
panelIndex: panelIndex,
type: type,
id: id
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js b/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
index d27a5a98e5803..b04607e89cb38 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
@@ -1,4 +1,5 @@
import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from 'plugins/kibana/dashboard/panel/panel_state';
+import chrome from 'ui/chrome';
import _ from 'lodash';
@@ -31,7 +32,7 @@ export class PanelUtils {
w: panel.size_x || DEFAULT_PANEL_WIDTH,
h: panel.size_y || DEFAULT_PANEL_HEIGHT,
i: panel.panelIndex.toString(),
- version: 6,
+ version: chrome.getKibanaVersion(),
};
delete panel.size_x;
delete panel.size_y;
@@ -39,32 +40,6 @@ export class PanelUtils {
delete panel.col;
}
- /**
- * Ensures that the panel object has the latest size/pos info.
- * @param {PanelState} panel
- * @param {Element} panelElement - jQuery element representing the element in the UI
- */
- static refreshSizeAndPosition(panel, panelElement) {
- const data = panelElement.coords().grid;
- panel.size_x = data.size_x;
- panel.size_y = data.size_y;
- panel.col = data.col;
- panel.row = data.row;
- }
-
- /**
- * Ensures that the grid element matches the latest size/pos info in the panel element.
- * @param {PanelState} panel
- * @param {Element} panelElement - jQuery element representing the element in the UI
- */
- static refreshElementSizeAndPosition(panel, panelElement) {
- const data = panelElement.coords().grid;
- data.size_x = panel.size_x;
- data.size_y = panel.size_y;
- data.col = panel.col;
- data.row = panel.row;
- }
-
/**
* Returns the panel with the given panelIndex from the panels array (*NOT* the panel at the given index).
* @param panelIndex {number} - Note this is *NOT* the index of the panel in the panels array.
diff --git a/src/ui/public/chrome/api/apps.js b/src/ui/public/chrome/api/apps.js
index 42c532b2d4e91..96c45fbfd33d6 100644
--- a/src/ui/public/chrome/api/apps.js
+++ b/src/ui/public/chrome/api/apps.js
@@ -38,6 +38,11 @@ export default function (chrome, internals) {
return internals.showAppsLink == null ? internals.nav.length > 1 : internals.showAppsLink;
};
+
+ chrome.getKibanaVersion = function () {
+ return internals.version;
+ };
+
chrome.getApp = function () {
return clone(internals.app);
};
diff --git a/test/functional/apps/dashboard/_dashboard.js b/test/functional/apps/dashboard/_dashboard.js
index b7b4c51550407..29f7863f534da 100644
--- a/test/functional/apps/dashboard/_dashboard.js
+++ b/test/functional/apps/dashboard/_dashboard.js
@@ -140,7 +140,11 @@ export default function ({ getService, getPageObjects }) {
if (newPanelDimensions.length < 0) {
throw new Error('No panel dimensions...');
}
- expect(newPanelDimensions[0].width).to.equal(currentPanelDimensions[0].width * 2);
+ // Some margin of error is allowed, I've noticed it being off by one pixel. Probably something to do with
+ // an odd width and dividing by two. Note that if we add margins, we'll have to adjust this as well.
+ const marginOfError = 5;
+ expect(newPanelDimensions[0].width).to.be.lessThan(currentPanelDimensions[0].width * 2 + marginOfError);
+ expect(newPanelDimensions[0].width).to.be.greaterThan(currentPanelDimensions[0].width * 2 - marginOfError);
});
});
});
From 75075c29aa00d7076eb5459d268166d342fbcded Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 13 Sep 2017 20:17:35 -0400
Subject: [PATCH 03/16] Add margin of error to test determining panel widths
use real kibana version when creating panel data. Will make future conversions easier.
Move default height and width to dashboard_constants so those that need it don't end up including extra stuff like ui/chrome
---
.../kibana/public/dashboard/dashboard_constants.js | 3 ++-
.../kibana/public/dashboard/panel/__tests__/panel_utils.js | 2 +-
.../kibana/public/dashboard/panel/panel_state.js | 5 +----
.../kibana/public/dashboard/panel/panel_utils.js | 2 +-
test/functional/apps/dashboard/_dashboard.js | 2 +-
5 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_constants.js b/src/core_plugins/kibana/public/dashboard/dashboard_constants.js
index 6bdb743804f25..ed4b62367c4c5 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard_constants.js
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_constants.js
@@ -4,7 +4,8 @@ export const DashboardConstants = {
LANDING_PAGE_PATH: '/dashboards',
CREATE_NEW_DASHBOARD_URL: '/dashboard',
};
-
+export const DEFAULT_PANEL_WIDTH = 6;
+export const DEFAULT_PANEL_HEIGHT = 3;
export const DASHBOARD_GRID_COLUMN_COUNT = 12;
export function createDashboardEditUrl(id) {
diff --git a/src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_utils.js b/src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_utils.js
index f8ebf46eed645..a500716a2cab6 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_utils.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/__tests__/panel_utils.js
@@ -1,5 +1,5 @@
import { PanelUtils } from '../panel_utils';
-import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../panel_state';
+import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../../dashboard_constants';
describe('PanelUtils', function () {
it('convertOldPanelData gives supplies width and height when missing', () => {
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_state.js b/src/core_plugins/kibana/public/dashboard/panel/panel_state.js
index c5d0238d5a70f..03778a99d2f70 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_state.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_state.js
@@ -1,7 +1,4 @@
-export const DEFAULT_PANEL_WIDTH = 6;
-export const DEFAULT_PANEL_HEIGHT = 3;
-
-import { DASHBOARD_GRID_COLUMN_COUNT } from '../dashboard_constants';
+import { DASHBOARD_GRID_COLUMN_COUNT, DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../dashboard_constants';
import chrome from 'ui/chrome';
/**
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js b/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
index b04607e89cb38..358ed1b94c3c0 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
@@ -1,4 +1,4 @@
-import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from 'plugins/kibana/dashboard/panel/panel_state';
+import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from 'plugins/kibana/dashboard/dashboard_constants';
import chrome from 'ui/chrome';
import _ from 'lodash';
diff --git a/test/functional/apps/dashboard/_dashboard.js b/test/functional/apps/dashboard/_dashboard.js
index 29f7863f534da..370bbe540e947 100644
--- a/test/functional/apps/dashboard/_dashboard.js
+++ b/test/functional/apps/dashboard/_dashboard.js
@@ -2,7 +2,7 @@ import expect from 'expect.js';
import {
DEFAULT_PANEL_WIDTH,
-} from '../../../../src/core_plugins/kibana/public/dashboard/panel/panel_state';
+} from '../../../../src/core_plugins/kibana/public/dashboard/dashboard_constants';
import {
VisualizeConstants
From e0321be4d95366c54c4375aae0519ce2aefa7ce5 Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 12:41:00 -0400
Subject: [PATCH 04/16] Remove unnecessary _.once when creating react
directives in dashboard.js
---
package.json | 1 -
.../kibana/public/dashboard/dashboard.js | 14 ++++++--------
2 files changed, 6 insertions(+), 9 deletions(-)
diff --git a/package.json b/package.json
index 176ceaa311c98..e682686a9fc00 100644
--- a/package.json
+++ b/package.json
@@ -178,7 +178,6 @@
"react-router": "2.0.0",
"react-router-redux": "4.0.4",
"react-select": "1.0.0-rc.5",
-
"react-sizeme": "2.3.4",
"react-sortable": "1.1.0",
"react-test-renderer": "15.6.1",
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard.js b/src/core_plugins/kibana/public/dashboard/dashboard.js
index ea55029f12c2a..fddba4013732b 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard.js
+++ b/src/core_plugins/kibana/public/dashboard/dashboard.js
@@ -46,15 +46,13 @@ const app = uiModules.get('app/dashboard', [
'kibana/typeahead',
]);
-_.once(() => {
- app.directive('dashboardGrid', function (reactDirective) {
- return reactDirective(DashboardGrid);
- });
+app.directive('dashboardGrid', function (reactDirective) {
+ return reactDirective(DashboardGrid);
+});
- app.directive('dashboardPanel', function (reactDirective) {
- return reactDirective(DashboardPanel);
- });
-})();
+app.directive('dashboardPanel', function (reactDirective) {
+ return reactDirective(DashboardPanel);
+});
uiRoutes
.when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {
From e58ac8d1f070c136ea6fd5cedcb793813f5705e6 Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 12:46:29 -0400
Subject: [PATCH 05/16] Remove unnecessary constructors
---
.../kibana/public/dashboard/grid/dashboard_grid.js | 10 +++-------
.../kibana/public/dashboard/panel/panel_header.js | 3 ---
.../public/dashboard/panel/panel_options_menu.js | 10 +++-------
3 files changed, 6 insertions(+), 17 deletions(-)
diff --git a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
index f0a24610aac34..1c8c1cdfe4e99 100644
--- a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
+++ b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
@@ -36,13 +36,9 @@ const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid);
export class DashboardGrid extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- layout: this.buildLayoutFromPanels()
- };
- }
+ state = {
+ layout: this.buildLayoutFromPanels()
+ };
buildLayoutFromPanels() {
return _.map(this.props.panels, panel => {
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header.js b/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
index aff4163126348..0ffe05b75b77a 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
@@ -6,9 +6,6 @@ import { KuiKeyboardAccessible } from 'ui_framework/components';
import { PanelOptionsMenu } from './panel_options_menu';
export class PanelHeader extends React.Component {
- constructor(props) {
- super(props);
- }
getOptionsDropDown() {
return (
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js b/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js
index 1596e0a90ae99..b0a0a2ca1e477 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js
@@ -8,13 +8,9 @@ import {
} from 'ui_framework/components';
export class PanelOptionsMenu extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- showMenu: false
- };
- }
+ state = {
+ showMenu: false
+ };
toggleMenu = () => {
this.setState({ showMenu: !this.state.showMenu });
From 1f70da29ac83c73fd75e7e2904d9f147d91287cf Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 13:05:17 -0400
Subject: [PATCH 06/16] Use componentDidMount instead of componentWillMount bc
of async calls, and handle case where destroyEmbeddable is not defined.
---
.../public/dashboard/panel/dashboard_panel.js | 27 ++++++++++++++-----
1 file changed, 20 insertions(+), 7 deletions(-)
diff --git a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
index 9430047da62d9..f333728bca9c0 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
@@ -14,9 +14,11 @@ export class DashboardPanel extends React.Component {
this.embeddable = null;
this.embeddableHandler = null;
this.parentNode = null;
+ this._isMounted = false;
}
- async componentWillMount() {
+ async componentDidMount() {
+ this._isMounted = true;
const { getEmbeddableHandler, panel, getContainerApi } = this.props;
this.containerApi = getContainerApi();
@@ -24,12 +26,16 @@ export class DashboardPanel extends React.Component {
const editUrl = await this.embeddableHandler.getEditPath(panel.id);
const title = await this.embeddableHandler.getTitleFor(panel.id);
- this.setState({ editUrl, title });
+ // TODO: use redux instead of the isMounted anti-pattern to handle the case when the component is unmounted
+ // before the async calls above return.
+ if (this._isMounted) {
+ this.setState({ editUrl, title });
- this.destroyEmbeddable = await this.embeddableHandler.render(
- this.panelElement,
- panel,
- this.containerApi);
+ this.destroyEmbeddable = await this.embeddableHandler.render(
+ this.panelElement,
+ panel,
+ this.containerApi);
+ }
}
isViewOnlyMode() {
@@ -61,7 +67,14 @@ export class DashboardPanel extends React.Component {
};
componentWillUnmount() {
- this.destroyEmbeddable();
+ // This is required because it's possible the component becomes unmounted before embeddableHandler.render returns.
+ // This is really an anti-pattern and could be cleaned up by implementing a redux framework for dashboard state.
+ // Because implementing that may be a very large change in and of itself, it will be a second step, and we'll live
+ // with this anti-pattern for the time being.
+ this._isMounted = false;
+ if (this.destroyEmbeddable) {
+ this.destroyEmbeddable();
+ }
}
render() {
From 70031508027cf3e175789a9e029237183569de07 Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 13:29:59 -0400
Subject: [PATCH 07/16] Remove unnecessary null in classNames
---
src/core_plugins/kibana/public/dashboard/panel/panel_header.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header.js b/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
index 0ffe05b75b77a..567e44b1bdfce 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
@@ -20,7 +20,7 @@ export class PanelHeader extends React.Component {
getExpandToggle() {
const { isExpanded } = this.props;
- const classes = classNames('kuiIcon', null, { 'fa-expand': !isExpanded, 'fa-compress': isExpanded });
+ const classes = classNames('kuiIcon', { 'fa-expand': !isExpanded, 'fa-compress': isExpanded });
return (
Date: Wed, 20 Sep 2017 13:32:27 -0400
Subject: [PATCH 08/16] Use loads defaultsDeep instead of Object.assign
---
.../kibana/public/dashboard/panel/dashboard_panel.test.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js
index 38a67c4f9fc91..8be3970f005b0 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.js
@@ -1,4 +1,5 @@
import React from 'react';
+import _ from 'lodash';
import { mount } from 'enzyme';
import { DashboardViewMode } from '../dashboard_view_mode';
import { DashboardPanel } from './dashboard_panel';
@@ -33,7 +34,7 @@ function getProps(props = {}) {
onToggleExpanded: () => {},
onDeletePanel: () => {}
};
- return Object.assign(props, defaultTestProps);
+ return _.defaultsDeep(props, defaultTestProps);
}
test('DashboardPanel matches snapshot', () => {
From 5064127b5a65f647f612cf2219b565057b2f45eb Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 13:35:33 -0400
Subject: [PATCH 09/16] use render* instead of get* for functions returning an
element
---
.../kibana/public/dashboard/grid/dashboard_grid.js | 4 ++--
.../kibana/public/dashboard/panel/panel_header.js | 6 +++---
.../public/dashboard/panel/panel_options_menu.js | 12 ++++++------
3 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
index 1c8c1cdfe4e99..002eb2756eb2d 100644
--- a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
+++ b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
@@ -69,7 +69,7 @@ export class DashboardGrid extends React.Component {
});
};
- generateDOM() {
+ renderDOM() {
const {
panels,
onPanelRemoved,
@@ -117,7 +117,7 @@ export class DashboardGrid extends React.Component {
buildLayoutFromPanels={this.buildLayoutFromPanels()}
onLayoutChange={this.onLayoutChange}
>
- {this.generateDOM()}
+ {this.renderDOM()}
);
}
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header.js b/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
index 567e44b1bdfce..5926f5a32078e 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
@@ -7,7 +7,7 @@ import { PanelOptionsMenu } from './panel_options_menu';
export class PanelHeader extends React.Component {
- getOptionsDropDown() {
+ renderOptionsDropDown() {
return (
- {this.props.isViewOnlyMode ? this.getExpandToggle() : this.getOptionsDropDown()}
+ {this.props.isViewOnlyMode ? this.renderExpandToggle() : this.renderOptionsDropDown()}
);
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js b/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js
index b0a0a2ca1e477..362c3b690c9ca 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_options_menu.js
@@ -17,7 +17,7 @@ export class PanelOptionsMenu extends React.Component {
};
closeMenu = () => this.setState({ showMenu: false });
- getEditVisualizationMenuItem() {
+ renderEditVisualizationMenuItem() {
return (
- {this.getEditVisualizationMenuItem()}
- {this.getToggleExpandMenuItem()}
- {this.props.isExpanded ? null : this.getDeleteMenuItem()}
+ {this.renderEditVisualizationMenuItem()}
+ {this.renderToggleExpandMenuItem()}
+ {this.props.isExpanded ? null : this.renderDeleteMenuItem()}
);
From 0e85f7c74c0a9fe4b2e06e8e1c1a5136a5d32ff3 Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 13:37:16 -0400
Subject: [PATCH 10/16] use relative css paths
---
src/core_plugins/kibana/public/dashboard/styles/index.less | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/core_plugins/kibana/public/dashboard/styles/index.less b/src/core_plugins/kibana/public/dashboard/styles/index.less
index 957107771809e..3d8478e4cd677 100644
--- a/src/core_plugins/kibana/public/dashboard/styles/index.less
+++ b/src/core_plugins/kibana/public/dashboard/styles/index.less
@@ -1,9 +1,8 @@
@import (reference) "~ui/styles/variables";
@import (reference) "~ui/styles/mixins";
@import "~ui/styles/local_search.less";
-
-@import "../../../../../../node_modules/react-grid-layout/css/styles.css";
-@import "../../../../../../node_modules/react-resizable/css/styles.css";
+@import "~react-grid-layout/css/styles.css";
+@import "~react-resizable/css/styles.css";
.fullScreenModePlaceholder {
text-align: center;
From 6d10b5a24ac15ecc6b1c4e0dae31a61d67ee335f Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 13:37:58 -0400
Subject: [PATCH 11/16] Use local import path
---
src/core_plugins/kibana/public/dashboard/dashboard_state.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_state.js b/src/core_plugins/kibana/public/dashboard/dashboard_state.js
index 0302b356ebe24..f291a68f4e485 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard_state.js
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_state.js
@@ -6,7 +6,7 @@ import { PanelUtils } from './panel/panel_utils';
import moment from 'moment';
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
-import { createPanelState, getPersistedStateId } from 'plugins/kibana/dashboard/panel';
+import { createPanelState, getPersistedStateId } from './panel';
function getStateDefaults(dashboard, hideWriteControls) {
return {
From f61e26877b5f1e6ac6aa566fea4d5ad238edb618 Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 13:53:22 -0400
Subject: [PATCH 12/16] Switch to local imports and remove need for plugins
path in jest tests
---
src/core_plugins/kibana/public/dashboard/panel/panel_utils.js | 2 +-
src/jest/config.json | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js b/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
index 358ed1b94c3c0..44b5eb35dcdc8 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_utils.js
@@ -1,4 +1,4 @@
-import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from 'plugins/kibana/dashboard/dashboard_constants';
+import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../dashboard_constants';
import chrome from 'ui/chrome';
import _ from 'lodash';
diff --git a/src/jest/config.json b/src/jest/config.json
index 5dcf367e9c1df..6b84171706dad 100644
--- a/src/jest/config.json
+++ b/src/jest/config.json
@@ -16,7 +16,6 @@
"^ui_framework/components": "/ui_framework/components",
"^ui_framework/services": "/ui_framework/services",
"^ui/(.*)": "/src/ui/public/$1",
- "^plugins/kibana/(.*)": "/src/core_plugins/kibana/public/$1",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/jest/file_mock.js",
"\\.(css|less|scss)$": "/src/jest/style_mock.js"
},
From f7347ddb021fdf113336d118387886f3c37515df Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 13:58:26 -0400
Subject: [PATCH 13/16] Improve accessibility of max/min panel toggle icon
---
.../public/dashboard/panel/panel_header.js | 26 +++++++++----------
1 file changed, 12 insertions(+), 14 deletions(-)
diff --git a/src/core_plugins/kibana/public/dashboard/panel/panel_header.js b/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
index 5926f5a32078e..d6437341fb649 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/panel_header.js
@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { KuiKeyboardAccessible } from 'ui_framework/components';
import { PanelOptionsMenu } from './panel_options_menu';
export class PanelHeader extends React.Component {
@@ -21,20 +20,19 @@ export class PanelHeader extends React.Component {
renderExpandToggle() {
const { isExpanded } = this.props;
const classes = classNames('kuiIcon', { 'fa-expand': !isExpanded, 'fa-compress': isExpanded });
+ const ariaLabel = isExpanded ? 'Minimize panel' : 'Maximize panel';
return (
-
-
-
-
-
+
+
+
);
}
From fca9d0d4a2de34b80137bb719a7f8d318a7a2a7e Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 14:19:01 -0400
Subject: [PATCH 14/16] remove unused css
Had to implement this via code
---
src/core_plugins/kibana/public/dashboard/styles/index.less | 7 -------
1 file changed, 7 deletions(-)
diff --git a/src/core_plugins/kibana/public/dashboard/styles/index.less b/src/core_plugins/kibana/public/dashboard/styles/index.less
index 3d8478e4cd677..8d7fb63a54391 100644
--- a/src/core_plugins/kibana/public/dashboard/styles/index.less
+++ b/src/core_plugins/kibana/public/dashboard/styles/index.less
@@ -279,10 +279,3 @@ dashboard-panel {
overflow: auto; /* 1 */
}
}
-
-/**
- * Allows popups inside each panel appear in front of other panels, if the boundaries are outside the panel itself.
- */
-.react-grid-item:active {
- z-index: 1; /* 1. */
-}
From 6290a44e8de7785545dced3f4098523acb5c6321 Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 14:25:11 -0400
Subject: [PATCH 15/16] disable eslint rule for setState in componentDidMount
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Am not aware of a better way to handle this, aside from switching to
redux, since it’s recommended not to put async calls in
componentWillMount. Since I plan to investigate redux next, disabling
for now. Open to other’s opinions on the matter.
---
.../kibana/public/dashboard/panel/dashboard_panel.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
index f333728bca9c0..64f6899b71b3e 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
@@ -27,8 +27,10 @@ export class DashboardPanel extends React.Component {
const title = await this.embeddableHandler.getTitleFor(panel.id);
// TODO: use redux instead of the isMounted anti-pattern to handle the case when the component is unmounted
- // before the async calls above return.
+ // before the async calls above return. We can then get rid of the eslint disable line. Without redux, there is
+ // not a better option, since you aren't supposed to run async calls inside of componentWillMount.
if (this._isMounted) {
+ /* eslint-disable react/no-did-mount-set-state */
this.setState({ editUrl, title });
this.destroyEmbeddable = await this.embeddableHandler.render(
From f5e28ed54f119d3f34f90b2f42fa9ecb4f0aea6d Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Wed, 20 Sep 2017 21:00:03 -0400
Subject: [PATCH 16/16] Add redux for viewport state management in dashboard.
---
package.json | 4 +-
.../kibana/public/dashboard/action_types.js | 10 ++
.../public/dashboard/dashboard_actions.js | 111 ++++++++++++++++
.../{dashboard.html => dashboard_app.html} | 24 +---
.../{dashboard.js => dashboard_app.js} | 94 ++++----------
.../dashboard/dashboard_container_api.js | 8 +-
.../public/dashboard/dashboard_reducers.js | 106 ++++++++++++++++
...rd_state.js => dashboard_state_manager.js} | 118 ++++++++++++++----
.../public/dashboard/dashboard_store.js | 19 +++
.../public/dashboard/grid/dashboard_grid.js | 49 +++-----
.../grid/dashboard_grid_container.js | 24 ++++
.../kibana/public/dashboard/index.js | 42 ++++++-
.../dashboard/lib/convert_time_to_string.js | 10 ++
.../dashboard/{ => lib}/filter_utils.js | 0
.../dashboard/lib/get_app_state_defaults.js | 17 +++
.../kibana/public/dashboard/lib/index.js | 1 +
.../public/dashboard/lib/save_dashboard.js | 27 ++++
.../dashboard/lib/update_saved_dashboard.js | 15 +++
.../public/dashboard/panel/dashboard_panel.js | 74 ++++-------
.../panel/dashboard_panel_container.js | 40 ++++++
.../kibana/public/dashboard/styles/index.less | 18 +--
.../dashboard/viewport/dashboard_container.js | 64 ++++++++++
.../dashboard/viewport/dashboard_viewport.js | 20 +++
.../embeddable/search_embeddable_handler.js | 14 ++-
src/core_plugins/kibana/public/reducers.js | 10 ++
src/core_plugins/kibana/public/store.js | 12 ++
.../visualize_embeddable_handler.js | 24 ++--
src/ui/public/embeddable/embeddable.js | 6 +
.../public/embeddable/embeddable_handler.js | 28 ++---
.../embeddable_handlers_registry.js | 1 +
src/ui/public/embeddable/index.js | 1 +
.../state_management/state_monitor_factory.js | 8 +-
.../state_management/update_app_state.js | 18 +++
33 files changed, 773 insertions(+), 244 deletions(-)
create mode 100644 src/core_plugins/kibana/public/dashboard/action_types.js
create mode 100644 src/core_plugins/kibana/public/dashboard/dashboard_actions.js
rename src/core_plugins/kibana/public/dashboard/{dashboard.html => dashboard_app.html} (81%)
rename src/core_plugins/kibana/public/dashboard/{dashboard.js => dashboard_app.js} (85%)
create mode 100644 src/core_plugins/kibana/public/dashboard/dashboard_reducers.js
rename src/core_plugins/kibana/public/dashboard/{dashboard_state.js => dashboard_state_manager.js} (80%)
create mode 100644 src/core_plugins/kibana/public/dashboard/dashboard_store.js
create mode 100644 src/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.js
create mode 100644 src/core_plugins/kibana/public/dashboard/lib/convert_time_to_string.js
rename src/core_plugins/kibana/public/dashboard/{ => lib}/filter_utils.js (100%)
create mode 100644 src/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.js
create mode 100644 src/core_plugins/kibana/public/dashboard/lib/index.js
create mode 100644 src/core_plugins/kibana/public/dashboard/lib/save_dashboard.js
create mode 100644 src/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.js
create mode 100644 src/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.js
create mode 100644 src/core_plugins/kibana/public/dashboard/viewport/dashboard_container.js
create mode 100644 src/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport.js
create mode 100644 src/core_plugins/kibana/public/reducers.js
create mode 100644 src/core_plugins/kibana/public/store.js
create mode 100644 src/ui/public/embeddable/embeddable.js
create mode 100644 src/ui/public/state_management/update_app_state.js
diff --git a/package.json b/package.json
index e682686a9fc00..ec5976c186b7b 100644
--- a/package.json
+++ b/package.json
@@ -183,8 +183,8 @@
"react-test-renderer": "15.6.1",
"react-toggle": "3.0.1",
"reactcss": "1.0.7",
- "redux": "3.0.0",
- "redux-thunk": "0.1.0",
+ "redux": "3.7.2",
+ "redux-thunk": "2.2.0",
"request": "2.61.0",
"resize-observer-polyfill": "1.2.1",
"rimraf": "2.4.3",
diff --git a/src/core_plugins/kibana/public/dashboard/action_types.js b/src/core_plugins/kibana/public/dashboard/action_types.js
new file mode 100644
index 0000000000000..fd889d8661bc2
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/action_types.js
@@ -0,0 +1,10 @@
+export const EMBEDDABLE_RENDER_REQUESTED = 'EMBEDDABLE_RENDER_REQUESTED';
+export const EMBEDDABLE_RENDER_FINISHED = 'EMBEDDABLE_RENDER_FINISHED';
+export const UPDATE_PANELS = 'UPDATE_PANELS';
+export const UPDATE_PANEL = 'UPDATE_PANEL';
+export const ADD_NEW_PANEl = 'ADD_NEW_PANEl';
+export const EMBEDDABLE_RENDER_ERROR = 'EMBEDDABLE_RENDER_ERROR';
+export const UPDATE_VIEW_MODE = 'UPDATE_VIEW_MODE';
+export const UPDATE_MAXIMIZED_PANEl_ID = 'UPDATE_MAXIMIZED_PANEl_ID';
+export const DELETE_PANEL = 'DELETE_PANEL';
+export const UPDATE_IS_FULL_SCREEN_MODE = 'UPDATE_IS_FULL_SCREEN_MODE';
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_actions.js b/src/core_plugins/kibana/public/dashboard/dashboard_actions.js
new file mode 100644
index 0000000000000..2e2e742f3054c
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_actions.js
@@ -0,0 +1,111 @@
+import {
+ EMBEDDABLE_RENDER_FINISHED,
+ EMBEDDABLE_RENDER_REQUESTED,
+ EMBEDDABLE_RENDER_ERROR,
+ UPDATE_PANELS,
+ UPDATE_VIEW_MODE,
+ UPDATE_MAXIMIZED_PANEl_ID,
+ DELETE_PANEL,
+ UPDATE_PANEL,
+ ADD_NEW_PANEl,
+ UPDATE_IS_FULL_SCREEN_MODE,
+} from './action_types';
+
+function embeddableRenderRequested(panelId) {
+ return {
+ type: EMBEDDABLE_RENDER_REQUESTED,
+ panelId: panelId
+ }
+}
+
+function embeddableRenderFinished(panelId, embeddable) {
+ return {
+ type: EMBEDDABLE_RENDER_FINISHED,
+ embeddable,
+ panelId,
+ }
+}
+
+function embeddableRenderError(panelId, error) {
+ return {
+ type: EMBEDDABLE_RENDER_ERROR,
+ panelId,
+ error
+ }
+}
+
+export function deletePanel(panelId) {
+ return {
+ type: DELETE_PANEL,
+ panelId
+ }
+}
+
+export function updateViewMode(viewMode) {
+ return {
+ type: UPDATE_VIEW_MODE,
+ viewMode
+ }
+}
+
+export function maximizePanel(panelId) {
+ return {
+ type: UPDATE_MAXIMIZED_PANEl_ID,
+ maximizedPanelId: panelId
+ }
+}
+
+export function minimizePanel() {
+ return {
+ type: UPDATE_MAXIMIZED_PANEl_ID,
+ maximizedPanelId: undefined
+ }
+}
+
+export function updatePanel(panel) {
+ return {
+ type: UPDATE_PANEL,
+ panel
+ }
+}
+
+export function addNewPanel(panel) {
+ return {
+ type: ADD_NEW_PANEl,
+ panel
+ }
+}
+
+export function updateIsFullScreenMode(isFullScreenMode) {
+ return {
+ type: UPDATE_IS_FULL_SCREEN_MODE,
+ isFullScreenMode
+ }
+}
+
+export function renderEmbeddable(embeddableHandler, panelElement, panelId, containerApi) {
+ return (dispatch, getState) => {
+ dispatch(embeddableRenderRequested(panelId));
+ const { dashboardState } = getState();
+ const panelState = dashboardState.panels[panelId];
+ return embeddableHandler.render(panelElement, panelState, containerApi)
+ .then(embeddable => {
+ return dispatch(embeddableRenderFinished(panelId, embeddable));
+ })
+ .catch(error => {
+ dispatch(embeddableRenderError(panelId, error));
+ console.log('err: ', error);
+ })
+ }
+}
+
+export function updatePanels(panels) {
+ const panelsMap = {};
+ panels.forEach(panel => {
+ panelsMap[panel.panelIndex] = panel;
+ });
+ return {
+ type: UPDATE_PANELS,
+ panels: panelsMap,
+ }
+}
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard.html b/src/core_plugins/kibana/public/dashboard/dashboard_app.html
similarity index 81%
rename from src/core_plugins/kibana/public/dashboard/dashboard.html
rename to src/core_plugins/kibana/public/dashboard/dashboard_app.html
index 07573ee07e654..e7b6d8e40a1b2 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard.html
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.html
@@ -77,26 +77,10 @@
-
-
-
+ >
+
+
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js
similarity index 85%
rename from src/core_plugins/kibana/public/dashboard/dashboard.js
rename to src/core_plugins/kibana/public/dashboard/dashboard_app.js
index fddba4013732b..7b2e5e8775411 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard.js
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js
@@ -1,8 +1,9 @@
import _ from 'lodash';
import angular from 'angular';
import { uiModules } from 'ui/modules';
-import uiRoutes from 'ui/routes';
import chrome from 'ui/chrome';
+import { store } from '../store';
+import { updatePanels } from './dashboard_actions';
import 'ui/query_bar';
@@ -11,13 +12,13 @@ import { getDashboardTitle, getUnsavedChangesWarningMessage } from './dashboard_
import { DashboardViewMode } from './dashboard_view_mode';
import { TopNavIds } from './top_nav/top_nav_ids';
import { ConfirmationButtonTypes } from 'ui/modals/confirm_modal';
-import dashboardTemplate from 'plugins/kibana/dashboard/dashboard.html';
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
import { DocTitleProvider } from 'ui/doc_title';
import { getTopNavConfig } from './top_nav/get_top_nav_config';
import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
import { VisualizeConstants } from 'plugins/kibana/visualize/visualize_constants';
-import { DashboardState } from './dashboard_state';
+import { DashboardStateManager } from './dashboard_state_manager';
+import { saveDashboard } from './lib';
import { notify } from 'ui/notify';
import { documentationLinks } from 'ui/documentation_links/documentation_links';
import { showCloneModal } from './top_nav/show_clone_modal';
@@ -28,13 +29,7 @@ import * as filterActions from 'ui/doc_table/actions/filter';
import { FilterManagerProvider } from 'ui/filter_manager';
import { EmbeddableHandlersRegistryProvider } from 'ui/embeddable/embeddable_handlers_registry';
-import {
- DashboardGrid
-} from './grid/dashboard_grid';
-
-import {
- DashboardPanel
-} from './panel';
+import { DashboardViewport } from './viewport/dashboard_viewport';
const app = uiModules.get('app/dashboard', [
'elasticsearch',
@@ -46,51 +41,10 @@ const app = uiModules.get('app/dashboard', [
'kibana/typeahead',
]);
-app.directive('dashboardGrid', function (reactDirective) {
- return reactDirective(DashboardGrid);
-});
-
-app.directive('dashboardPanel', function (reactDirective) {
- return reactDirective(DashboardPanel);
+app.directive('dashboardViewport', function (reactDirective) {
+ return reactDirective(DashboardViewport);
});
-uiRoutes
- .when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {
- template: dashboardTemplate,
- resolve: {
- dash: function (savedDashboards, courier) {
- return savedDashboards.get()
- .catch(courier.redirectWhenMissing({
- 'dashboard': DashboardConstants.LANDING_PAGE_PATH
- }));
- }
- }
- })
- .when(createDashboardEditUrl(':id'), {
- template: dashboardTemplate,
- resolve: {
- dash: function (savedDashboards, Notifier, $route, $location, courier, kbnUrl, AppState) {
- const id = $route.current.params.id;
- return savedDashboards.get(id)
- .catch((error) => {
- // Preserve BWC of v5.3.0 links for new, unsaved dashboards.
- // See https://github.com/elastic/kibana/issues/10951 for more context.
- if (error instanceof SavedObjectNotFound && id === 'create') {
- // Note "new AppState" is neccessary so the state in the url is preserved through the redirect.
- kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState());
- notify.error(
- 'The url "dashboard/create" is deprecated and will be removed in 6.0. Please update your bookmarks.');
- } else {
- throw error;
- }
- })
- .catch(courier.redirectWhenMissing({
- 'dashboard' : DashboardConstants.LANDING_PAGE_PATH
- }));
- }
- }
- });
-
app.directive('dashboardApp', function ($injector) {
const Notifier = $injector.get('Notifier');
const courier = $injector.get('courier');
@@ -119,13 +73,15 @@ app.directive('dashboardApp', function ($injector) {
docTitle.change(dash.title);
}
- const dashboardState = new DashboardState(dash, AppState, dashboardConfig.getHideWriteControls());
+ const dashboardState = new DashboardStateManager(dash, AppState, dashboardConfig.getHideWriteControls());
+
+ $scope.getDashboardState = () => dashboardState;
$scope.appState = dashboardState.getAppState();
$scope.containerApi = new DashboardContainerAPI(
dashboardState,
(field, value, operator, index) => {
filterActions.addFilter(field, value, operator, index, dashboardState.getAppState(), filterManager);
- dashboardState.saveState();
+ dashboardState.saveAppState();
}
);
$scope.getContainerApi = () => $scope.containerApi;
@@ -155,7 +111,8 @@ app.directive('dashboardApp', function ($injector) {
this.appStatus = {
dirty: !dash.id
};
- dashboardState.stateMonitor.onChange(status => {
+
+ dashboardState.registerChangeListener(status => {
this.appStatus.dirty = status.dirty || !dash.id;
updateState();
});
@@ -301,19 +258,20 @@ app.directive('dashboardApp', function ($injector) {
};
$scope.save = function () {
- return dashboardState.saveDashboard(angular.toJson, timefilter).then(function (id) {
- $scope.kbnTopNav.close('save');
- if (id) {
- notify.info(`Saved Dashboard as "${dash.title}"`);
- if (dash.id !== $routeParams.id) {
- kbnUrl.change(createDashboardEditUrl(dash.id));
- } else {
- docTitle.change(dash.lastSavedTitle);
- updateViewMode(DashboardViewMode.VIEW);
+ return Promise.resolve(saveDashboard(angular.toJson, timefilter, dashboardState))
+ .then(function (id) {
+ $scope.kbnTopNav.close('save');
+ if (id) {
+ notify.info(`Saved Dashboard as "${dash.title}"`);
+ if (dash.id !== $routeParams.id) {
+ kbnUrl.change(createDashboardEditUrl(dash.id));
+ } else {
+ docTitle.change(dash.lastSavedTitle);
+ updateViewMode(DashboardViewMode.VIEW);
+ }
}
- }
- return id;
- }).catch(notify.error);
+ return id;
+ }).catch(notify.error);
};
$scope.showFilterBar = () => filterBar.getFilters().length > 0 || !$scope.fullScreenMode;
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_container_api.js b/src/core_plugins/kibana/public/dashboard/dashboard_container_api.js
index dafe443037660..3581f12af322a 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard_container_api.js
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_container_api.js
@@ -1,4 +1,7 @@
+import _ from 'lodash';
import { ContainerAPI } from 'ui/embeddable';
+import { store } from '../store';
+import { updatePanel } from './dashboard_actions';
export class DashboardContainerAPI extends ContainerAPI {
constructor(dashboardState, addFilter) {
@@ -8,10 +11,7 @@ export class DashboardContainerAPI extends ContainerAPI {
}
updatePanel(panelIndex, panelAttributes) {
- const panelToUpdate = this.dashboardState.getPanels().find((panel) => panel.panelIndex === panelIndex);
- Object.assign(panelToUpdate, panelAttributes);
- this.dashboardState.saveState();
- return panelToUpdate;
+ return this.dashboardState.updatePanel(panelIndex, panelAttributes);
}
getAppState() {
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_reducers.js b/src/core_plugins/kibana/public/dashboard/dashboard_reducers.js
new file mode 100644
index 0000000000000..eaaa90d0f94ef
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_reducers.js
@@ -0,0 +1,106 @@
+import _ from 'lodash';
+
+import {
+ UPDATE_PANELS,
+ EMBEDDABLE_RENDER_FINISHED,
+ EMBEDDABLE_RENDER_ERROR,
+ UPDATE_VIEW_MODE,
+ UPDATE_MAXIMIZED_PANEl_ID,
+ DELETE_PANEL,
+ UPDATE_PANEL,
+ ADD_NEW_PANEl,
+ UPDATE_IS_FULL_SCREEN_MODE
+} from './action_types';
+
+import { getInitialState } from './dashboard_store';
+
+export const dashboardReducers = (state = getInitialState(), action) => {
+ switch (action.type) {
+ case EMBEDDABLE_RENDER_FINISHED: {
+ return {
+ ...state,
+ embeddables: {
+ ...state.embeddables,
+ [action.panelId]: action.embeddable
+ },
+ panels: {
+ ...state.panels,
+ [action.panelId]: {
+ ...state.panels[action.panelId],
+ renderError: null
+ }
+ }
+ };
+ }
+ case UPDATE_PANELS: {
+ return {
+ ...state,
+ panels: _.cloneDeep(action.panels)
+ }
+ }
+ case UPDATE_PANEL: {
+ return {
+ ...state,
+ panels: {
+ ...state.panels,
+ [action.panel.panelIndex]: _.defaultsDeep(state.panels[action.panel.panelIndex], action.panel)
+ }
+ }
+ }
+ case ADD_NEW_PANEl: {
+ return {
+ ...state,
+ panels: {
+ ...state.panels,
+ [action.panel.panelIndex]: action.panel
+ }
+ }
+ }
+ case UPDATE_VIEW_MODE: {
+ return {
+ ...state,
+ viewMode: action.viewMode
+ }
+ }
+ case UPDATE_IS_FULL_SCREEN_MODE: {
+ return {
+ ...state,
+ isFullScreenMode: action.isFullScreenMode
+ }
+ }
+ case UPDATE_MAXIMIZED_PANEl_ID: {
+ return {
+ ...state,
+ maximizedPanelId: action.maximizedPanelId
+ }
+ }
+ case EMBEDDABLE_RENDER_ERROR: {
+ return {
+ ...state,
+ panels: {
+ ...state.panels,
+ [action.panelId]: {
+ ...state.panels[action.panelId],
+ renderError: action.error
+ }
+ }
+ }
+ }
+ case DELETE_PANEL: {
+ const stateCopy = {
+ ...state,
+ panels: {
+ ...state.panels
+ },
+ embeddables: {
+ ...state.embeddables
+ }
+ };
+ delete stateCopy.panels[action.panelId];
+ delete stateCopy.embeddables[action.panelId];
+ return stateCopy;
+ }
+ default:
+ return state;
+ }
+};
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_state.js b/src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js
similarity index 80%
rename from src/core_plugins/kibana/public/dashboard/dashboard_state.js
rename to src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js
index f291a68f4e485..2198998692848 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard_state.js
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js
@@ -1,27 +1,16 @@
import _ from 'lodash';
-import { FilterUtils } from './filter_utils';
+import { FilterUtils } from './lib/filter_utils';
import { DashboardViewMode } from './dashboard_view_mode';
import { PanelUtils } from './panel/panel_utils';
+import { store } from '../store';
+import { updateViewMode, updatePanels, updateIsFullScreenMode } from './dashboard_actions';
+
import moment from 'moment';
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
import { createPanelState, getPersistedStateId } from './panel';
-
-function getStateDefaults(dashboard, hideWriteControls) {
- return {
- fullScreenMode: false,
- title: dashboard.title,
- description: dashboard.description,
- timeRestore: dashboard.timeRestore,
- panels: dashboard.panelsJSON ? JSON.parse(dashboard.panelsJSON) : [],
- options: dashboard.optionsJSON ? JSON.parse(dashboard.optionsJSON) : {},
- uiState: dashboard.uiStateJSON ? JSON.parse(dashboard.uiStateJSON) : {},
- query: FilterUtils.getQueryFilterForDashboard(dashboard),
- filters: FilterUtils.getFilterBarsForDashboard(dashboard),
- viewMode: dashboard.id || hideWriteControls ? DashboardViewMode.VIEW : DashboardViewMode.EDIT,
- };
-}
+import { getAppStateDefaults } from './lib/get_app_state_defaults';
/**
* Depending on how a dashboard is loaded, the filter object may contain a $$hashKey and $state that will throw
@@ -54,18 +43,42 @@ function areTimesEqual(timeA, timeB) {
return convertTimeToString(timeA) === convertTimeToString(timeB);
}
-export class DashboardState {
+/**
+ * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the
+ * app. There are two "sources of truth" that need to stay in sync - AppState and the Store. They aren't complete
+ * duplicates of each other as AppState has state that the Store doesn't, and vice versa.
+ *
+ * State that is only stored in AppState:
+ * - title
+ * - description
+ * - timeRestore
+ * - query
+ * - uiState
+ * - filters
+ *
+ * State that is only stored in the Store:
+ * - embeddables
+ * - maximizedPanelId
+ *
+ * State that is shared and needs to be synced:
+ * - fullScreenMode - changes only propagate from AppState -> Store
+ * - viewMode - changes only propagate from AppState -> Store
+ * - panels - changes propagate from AppState -> Store and from Store -> AppState.
+ *
+ *
+ */
+export class DashboardStateManager {
/**
*
* @param savedDashboard {SavedDashboard}
- * @param AppState {AppState}
+ * @param AppState {AppState} The AppState class to use when instantiating a new AppState instance.
* @param hideWriteControls {boolean} true if write controls should be hidden.
*/
constructor(savedDashboard, AppState, hideWriteControls) {
this.savedDashboard = savedDashboard;
this.hideWriteControls = hideWriteControls;
- this.stateDefaults = getStateDefaults(this.savedDashboard, this.hideWriteControls);
+ this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls);
this.appState = new AppState(this.stateDefaults);
this.uiState = this.appState.makeStateful('uiState');
@@ -74,7 +87,7 @@ export class DashboardState {
// We can't compare the filters stored on this.appState to this.savedDashboard because in order to apply
// the filters to the visualizations, we need to save it on the dashboard. We keep track of the original
// filter state in order to let the user know if their filters changed and provide this specific information
- //in the 'lose changes' warning message.
+ // in the 'lose changes' warning message.
this.lastSavedDashboardFilters = this.getFilterState();
// A mapping of panel index to the index pattern it uses.
@@ -82,6 +95,57 @@ export class DashboardState {
PanelUtils.initPanelIndexes(this.getPanels());
this.createStateMonitor();
+
+ store.dispatch(updatePanels(this.getPanels()));
+
+ this.changeListeners = [];
+
+ store.subscribe(() => this._handleStoreChanges());
+ this.stateMonitor.onChange(status => {
+ this.changeListeners.forEach(listener => listener(status));
+ this._pushAppStateChangesToStore();
+ });
+ }
+
+ /**
+ * Changes made to app state outside of direct calls to this class will need to be propagated to the store.
+ * @private
+ */
+ _pushAppStateChangesToStore() {
+ store.dispatch(updatePanels(this.getPanels()));
+ store.dispatch(updateViewMode(this.getViewMode()));
+ store.dispatch(updateIsFullScreenMode(this.getFullScreenMode()));
+ }
+
+ registerChangeListener(callback) {
+ this.changeListeners.push(callback);
+ }
+
+ _handleStoreChanges() {
+ const { dashboardState } = store.getState();
+
+ // We need to run this comparison check or we can enter an infinite loop.
+ let differencesFound = false;
+ for (let i = 0; i < this.appState.panels.length; i++) {
+ const appStatePanel = this.appState.panels[i];
+ if (!_.isEqual(appStatePanel, dashboardState.panels[appStatePanel.panelIndex])) {
+ differencesFound = true;
+ break;
+ }
+ }
+
+ if (!differencesFound) {
+ return;
+ }
+
+ // The only state that the store deals with that appState cares about is the panels array. Every other state change
+ // (that appState cares about) is initiated from appState (e.g. view mode).
+ this.appState.panels = [];
+ _.map(dashboardState.panels, panel => {
+ this.appState.panels.push(panel);
+ });
+ this.changeListeners.forEach(listener => listener(status));
+ this.saveState();
}
getFullScreenMode() {
@@ -117,7 +181,7 @@ export class DashboardState {
// The right way to fix this might be to ensure the defaults object stored on state is a deep
// clone, but given how much code uses the state object, I determined that to be too risky of a change for
// now. TODO: revisit this!
- this.stateDefaults = getStateDefaults(this.savedDashboard, this.hideWriteControls);
+ this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls);
// The original query won't be restored by the above because the query on this.savedDashboard is applied
// in place in order for it to affect the visualizations.
this.stateDefaults.query = this.lastSavedDashboardFilters.query;
@@ -291,6 +355,13 @@ export class DashboardState {
return this.appState.panels;
}
+ updatePanel(panelIndex, panelAttributes) {
+ const updatedPanel = this.getPanels().find((panel) => panel.panelIndex === panelIndex);
+ Object.assign(updatedPanel, panelAttributes);
+ this.saveState();
+ return updatedPanel;
+ }
+
/**
* Creates and initializes a basic panel, adding it to the state.
* @param {number} id
@@ -298,7 +369,9 @@ export class DashboardState {
*/
addNewPanel(id, type) {
const maxPanelIndex = PanelUtils.getMaxPanelIndex(this.getPanels());
- this.getPanels().push(createPanelState(id, type, maxPanelIndex, this.getPanels()));
+ const newPanel = createPanelState(id, type, maxPanelIndex, this.getPanels());
+ this.getPanels().push(newPanel);
+ this.saveState();
}
removePanel(panelIndex) {
@@ -443,6 +516,7 @@ export class DashboardState {
*/
switchViewMode(newMode) {
this.appState.viewMode = newMode;
+ store.dispatch(updateViewMode(newMode));
this.saveState();
}
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_store.js b/src/core_plugins/kibana/public/dashboard/dashboard_store.js
new file mode 100644
index 0000000000000..7ea6a1a74c29d
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_store.js
@@ -0,0 +1,19 @@
+import { DashboardViewMode } from './dashboard_view_mode';
+
+/**
+ * This is a poor name choice for this state but we have dashboard_state and DashboardState to contend with.
+ * Redux state trees hold no logic, while DashboardState does, and is tightly somewhat tightly to angular. The
+ * end goal will be to replace dashboard_state with this component, and then we can improve the naming.
+ *
+ * @type {{}}
+ */
+export function getInitialState() {
+ return {
+ panels: {}, // Mapping of panel ids to panel state
+ // Mapping of panel id to the function that should be used to destroy the rendered embeddable:
+ embeddables: {}, // Panel id to embeddable object
+ isFullScreenMode: false,
+ viewMode: DashboardViewMode.VIEW,
+ maximizedPanelId: undefined
+ }
+}
diff --git a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
index 002eb2756eb2d..7f6341e657125 100644
--- a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
+++ b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
@@ -4,7 +4,7 @@ import _ from 'lodash';
import ReactGridLayout from 'react-grid-layout';
import { PanelUtils } from '../panel/panel_utils';
import { DashboardViewMode } from '../dashboard_view_mode';
-import { DashboardPanel } from '../panel/dashboard_panel';
+import { DashboardPanelContainer } from '../panel/dashboard_panel_container';
import { DASHBOARD_GRID_COLUMN_COUNT } from '../dashboard_constants';
import sizeMe from 'react-sizeme';
@@ -50,38 +50,32 @@ export class DashboardGrid extends React.Component {
}
onLayoutChange = (layout) => {
- const { panels, getContainerApi } = this.props;
-
- const containerApi = getContainerApi();
-
+ const { onPanelUpdated } = this.props;
layout.forEach(panelLayout => {
- const panelUpdated = _.find(panels, panel => panel.panelIndex.toString() === panelLayout.i);
- panelUpdated.gridData = {
- x: panelLayout.x,
- y: panelLayout.y,
- w: panelLayout.w,
- h: panelLayout.h,
- i: panelLayout.i,
+ const updatedPanel = {
+ panelIndex: panelLayout.i,
+ gridData: {
+ x: panelLayout.x,
+ y: panelLayout.y,
+ w: panelLayout.w,
+ h: panelLayout.h,
+ i: panelLayout.i,
+ },
version: panelLayout.version
};
- panelUpdated.gridData.version = 6.0;
- containerApi.updatePanel(panelUpdated.panelIndex, panelUpdated);
+ onPanelUpdated(updatedPanel);
});
};
renderDOM() {
const {
panels,
- onPanelRemoved,
- expandPanel,
- isFullScreenMode,
getEmbeddableHandler,
getContainerApi,
- dashboardViewMode
} = this.props;
// Part of our unofficial API - need to render in a consistent order for plugins.
- const panelsInOrder = panels.slice(0);
+ const panelsInOrder = Object.values(panels);
panelsInOrder.sort((panelA, panelB) => {
if (panelA.gridData.y === panelB.gridData.y) {
return panelA.gridData.x - panelB.gridData.x;
@@ -93,15 +87,10 @@ export class DashboardGrid extends React.Component {
return _.map(panelsInOrder, panel => {
return (
-
);
@@ -124,12 +113,10 @@ export class DashboardGrid extends React.Component {
}
DashboardGrid.propTypes = {
- isFullScreenMode: PropTypes.bool.isRequired,
- panels: PropTypes.array.isRequired,
+ panels: PropTypes.object.isRequired,
getContainerApi: PropTypes.func.isRequired,
getEmbeddableHandler: PropTypes.func.isRequired,
dashboardViewMode: PropTypes.oneOf([DashboardViewMode.EDIT, DashboardViewMode.VIEW]).isRequired,
- expandPanel: PropTypes.func.isRequired,
- onPanelRemoved: PropTypes.func.isRequired,
+ onPanelUpdated: PropTypes.func.isRequired,
};
diff --git a/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.js b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.js
new file mode 100644
index 0000000000000..db53ee245c795
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.js
@@ -0,0 +1,24 @@
+import { connect } from 'react-redux'
+import { DashboardGrid } from './dashboard_grid';
+import { updatePanel } from '../dashboard_actions';
+
+const mapStateToProps = ({ dashboardState }, ownProps) => {
+ return {
+ panels: dashboardState.panels,
+ getContainerApi: ownProps.getContainerApi,
+ getEmbeddableHandler: ownProps.getEmbeddableHandler,
+ dashboardViewMode: dashboardState.viewMode,
+ };
+};
+
+const mapDispatchToProps = (dispatch, ownProps) => ({
+ onPanelUpdated: updatedPanel => {
+ dispatch(updatePanel(updatedPanel));
+ }
+});
+
+export const DashboardGridContainer = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(DashboardGrid);
+
diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js
index fbcb1ae500260..3d741eea943cd 100644
--- a/src/core_plugins/kibana/public/dashboard/index.js
+++ b/src/core_plugins/kibana/public/dashboard/index.js
@@ -1,12 +1,15 @@
-import 'plugins/kibana/dashboard/dashboard';
+import 'plugins/kibana/dashboard/dashboard_app';
import 'plugins/kibana/dashboard/saved_dashboard/saved_dashboards';
import 'plugins/kibana/dashboard/styles/index.less';
import 'plugins/kibana/dashboard/dashboard_config';
import uiRoutes from 'ui/routes';
+import dashboardTemplate from 'plugins/kibana/dashboard/dashboard_app.html';
import dashboardListingTemplate from './listing/dashboard_listing.html';
+
import { DashboardListingController } from './listing/dashboard_listing';
-import { DashboardConstants } from './dashboard_constants';
+import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
+import { SavedObjectNotFound } from 'ui/errors';
uiRoutes
.defaults(/dashboard/, {
@@ -16,4 +19,39 @@ uiRoutes
template: dashboardListingTemplate,
controller: DashboardListingController,
controllerAs: 'listingController'
+ })
+ .when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {
+ template: dashboardTemplate,
+ resolve: {
+ dash: function (savedDashboards, courier) {
+ return savedDashboards.get()
+ .catch(courier.redirectWhenMissing({
+ 'dashboard': DashboardConstants.LANDING_PAGE_PATH
+ }));
+ }
+ }
+ })
+ .when(createDashboardEditUrl(':id'), {
+ template: dashboardTemplate,
+ resolve: {
+ dash: function (savedDashboards, Notifier, $route, $location, courier, kbnUrl, AppState) {
+ const id = $route.current.params.id;
+ return savedDashboards.get(id)
+ .catch((error) => {
+ // Preserve BWC of v5.3.0 links for new, unsaved dashboards.
+ // See https://github.com/elastic/kibana/issues/10951 for more context.
+ if (error instanceof SavedObjectNotFound && id === 'create') {
+ // Note "new AppState" is neccessary so the state in the url is preserved through the redirect.
+ kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState());
+ notify.error(
+ 'The url "dashboard/create" is deprecated and will be removed in 6.0. Please update your bookmarks.');
+ } else {
+ throw error;
+ }
+ })
+ .catch(courier.redirectWhenMissing({
+ 'dashboard' : DashboardConstants.LANDING_PAGE_PATH
+ }));
+ }
+ }
});
diff --git a/src/core_plugins/kibana/public/dashboard/lib/convert_time_to_string.js b/src/core_plugins/kibana/public/dashboard/lib/convert_time_to_string.js
new file mode 100644
index 0000000000000..1ef9337f3e826
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/lib/convert_time_to_string.js
@@ -0,0 +1,10 @@
+import moment from 'moment';
+
+/**
+ * Converts the time to a string, if it isn't already.
+ * @param time {string|Moment}
+ * @returns {string}
+ */
+function convertTimeToString(time) {
+ return typeof time === 'string' ? time : moment(time).toString();
+}
diff --git a/src/core_plugins/kibana/public/dashboard/filter_utils.js b/src/core_plugins/kibana/public/dashboard/lib/filter_utils.js
similarity index 100%
rename from src/core_plugins/kibana/public/dashboard/filter_utils.js
rename to src/core_plugins/kibana/public/dashboard/lib/filter_utils.js
diff --git a/src/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.js b/src/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.js
new file mode 100644
index 0000000000000..5880cf3503e37
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.js
@@ -0,0 +1,17 @@
+import { DashboardViewMode } from '../dashboard_view_mode';
+import { FilterUtils } from './filter_utils';
+
+export function getAppStateDefaults(savedDashboard, hideWriteControls) {
+ return {
+ fullScreenMode: false,
+ title: savedDashboard.title,
+ description: savedDashboard.description,
+ timeRestore: savedDashboard.timeRestore,
+ panels: savedDashboard.panelsJSON ? JSON.parse(savedDashboard.panelsJSON) : [],
+ options: savedDashboard.optionsJSON ? JSON.parse(savedDashboard.optionsJSON) : {},
+ uiState: savedDashboard.uiStateJSON ? JSON.parse(savedDashboard.uiStateJSON) : {},
+ query: FilterUtils.getQueryFilterForDashboard(savedDashboard),
+ filters: FilterUtils.getFilterBarsForDashboard(savedDashboard),
+ viewMode: savedDashboard.id || hideWriteControls ? DashboardViewMode.VIEW : DashboardViewMode.EDIT,
+ };
+}
diff --git a/src/core_plugins/kibana/public/dashboard/lib/index.js b/src/core_plugins/kibana/public/dashboard/lib/index.js
new file mode 100644
index 0000000000000..5cbae8cf151d9
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/lib/index.js
@@ -0,0 +1 @@
+export { saveDashboard } from './save_dashboard';
diff --git a/src/core_plugins/kibana/public/dashboard/lib/save_dashboard.js b/src/core_plugins/kibana/public/dashboard/lib/save_dashboard.js
new file mode 100644
index 0000000000000..82a1375b69b04
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/lib/save_dashboard.js
@@ -0,0 +1,27 @@
+import { DashboardViewMode } from '../dashboard_view_mode';
+import { updateSavedDashboard } from './update_saved_dashboard';
+import { getAppStateDefaults } from './get_app_state_defaults';
+
+/**
+ * Saves the dashboard.
+ * @param toJson {function} A custom toJson function. Used because the previous code used
+ * the angularized toJson version, and it was unclear whether there was a reason not to use
+ * JSON.stringify
+ * @param timefilter
+ * @returns {Promise} A promise that if resolved, will contain the id of the newly saved
+ * dashboard.
+ */
+export function saveDashboard(toJson, timeFilter, dashboardStateManager) {
+ dashboardStateManager.saveState();
+
+ const savedDashboard = dashboardStateManager.savedDashboard;
+ const appState = dashboardStateManager.appState;
+
+ updateSavedDashboard(savedDashboard, appState, dashboardStateManager.uiState, timeFilter, toJson);
+
+ return savedDashboard.save()
+ .then((id) => {
+ dashboardStateManager.resetState();
+ return id;
+ });
+}
diff --git a/src/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.js b/src/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.js
new file mode 100644
index 0000000000000..33a63895df116
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.js
@@ -0,0 +1,15 @@
+import { convertTimeToString } from './convert_time_to_string';
+
+export function updateSavedDashboard(savedDashboard, appState, uiState, timeFilter, toJson) {
+ savedDashboard.title = appState.title;
+ savedDashboard.description = appState.description;
+ savedDashboard.timeRestore = appState.timeRestore;
+ savedDashboard.panelsJSON = toJson(appState.panels);
+ savedDashboard.uiStateJSON = toJson(uiState.getChanges());
+ savedDashboard.optionsJSON = toJson(appState.options);
+
+ savedDashboard.timeFrom = savedDashboard.timeRestore ? convertTimeToString(timeFilter.time.from) : undefined;
+ savedDashboard.timeTo = savedDashboard.timeRestore ? convertTimeToString(timeFilter.time.to) : undefined;
+ const timeRestoreObj = _.pick(timeFilter.refreshInterval, ['display', 'pause', 'section', 'value']);
+ savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined;
+}
diff --git a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
index 64f6899b71b3e..91231fcf0c977 100644
--- a/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
+++ b/src/core_plugins/kibana/public/dashboard/panel/dashboard_panel.js
@@ -3,45 +3,17 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { DashboardViewMode } from '../dashboard_view_mode';
import { PanelHeader } from './panel_header';
export class DashboardPanel extends React.Component {
constructor(props) {
super(props);
- this.state = {};
- this.embeddable = null;
- this.embeddableHandler = null;
this.parentNode = null;
- this._isMounted = false;
}
async componentDidMount() {
- this._isMounted = true;
- const { getEmbeddableHandler, panel, getContainerApi } = this.props;
-
- this.containerApi = getContainerApi();
- this.embeddableHandler = getEmbeddableHandler(panel.type);
- const editUrl = await this.embeddableHandler.getEditPath(panel.id);
- const title = await this.embeddableHandler.getTitleFor(panel.id);
-
- // TODO: use redux instead of the isMounted anti-pattern to handle the case when the component is unmounted
- // before the async calls above return. We can then get rid of the eslint disable line. Without redux, there is
- // not a better option, since you aren't supposed to run async calls inside of componentWillMount.
- if (this._isMounted) {
- /* eslint-disable react/no-did-mount-set-state */
- this.setState({ editUrl, title });
-
- this.destroyEmbeddable = await this.embeddableHandler.render(
- this.panelElement,
- panel,
- this.containerApi);
- }
- }
-
- isViewOnlyMode() {
- return this.props.dashboardViewMode === DashboardViewMode.VIEW || this.props.isFullScreenMode;
+ this.props.renderEmbeddable(this.panelElement);
}
getParentNode() {
@@ -51,11 +23,16 @@ export class DashboardPanel extends React.Component {
return this.parentNode;
}
- toggleExpandedPanel = () => this.props.onToggleExpanded(this.props.panel.panelIndex);
- deletePanel = () => {
- this.props.onDeletePanel(this.props.panel.panelIndex);
+ toggleExpandedPanel = () => {
+ const { isExpanded, onMaximizePanel, onMinimizePanel } = this.props;
+ if (isExpanded) {
+ onMinimizePanel();
+ } else {
+ onMaximizePanel();
+ }
};
- onEditPanel = () => window.location = this.state.editUrl;
+ deletePanel = () => this.props.onDeletePanel();
+ onEditPanel = () => window.location = this.props.editUrl;
/**
* Setting the zIndex on onFocus and onBlur allows popups, like the panel menu, to appear above other panels in the
@@ -69,21 +46,13 @@ export class DashboardPanel extends React.Component {
};
componentWillUnmount() {
- // This is required because it's possible the component becomes unmounted before embeddableHandler.render returns.
- // This is really an anti-pattern and could be cleaned up by implementing a redux framework for dashboard state.
- // Because implementing that may be a very large change in and of itself, it will be a second step, and we'll live
- // with this anti-pattern for the time being.
- this._isMounted = false;
- if (this.destroyEmbeddable) {
- this.destroyEmbeddable();
- }
+ this.props.onDestroy();
}
render() {
- const { title } = this.state;
- const { dashboardViewMode, isFullScreenMode, isExpanded } = this.props;
+ const { viewOnlyMode, isExpanded, title } = this.props;
const classes = classNames('panel panel-default', this.props.className, {
- 'panel--edit-mode': !this.isViewOnlyMode()
+ 'panel--edit-mode': !viewOnlyMode
});
return (
{
+ const embeddable = dashboardState.embeddables[ownProps.panelId];
+ const title = embeddable ? embeddable.title : '';
+ const editUrl = embeddable ? embeddable.editUrl : '';
+ return {
+ viewOnlyMode: dashboardState.isFullScreenMode || dashboardState.viewMode === DashboardViewMode.VIEW,
+ title,
+ editUrl,
+ isExpanded: dashboardState.maximizedPanelId === ownProps.panelId,
+ };
+};
+
+const mapDispatchToProps = (dispatch, ownProps) => ({
+ renderEmbeddable: (panelElement) => {
+ dispatch(renderEmbeddable(ownProps.embeddableHandler, panelElement, ownProps.panelId, ownProps.getContainerApi()))
+ },
+ onDeletePanel: () => {
+ dispatch(deletePanel(ownProps.panelId));
+ },
+ onMaximizePanel: () => {
+ dispatch(maximizePanel(ownProps.panelId));
+ },
+ onMinimizePanel: () => {
+ dispatch(minimizePanel());
+ },
+ onDestroy: () => {
+ ownProps.embeddableHandler.destroy(ownProps.panelId);
+ }
+});
+
+export const DashboardPanelContainer = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(DashboardPanel);
+
diff --git a/src/core_plugins/kibana/public/dashboard/styles/index.less b/src/core_plugins/kibana/public/dashboard/styles/index.less
index 8d7fb63a54391..db80779e71da3 100644
--- a/src/core_plugins/kibana/public/dashboard/styles/index.less
+++ b/src/core_plugins/kibana/public/dashboard/styles/index.less
@@ -101,17 +101,14 @@
border-radius: 4px;
}
-/**
- * 1. Not entirely sure why but when the panel is within the grid, it requires height 100%. When it's an expanded
- * panel, however, outside the grid, height: 100% will cause the panel not to expand properly.
- * 2. We need this so the panel menu pop up shows up outside the boundaries of a panel.
+/**.
+ * 1. We need this so the panel menu pop up shows up outside the boundaries of a panel.
*/
.react-grid-layout {
background-color: @dashboard-bg;
.dashboard-panel {
- height: 100%; /* 1. */
- overflow: visible; /* 2. */
+ overflow: visible; /* 1. */
}
.gs-w {
@@ -140,9 +137,15 @@
flex-direction: column;
}
-dashboard-panel {
+/**
+ * Needs to correspond with the react root nested inside angular.
+ */
+dashboard-viewport {
flex: 1;
display: flex;
+ [data-reactroot] {
+ flex: 1;
+ }
}
/**
@@ -155,6 +158,7 @@ dashboard-panel {
flex: 1;
display: flex;
flex-direction: column;
+ height: 100%;
background: @dashboard-panel-bg;
color: @dashboard-panel-color;
diff --git a/src/core_plugins/kibana/public/dashboard/viewport/dashboard_container.js b/src/core_plugins/kibana/public/dashboard/viewport/dashboard_container.js
new file mode 100644
index 0000000000000..ea1d8078fe010
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/viewport/dashboard_container.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux'
+import { DashboardPanelContainer } from '../panel/dashboard_panel_container';
+import { DashboardGridContainer } from '../grid/dashboard_grid_container';
+
+function Dashboard({ getContainerApi, maximizedPanelId, maximizedPanelEmbeddableHandler, getEmbeddableHandler }) {
+ function renderMaximizedPanel() {
+ return (
+
+ );
+ }
+
+ function renderDashboardGrid() {
+ return (
+
+ );
+ }
+
+ return (
+
+ {maximizedPanelId === undefined ? renderDashboardGrid() : renderMaximizedPanel()}
+
+ );
+}
+
+Dashboard.propTypes = {
+ getContainerApi: PropTypes.func,
+ getEmbeddableHandler: PropTypes.func,
+ maximizedPanelId: PropTypes.number,
+ maximizedPanelEmbeddableHandler: PropTypes.object
+};
+
+const mapStateToProps = ({ dashboardState }, ownProps) => {
+ if (dashboardState.maximizedPanelId !== undefined) {
+ const panel = dashboardState.panels[dashboardState.maximizedPanelId];
+ const maximizedPanelEmbeddableHandler = ownProps.getEmbeddableHandler(panel.type);
+ return {
+ maximizedPanelId: dashboardState.maximizedPanelId,
+ maximizedPanelEmbeddableHandler
+ }
+ } else {
+ return {
+ getEmbeddableHandler: ownProps.getEmbeddableHandler,
+ getContainerApi: ownProps.getContainerApi
+ }
+ }
+};
+
+const mapDispatchToProps = (dispatch, ownProps) => ({
+});
+
+export const DashboardContainer = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(Dashboard);
+
diff --git a/src/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport.js b/src/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport.js
new file mode 100644
index 0000000000000..c954c5b6b7c69
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { store } from '../../store';
+import { Provider } from 'react-redux';
+import { DashboardContainer } from './dashboard_container';
+
+export class DashboardViewport extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+DashboardViewport.propTypes = {
+ getContainerApi: PropTypes.func.isRequired,
+ getEmbeddableHandler: PropTypes.func.isRequired,
+};
diff --git a/src/core_plugins/kibana/public/discover/embeddable/search_embeddable_handler.js b/src/core_plugins/kibana/public/discover/embeddable/search_embeddable_handler.js
index f816b13ec771d..29aef9878d59c 100644
--- a/src/core_plugins/kibana/public/discover/embeddable/search_embeddable_handler.js
+++ b/src/core_plugins/kibana/public/discover/embeddable/search_embeddable_handler.js
@@ -2,8 +2,7 @@ import searchTemplate from './search_template.html';
import angular from 'angular';
import * as columnActions from 'ui/doc_table/actions/columns';
import { getPersistedStateId } from 'plugins/kibana/dashboard/panel/panel_state';
-import { EmbeddableHandler } from 'ui/embeddable';
-
+import { EmbeddableHandler, Embeddable } from 'ui/embeddable';
export class SearchEmbeddableHandler extends EmbeddableHandler {
@@ -17,7 +16,7 @@ export class SearchEmbeddableHandler extends EmbeddableHandler {
}
getEditPath(panelId) {
- return this.Promise.resolve(this.searchLoader.urlFor(panelId));
+ return this.searchLoader.urlFor(panelId);
}
getTitleFor(panelId) {
@@ -77,11 +76,16 @@ export class SearchEmbeddableHandler extends EmbeddableHandler {
const rootNode = angular.element(domNode);
rootNode.append(searchInstance);
- return () => {
+ this.addDestroyEmeddable(panel.panelIndex, () => {
searchInstance.remove();
searchScope.savedObj.destroy();
searchScope.$destroy();
- };
+ });
+
+ return new Embeddable({
+ title: savedObject.title,
+ editUrl: searchScope.editPath
+ });
});
}
}
diff --git a/src/core_plugins/kibana/public/reducers.js b/src/core_plugins/kibana/public/reducers.js
new file mode 100644
index 0000000000000..23e3ac32ea710
--- /dev/null
+++ b/src/core_plugins/kibana/public/reducers.js
@@ -0,0 +1,10 @@
+import { combineReducers } from 'redux'
+import { dashboardReducers } from './dashboard/dashboard_reducers';
+
+/**
+ * Only a single reducer now, but eventually there should be one for each sub app that is part of the
+ * core kibana plugins.
+ */
+export const reducers = combineReducers({
+ dashboardState: dashboardReducers
+});
diff --git a/src/core_plugins/kibana/public/store.js b/src/core_plugins/kibana/public/store.js
new file mode 100644
index 0000000000000..1364362b5da56
--- /dev/null
+++ b/src/core_plugins/kibana/public/store.js
@@ -0,0 +1,12 @@
+import { createStore, applyMiddleware } from 'redux';
+import { reducers } from './reducers';
+import thunkMiddleware from 'redux-thunk';
+import { getInitialState } from './dashboard/dashboard_store';
+
+export const store = createStore(
+ reducers,
+ {
+ dashboardState: getInitialState()
+ },
+ applyMiddleware(thunkMiddleware)
+);
diff --git a/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_handler.js b/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_handler.js
index f5a15b215c2be..04740b12f91a1 100644
--- a/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_handler.js
+++ b/src/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_handler.js
@@ -5,6 +5,8 @@ import { getPersistedStateId } from 'plugins/kibana/dashboard/panel/panel_state'
import { UtilsBrushEventProvider as utilsBrushEventProvider } from 'ui/utils/brush_event';
import { FilterBarClickHandlerProvider as filterBarClickHandlerProvider } from 'ui/filter_bar/filter_bar_click_handler';
import { EmbeddableHandler } from 'ui/embeddable';
+import { Embeddable } from 'ui/embeddable';
+
import chrome from 'ui/chrome';
export class VisualizeEmbeddableHandler extends EmbeddableHandler {
@@ -20,20 +22,13 @@ export class VisualizeEmbeddableHandler extends EmbeddableHandler {
}
getEditPath(panelId) {
- return this.Promise.resolve(this.visualizeLoader.urlFor(panelId));
- }
-
- getTitleFor(panelId) {
- return this.visualizeLoader.get(panelId).then(savedObject => savedObject.title);
+ return this.visualizeLoader.urlFor(panelId);
}
render(domNode, panel, container) {
const visualizeScope = this.$rootScope.$new();
- return this.getEditPath(panel.id)
- .then(editPath => {
- visualizeScope.editUrl = editPath;
- return this.visualizeLoader.get(panel.id);
- })
+ visualizeScope.editUrl = this.getEditPath(panel.id);
+ return this.visualizeLoader.get(panel.id)
.then(savedObject => {
visualizeScope.savedObj = savedObject;
visualizeScope.panel = panel;
@@ -56,11 +51,16 @@ export class VisualizeEmbeddableHandler extends EmbeddableHandler {
const rootNode = angular.element(domNode);
rootNode.append(visualizationInstance);
- return () => {
+ this.addDestroyEmeddable(panel.panelIndex, () => {
visualizationInstance.remove();
visualizeScope.savedObj.destroy();
visualizeScope.$destroy();
- };
+ });
+
+ return new Embeddable({
+ title: savedObject.title,
+ editUrl: visualizeScope.editUrl
+ });
});
}
}
diff --git a/src/ui/public/embeddable/embeddable.js b/src/ui/public/embeddable/embeddable.js
new file mode 100644
index 0000000000000..c111cb997ed99
--- /dev/null
+++ b/src/ui/public/embeddable/embeddable.js
@@ -0,0 +1,6 @@
+export class Embeddable {
+ constructor(config) {
+ this.title = config.title || '';
+ this.editUrl = config.editUrl || '';
+ }
+}
diff --git a/src/ui/public/embeddable/embeddable_handler.js b/src/ui/public/embeddable/embeddable_handler.js
index a60fec577d7a8..0e64b909a2c57 100644
--- a/src/ui/public/embeddable/embeddable_handler.js
+++ b/src/ui/public/embeddable/embeddable_handler.js
@@ -3,21 +3,8 @@
* container that supports EmbeddableHandlers.
*/
export class EmbeddableHandler {
- /**
- * @param {string} panelId - the id of the panel to grab the title for.
- * @return {Promise.
} a promise that resolves with the path that dictates where the user will be navigated to
- * when they click the edit icon.
- */
- getEditPath(/* panelId */) {
- throw new Error('Must implement getEditPath.');
- }
-
- /**
- * @param {string} panelId - the id of the panel to grab the title for.
- * @return {Promise.} - Promise that resolves with the title to display for the particular panel.
- */
- getTitleFor(/* panelId */) {
- throw new Error('Must implement getTitleFor.');
+ constructor() {
+ this.destroyEmbeddableMap = {};
}
/**
@@ -26,10 +13,19 @@ export class EmbeddableHandler {
* store per panel information.
* @property {ContainerApi} containerApi - an id to specify the object that this panel contains.
* @param {Promise.} A promise that resolves when the object is finished rendering.
- * @return {Promise.} A promise that resolves to a function that should be used to destroy the
+ * @return {Promise.} A promise that resolves to a function that should be used to destroy the
* rendered embeddable.
*/
render(/* domNode, panel, container */) {
throw new Error('Must implement render.');
}
+
+ addDestroyEmeddable(panelIndex, destroyEmbeddable) {
+ this.destroyEmbeddableMap[panelIndex] = destroyEmbeddable;
+ }
+
+ destroy(panelIndex) {
+ this.destroyEmbeddableMap[panelIndex]();
+ delete this.destroyEmbeddableMap[panelIndex];
+ }
}
diff --git a/src/ui/public/embeddable/embeddable_handlers_registry.js b/src/ui/public/embeddable/embeddable_handlers_registry.js
index 408288c842173..592df6c513f61 100644
--- a/src/ui/public/embeddable/embeddable_handlers_registry.js
+++ b/src/ui/public/embeddable/embeddable_handlers_registry.js
@@ -7,3 +7,4 @@ export const EmbeddableHandlersRegistryProvider = uiRegistry({
name: 'embeddableHandlers',
index: ['name']
});
+
diff --git a/src/ui/public/embeddable/index.js b/src/ui/public/embeddable/index.js
index bdd73ce99a110..654d75a75b8b3 100644
--- a/src/ui/public/embeddable/index.js
+++ b/src/ui/public/embeddable/index.js
@@ -1,3 +1,4 @@
export { EmbeddableHandler } from './embeddable_handler';
+export { Embeddable } from './embeddable';
export { EmbeddableHandlersRegistryProvider } from './embeddable_handlers_registry';
export { ContainerAPI } from './container_api';
diff --git a/src/ui/public/state_management/state_monitor_factory.js b/src/ui/public/state_management/state_monitor_factory.js
index 4b667b330b08d..df8bdcb6125a0 100644
--- a/src/ui/public/state_management/state_monitor_factory.js
+++ b/src/ui/public/state_management/state_monitor_factory.js
@@ -37,9 +37,11 @@ function stateMonitor(state, customInitialState) {
function dispatchChange(type = null, keys = []) {
const status = getStatus();
- changeHandlers.forEach(changeHandler => {
- changeHandler(status, type, keys);
- });
+ if (changeHandlers) {
+ changeHandlers.forEach(changeHandler => {
+ changeHandler(status, type, keys);
+ });
+ }
}
function dispatchFetch(keys) {
diff --git a/src/ui/public/state_management/update_app_state.js b/src/ui/public/state_management/update_app_state.js
new file mode 100644
index 0000000000000..747549e74b342
--- /dev/null
+++ b/src/ui/public/state_management/update_app_state.js
@@ -0,0 +1,18 @@
+import _ from 'lodash';
+
+/**
+ * Syncs values from pureState to appState, and then saves the appState so that changes are propagated.
+ * Helps connect the angularized AppState with pure redux stores.
+ *
+ * @param appState {AppState} - An instance of AppState. Holds state and manages synchronization between the url,
+ * angular and other apps.
+ * @param pureState {Object} - a pure state tree (no functionality).
+ */
+export function updateAppState(appState, pureState) {
+ for (const key in pureState) {
+ if (pureState.hasOwnProperty(key)) {
+ appState[key] = _.cloneDeep(pureState[key]);
+ }
+ }
+ appState.save();
+}