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..fddba4013732b 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 16542f1edaf94..ed4b62367c4c5 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 e8e90a6f71011..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/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 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..1d3bcbd00cc13
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap
@@ -0,0 +1,89 @@
+// 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..c84589f381383
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
@@ -0,0 +1,156 @@
+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 };
+let lastValidGridSize = 0;
+
+function ResponsiveGrid({ size, isViewMode, layout, onLayoutChange, children }) {
+ // This is to prevent a bug where view mode changes when the panel is expanded. View mode changes will trigger
+ // the grid to re-render, but when a panel is expanded, the size will be 0. Minimizing the panel won't cause the
+ // grid to re-render so it'll show a grid with a width of 0.
+ lastValidGridSize = size.width > 0 ? size.width : lastValidGridSize;
+ // We can't take advantage of isDraggable or isResizable due to performance concerns:
+ // https://github.com/STRML/react-grid-layout/issues/240
+ 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);
+ // A mapping of panelIndexes to grid items so we can set the zIndex appropriately on the last focused
+ // item.
+ this.gridItems = {};
+ this.state = {
+ layout: this.buildLayoutFromPanels()
+ };
+ }
+
+ buildLayoutFromPanels() {
+ return this.props.panels.map(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,
+ };
+ containerApi.updatePanel(panelUpdated.panelIndex, panelUpdated);
+ });
+ };
+
+ onPanelFocused = panelIndex => {
+ this.gridItems[panelIndex].style.zIndex = '1';
+ };
+ onPanelBlurred = panelIndex => {
+ this.gridItems[panelIndex].style.zIndex = 'auto';
+ };
+
+ 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);
+ 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 panelsInOrder.map(panel => {
+ return (
+