diff --git a/package.json b/package.json index 30aa3c178ae00f..7bbd96666f9710 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", @@ -172,6 +171,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", @@ -179,6 +179,7 @@ "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-toggle": "3.0.1", "reactcss": "1.0.7", 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 109e9c34e96242..00000000000000 --- 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 35b807ff0cf547..00000000000000 --- 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 4586d851d2c338..07573ee07e6541 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 0d52e6cae21656..fddba4013732b3 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,34 @@ 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', ]); +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 +111,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 +128,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 +200,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 16542f1edaf94b..ed4b62367c4c52 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_constants.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_constants.js @@ -4,6 +4,9 @@ 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) { 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 e8e90a6f71011c..f291a68f4e4859 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 './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 54fd3fab1ab79d..00000000000000 --- 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 = $('