-
-
-
+
+
diff --git a/src/core_plugins/kibana/public/dashboard/dashboard.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js
similarity index 84%
rename from src/core_plugins/kibana/public/dashboard/dashboard.js
rename to src/core_plugins/kibana/public/dashboard/dashboard_app.js
index 0d52e6cae2165..7b2e5e8775411 100644
--- a/src/core_plugins/kibana/public/dashboard/dashboard.js
+++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js
@@ -1,11 +1,10 @@
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 'plugins/kibana/dashboard/grid';
-import 'plugins/kibana/dashboard/panel/panel';
import 'ui/query_bar';
import { SavedObjectNotFound } from 'ui/errors';
@@ -13,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,52 +27,23 @@ 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 { DashboardViewport } from './viewport/dashboard_viewport';
const app = uiModules.get('app/dashboard', [
'elasticsearch',
'ngRoute',
+ 'react',
'kibana/courier',
'kibana/config',
'kibana/notify',
'kibana/typeahead',
]);
-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('dashboardViewport', function (reactDirective) {
+ return reactDirective(DashboardViewport);
+});
app.directive('dashboardApp', function ($injector) {
const Notifier = $injector.get('Notifier');
@@ -95,21 +65,26 @@ 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) {
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;
// The 'previouslyStored' check is so we only update the time filter on dashboard open, not during
// normal cross app navigation.
@@ -136,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();
});
@@ -181,13 +157,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) {
@@ -282,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_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_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 79%
rename from src/core_plugins/kibana/public/dashboard/dashboard_state.js
rename to src/core_plugins/kibana/public/dashboard/dashboard_state_manager.js
index e8e90a6f71011..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 'plugins/kibana/dashboard/panel/panel_state';
-
-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 { createPanelState, getPersistedStateId } from './panel';
+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));
+ 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.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..7f6341e657125
--- /dev/null
+++ b/src/core_plugins/kibana/public/dashboard/grid/dashboard_grid.js
@@ -0,0 +1,122 @@
+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 { DashboardPanelContainer } from '../panel/dashboard_panel_container';
+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 {
+ 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 { onPanelUpdated } = this.props;
+ layout.forEach(panelLayout => {
+ const updatedPanel = {
+ panelIndex: panelLayout.i,
+ gridData: {
+ x: panelLayout.x,
+ y: panelLayout.y,
+ w: panelLayout.w,
+ h: panelLayout.h,
+ i: panelLayout.i,
+ },
+ version: panelLayout.version
+ };
+ onPanelUpdated(updatedPanel);
+ });
+ };
+
+ renderDOM() {
+ const {
+ panels,
+ getEmbeddableHandler,
+ getContainerApi,
+ } = this.props;
+
+ // Part of our unofficial API - need to render in a consistent order for plugins.
+ const panelsInOrder = Object.values(panels);
+ 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 (
+