From 6165bb344e0a8773e9eb54c1e852b7e05fe1b7c8 Mon Sep 17 00:00:00 2001
From: Lorenzo Natali <offtherailz@gmail.com>
Date: Tue, 15 May 2018 11:01:21 +0200
Subject: [PATCH] Fix #2663. Add edit and view mode to the dashboard (#2901)

---
 web/client/components/dashboard/Dashboard.jsx |   4 +-
 .../components/widgets/view/WidgetsView.jsx   |   6 +-
 .../components/widgets/widget/ChartWidget.jsx |   8 +-
 .../widgets/widget/CounterWidget.jsx          |   7 +-
 .../widgets/widget/LegendWidget.jsx           |   5 +-
 .../components/widgets/widget/MapWidget.jsx   |   5 +-
 .../components/widgets/widget/TableWidget.jsx |   5 +-
 .../components/widgets/widget/TextWidget.jsx  |   5 +-
 .../widget/__tests__/ChartWidget-test.jsx     |   8 +
 .../widget/__tests__/CounterWidget-test.jsx   |   8 +
 .../widget/__tests__/LegendWidget-test.jsx    |   8 +
 .../widget/__tests__/MapWidget-test.jsx       |  13 +-
 .../widget/__tests__/TableWidget-test.jsx     |   8 +
 .../widget/__tests__/TextWidget-test.jsx      |  57 +++++++
 web/client/epics/dashboard.js                 |  17 +-
 web/client/plugins/Dashboard.jsx              |  36 +++--
 web/client/plugins/DashboardEditor.jsx        | 153 ++++++++++--------
 17 files changed, 239 insertions(+), 114 deletions(-)
 create mode 100644 web/client/components/widgets/widget/__tests__/TextWidget-test.jsx

diff --git a/web/client/components/dashboard/Dashboard.jsx b/web/client/components/dashboard/Dashboard.jsx
index 5494d4fca9..e8c9805ceb 100644
--- a/web/client/components/dashboard/Dashboard.jsx
+++ b/web/client/components/dashboard/Dashboard.jsx
@@ -22,9 +22,9 @@ module.exports =
         widthProvider({ overrideWidthProvider: true}),
         emptyState(
             ({widgets = []} = {}) => widgets.length === 0,
-            () => ({
+            ({loading}) => ({
                 glyph: "dashboard",
-                title: <Message msgId="dashboard.emptyTitle" />
+                title: loading ? <Message msgId="loading" /> : <Message msgId="dashboard.emptyTitle" />
             })
         ),
         defaultProps({
diff --git a/web/client/components/widgets/view/WidgetsView.jsx b/web/client/components/widgets/view/WidgetsView.jsx
index db96f8d009..8847c82eec 100644
--- a/web/client/components/widgets/view/WidgetsView.jsx
+++ b/web/client/components/widgets/view/WidgetsView.jsx
@@ -40,6 +40,7 @@ module.exports = pure(({
     width,
     showGroupColor,
     groups = [],
+    canEdit = true,
     getWidgetClass = () => { },
     onWidgetClick = () => { },
     updateWidgetProperty = () => { },
@@ -49,10 +50,12 @@ module.exports = pure(({
     ...actions
 } = {}) =>
     (<ResponsiveReactGridLayout
-        key={id}
+        key={id || "widgets-view"}
         useDefaultWidthProvider={useDefaultWidthProvider}
         measureBeforeMount={measureBeforeMount}
         width={!useDefaultWidthProvider ? width : undefined}
+        isResizable={canEdit}
+        isDraggable={canEdit}
         draggableHandle={".draggableHandle"}
         onLayoutChange={onLayoutChange}
         preventCollision
@@ -72,6 +75,7 @@ module.exports = pure(({
                 groups={getWidgetGroups(groups, w)}
                 showGroupColor={showGroupColor}
                 dependencies={dependencies}
+                canEdit={canEdit}
                 updateProperty={(...args) => updateWidgetProperty(w.id, ...args)}
                 onDelete={() => deleteWidget(w)}
                 onEdit={() => editWidget(w)} /></div>))
diff --git a/web/client/components/widgets/widget/ChartWidget.jsx b/web/client/components/widgets/widget/ChartWidget.jsx
index 2924101213..09886ae6fd 100644
--- a/web/client/components/widgets/widget/ChartWidget.jsx
+++ b/web/client/components/widgets/widget/ChartWidget.jsx
@@ -36,6 +36,7 @@ module.exports = ({
     series = [],
     loading,
     showTable,
+    canEdit = true,
     confirmDelete= false,
     toggleTableView= () => {},
     toggleDeleteConfirm= () => {},
@@ -56,8 +57,11 @@ module.exports = ({
             ? null : <ButtonToolbar>
             <DropdownButton pullRight bsStyle="default" className="widget-menu" title={<Glyphicon glyph="option-vertical" />} noCaret id="dropdown-no-caret">
                 <MenuItem onClick={() => toggleTableView()} eventKey="1"><Glyphicon glyph="features-grid"/>&nbsp;<Message msgId="widgets.widget.menu.showChartData" /></MenuItem>
-                <MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil"/>&nbsp;<Message msgId="widgets.widget.menu.edit" /></MenuItem>
-                <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/>&nbsp;<Message msgId="widgets.widget.menu.delete" /></MenuItem>
+                {canEdit
+                    ? [
+                        <MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil"/>&nbsp;<Message msgId="widgets.widget.menu.edit" /></MenuItem>,
+                        <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/>&nbsp;<Message msgId="widgets.widget.menu.delete" /></MenuItem>]
+                    : null}
                 <MenuItem onClick={() => exportCSV({data, title})} eventKey="4"><Glyphicon className="exportCSV" glyph="download"/>&nbsp;<Message msgId="widgets.widget.menu.downloadData" /></MenuItem>
                 <MenuItem onClick={() => exportImage({widgetDivId: `widget-chart-${id}`, title})} eventKey="4"><Glyphicon className="exportImage" glyph="download"/>&nbsp;<Message msgId="widgets.widget.menu.exportImage" /></MenuItem>
             </DropdownButton>
diff --git a/web/client/components/widgets/widget/CounterWidget.jsx b/web/client/components/widgets/widget/CounterWidget.jsx
index c21c127a66..766d752461 100644
--- a/web/client/components/widgets/widget/CounterWidget.jsx
+++ b/web/client/components/widgets/widget/CounterWidget.jsx
@@ -36,6 +36,7 @@ module.exports = ({
     showTable,
     confirmDelete= false,
     headerStyle,
+    canEdit = true,
     toggleTableView= () => {},
     toggleDeleteConfirm= () => {},
     onEdit= () => {},
@@ -50,12 +51,12 @@ module.exports = ({
         onDelete={onDelete}
         toggleDeleteConfirm = {toggleDeleteConfirm}
         headerStyle={headerStyle}
-        topRightItems={showTable
-            ? null : <ButtonToolbar>
+        topRightItems={canEdit
+            ? (<ButtonToolbar>
             <DropdownButton pullRight bsStyle="default" className="widget-menu" title={<Glyphicon glyph="option-vertical" />} noCaret id="dropdown-no-caret">
                 <MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil"/>&nbsp;<Message msgId="widgets.widget.menu.edit" /></MenuItem>
                 <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/>&nbsp;<Message msgId="widgets.widget.menu.delete" /></MenuItem>
             </DropdownButton>
-        </ButtonToolbar>}>
+        </ButtonToolbar>) : null}>
         <CounterView id={id} isAnimationActive={!loading} loading={loading} data={data} series={series} iconFit {...props} />
     </WidgetContainer>);
diff --git a/web/client/components/widgets/widget/LegendWidget.jsx b/web/client/components/widgets/widget/LegendWidget.jsx
index ead43ffce0..063ecf3906 100644
--- a/web/client/components/widgets/widget/LegendWidget.jsx
+++ b/web/client/components/widgets/widget/LegendWidget.jsx
@@ -28,6 +28,7 @@ module.exports = ({
     id, title,
     headerStyle,
     confirmDelete= false,
+    canEdit = true,
     onDelete=() => {},
     loading,
     description,
@@ -36,12 +37,12 @@ module.exports = ({
     (<WidgetContainer id={`widget-text-${id}`} title={title} confirmDelete={confirmDelete} onDelete={onDelete} toggleDeleteConfirm={toggleDeleteConfirm} headerStyle={headerStyle}
     topLeftItems={renderHeaderLeftTopItem({ loading, title, description })}
 
-    topRightItems={<ButtonToolbar>
+    topRightItems={canEdit ? (<ButtonToolbar>
         <DropdownButton pullRight bsStyle="default" className="widget-menu" title={<Glyphicon glyph="option-vertical" />} noCaret id="dropdown-no-caret">
             <MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil"/>&nbsp;<Message msgId="widgets.widget.menu.edit" /></MenuItem>
             <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/>&nbsp;<Message msgId="widgets.widget.menu.delete" /></MenuItem>
         </DropdownButton>
-    </ButtonToolbar>}
+    </ButtonToolbar>) : null}
         >
         <LegendView {...props} />
     </WidgetContainer>
diff --git a/web/client/components/widgets/widget/MapWidget.jsx b/web/client/components/widgets/widget/MapWidget.jsx
index fb326d79f7..a31122e937 100644
--- a/web/client/components/widgets/widget/MapWidget.jsx
+++ b/web/client/components/widgets/widget/MapWidget.jsx
@@ -34,18 +34,19 @@ module.exports = ({
     id, title, loading, description,
     map,
     mapStateSource,
+    canEdit = true,
     confirmDelete = false,
     onDelete = () => {},
     headerStyle
 } = {}) =>
     (<WidgetContainer id={`widget-text-${id}`} title={title} confirmDelete={confirmDelete} onDelete={onDelete} toggleDeleteConfirm={toggleDeleteConfirm} headerStyle={headerStyle}
         topLeftItems={renderHeaderLeftTopItem({ loading, title, description })}
-        topRightItems={<ButtonToolbar>
+        topRightItems={canEdit ? (<ButtonToolbar>
             <DropdownButton pullRight bsStyle="default" className="widget-menu" title={<Glyphicon glyph="option-vertical" />} noCaret id="dropdown-no-caret">
                 <MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil" />&nbsp;<Message msgId="widgets.widget.menu.edit" /></MenuItem>
                 <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash" />&nbsp;<Message msgId="widgets.widget.menu.delete" /></MenuItem>
             </DropdownButton>
-        </ButtonToolbar>}
+        </ButtonToolbar>) : null}
     >
         <MapView updateProperty={updateProperty} id={id} map={omit(map, 'mapStateSource')} mapStateSource={mapStateSource} layers={map && map.layers} options={{ style: { margin: 10, height: 'calc(100% - 20px)' }}}/>
     </WidgetContainer>);
diff --git a/web/client/components/widgets/widget/TableWidget.jsx b/web/client/components/widgets/widget/TableWidget.jsx
index 83a7e69572..3258dddaa5 100644
--- a/web/client/components/widgets/widget/TableWidget.jsx
+++ b/web/client/components/widgets/widget/TableWidget.jsx
@@ -36,6 +36,7 @@ module.exports = ({
     loading,
     confirmDelete = false,
     headerStyle,
+    canEdit = true,
     toggleTableView = () => { },
     toggleDeleteConfirm = () => { },
     onEdit = () => { },
@@ -62,10 +63,10 @@ module.exports = ({
         onDelete={onDelete}
         toggleDeleteConfirm={toggleDeleteConfirm}
         topRightItems={<ButtonToolbar>
-            <DropdownButton pullRight bsStyle="default" className="widget-menu" title={<Glyphicon glyph="option-vertical" />} noCaret id="dropdown-no-caret">
+            {canEdit ? (<DropdownButton pullRight bsStyle="default" className="widget-menu" title={<Glyphicon glyph="option-vertical" />} noCaret id="dropdown-no-caret">
                 <MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil" />&nbsp;<Message msgId="widgets.widget.menu.edit" /></MenuItem>
                 <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash" />&nbsp;<Message msgId="widgets.widget.menu.delete" /></MenuItem>
-            </DropdownButton>
+            </DropdownButton>) : null}
         </ButtonToolbar>}>
         <BorderLayout
             footer={pagination.totalFeatures ? (
diff --git a/web/client/components/widgets/widget/TextWidget.jsx b/web/client/components/widgets/widget/TextWidget.jsx
index 7dbacccb4c..a5c814b4e6 100644
--- a/web/client/components/widgets/widget/TextWidget.jsx
+++ b/web/client/components/widgets/widget/TextWidget.jsx
@@ -22,15 +22,16 @@ module.exports = ({
     toggleDeleteConfirm = () => {},
     id, title, text,
     headerStyle,
+    canEdit = true,
     confirmDelete= false,
     onDelete=() => {}
 } = {}) =>
     (<WidgetContainer id={`widget-text-${id}`} title={title} confirmDelete={confirmDelete} onDelete={onDelete} toggleDeleteConfirm={toggleDeleteConfirm} headerStyle={headerStyle}
     topRightItems={<ButtonToolbar>
-        <DropdownButton pullRight bsStyle="default" className="widget-menu" title={<Glyphicon glyph="option-vertical" />} noCaret id="dropdown-no-caret">
+        {canEdit ? (<DropdownButton pullRight bsStyle="default" className="widget-menu" title={<Glyphicon glyph="option-vertical" />} noCaret id="dropdown-no-caret">
             <MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil"/>&nbsp;<Message msgId="widgets.widget.menu.edit" /></MenuItem>
             <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/>&nbsp;<Message msgId="widgets.widget.menu.delete" /></MenuItem>
-        </DropdownButton>
+        </DropdownButton>) : null}
     </ButtonToolbar>}
         >
     <TextView text={text} />
diff --git a/web/client/components/widgets/widget/__tests__/ChartWidget-test.jsx b/web/client/components/widgets/widget/__tests__/ChartWidget-test.jsx
index d3463108e1..011bc1fca3 100644
--- a/web/client/components/widgets/widget/__tests__/ChartWidget-test.jsx
+++ b/web/client/components/widgets/widget/__tests__/ChartWidget-test.jsx
@@ -25,8 +25,16 @@ describe('ChartWidget component', () => {
         ReactDOM.render(<ChartWidget />, document.getElementById("container"));
         const container = document.getElementById('container');
         const el = container.querySelector('.mapstore-widget-card');
+        expect(container.querySelector('.glyphicon-pencil')).toExist();
+        expect(container.querySelector('.glyphicon-trash')).toExist();
         expect(el).toExist();
     });
+    it('view only mode', () => {
+        ReactDOM.render(<ChartWidget canEdit={false} />, document.getElementById("container"));
+        const container = document.getElementById('container');
+        expect(container.querySelector('.glyphicon-pencil')).toNotExist();
+        expect(container.querySelector('.glyphicon-trash')).toNotExist();
+    });
     it('Test ChartWidget onEdit callback', () => {
         const actions = {
             onEdit: () => {}
diff --git a/web/client/components/widgets/widget/__tests__/CounterWidget-test.jsx b/web/client/components/widgets/widget/__tests__/CounterWidget-test.jsx
index 1526d7c68a..f21635fb64 100644
--- a/web/client/components/widgets/widget/__tests__/CounterWidget-test.jsx
+++ b/web/client/components/widgets/widget/__tests__/CounterWidget-test.jsx
@@ -25,8 +25,16 @@ describe('CounterWidget component', () => {
         ReactDOM.render(<CounterWidget />, document.getElementById("container"));
         const container = document.getElementById('container');
         const el = container.querySelector('.mapstore-widget-card');
+        expect(container.querySelector('.glyphicon-pencil')).toExist();
+        expect(container.querySelector('.glyphicon-trash')).toExist();
         expect(el).toExist();
     });
+    it('view only mode', () => {
+        ReactDOM.render(<CounterWidget canEdit={false} />, document.getElementById("container"));
+        const container = document.getElementById('container');
+        expect(container.querySelector('.glyphicon-pencil')).toNotExist();
+        expect(container.querySelector('.glyphicon-trash')).toNotExist();
+    });
     it('Test CounterWidget onEdit callback', () => {
         const actions = {
             onEdit: () => {}
diff --git a/web/client/components/widgets/widget/__tests__/LegendWidget-test.jsx b/web/client/components/widgets/widget/__tests__/LegendWidget-test.jsx
index 246b3cb721..4b2e46950f 100644
--- a/web/client/components/widgets/widget/__tests__/LegendWidget-test.jsx
+++ b/web/client/components/widgets/widget/__tests__/LegendWidget-test.jsx
@@ -26,6 +26,14 @@ describe('LegendWidget component', () => {
         const container = document.getElementById('container');
         const el = container.querySelector('.mapstore-widget-card');
         expect(el).toExist();
+        expect(container.querySelector('.glyphicon-pencil')).toExist();
+        expect(container.querySelector('.glyphicon-trash')).toExist();
+    });
+    it('view only mode', () => {
+        ReactDOM.render(<LegendWidget canEdit={false} />, document.getElementById("container"));
+        const container = document.getElementById('container');
+        expect(container.querySelector('.glyphicon-pencil')).toNotExist();
+        expect(container.querySelector('.glyphicon-trash')).toNotExist();
     });
     it('Test LegendWidget onEdit callback', () => {
         const actions = {
diff --git a/web/client/components/widgets/widget/__tests__/MapWidget-test.jsx b/web/client/components/widgets/widget/__tests__/MapWidget-test.jsx
index eb5c1d9fc6..12afc5a0ae 100644
--- a/web/client/components/widgets/widget/__tests__/MapWidget-test.jsx
+++ b/web/client/components/widgets/widget/__tests__/MapWidget-test.jsx
@@ -21,8 +21,15 @@ describe('MapWidget component', () => {
         setTimeout(done);
     });
     it('MapWidget rendering with defaults', () => {
-        ReactDOM.render(<Provider store={{subscribe: () => {}, getState: () => ({maptype: {mapType: 'openlayers'}})}} ><MapWidget /></Provider>, document.getElementById("container"));
-        const el = document.querySelector('div');
-        expect(el).toExist();
+        ReactDOM.render(<Provider store={{subscribe: () => {}, getState: () => ({maptype: {mapType: 'openlayers'}})}} ><MapWidget map={{layers: []}}/></Provider>, document.getElementById("container"));
+        const container = document.getElementById('container');
+        expect(container.querySelector('.glyphicon-pencil')).toExist();
+        expect(container.querySelector('.glyphicon-trash')).toExist();
+    });
+    it('view only mode', () => {
+        ReactDOM.render(<Provider store={{ subscribe: () => { }, getState: () => ({ maptype: { mapType: 'openlayers' } }) }} ><MapWidget map={{ layers: [] }} canEdit={false}/></Provider>, document.getElementById("container"));
+        const container = document.getElementById('container');
+        expect(container.querySelector('.glyphicon-pencil')).toNotExist();
+        expect(container.querySelector('.glyphicon-trash')).toNotExist();
     });
 });
diff --git a/web/client/components/widgets/widget/__tests__/TableWidget-test.jsx b/web/client/components/widgets/widget/__tests__/TableWidget-test.jsx
index 2117755cc6..415bf05062 100644
--- a/web/client/components/widgets/widget/__tests__/TableWidget-test.jsx
+++ b/web/client/components/widgets/widget/__tests__/TableWidget-test.jsx
@@ -36,6 +36,14 @@ describe('TableWidget component', () => {
         const container = document.getElementById('container');
         const el = container.querySelector('.mapstore-widget-card');
         expect(el).toExist();
+        expect(container.querySelector('.glyphicon-pencil')).toExist();
+        expect(container.querySelector('.glyphicon-trash')).toExist();
+    });
+    it('view only mode', () => {
+        ReactDOM.render(<TableWidget canEdit={false} />, document.getElementById("container"));
+        const container = document.getElementById('container');
+        expect(container.querySelector('.glyphicon-pencil')).toNotExist();
+        expect(container.querySelector('.glyphicon-trash')).toNotExist();
     });
     it('Test TableWidget onEdit callback', () => {
         const actions = {
diff --git a/web/client/components/widgets/widget/__tests__/TextWidget-test.jsx b/web/client/components/widgets/widget/__tests__/TextWidget-test.jsx
new file mode 100644
index 0000000000..40c20475c5
--- /dev/null
+++ b/web/client/components/widgets/widget/__tests__/TextWidget-test.jsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const React = require('react');
+const ReactDOM = require('react-dom');
+const ReactTestUtils = require('react-dom/test-utils');
+
+const expect = require('expect');
+const TextWidget = require('../TextWidget');
+
+describe('TextWidget component', () => {
+    beforeEach((done) => {
+        document.body.innerHTML = '<div id="container"></div>';
+        setTimeout(done);
+    });
+    afterEach((done) => {
+        ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+        document.body.innerHTML = '';
+        setTimeout(done);
+    });
+    it('rendering with defaults', () => {
+        ReactDOM.render(<TextWidget />, document.getElementById("container"));
+        const container = document.getElementById('container');
+        const el = container.querySelector('.mapstore-widget-card');
+        expect(container.querySelector('.glyphicon-pencil')).toExist();
+        expect(container.querySelector('.glyphicon-trash')).toExist();
+        expect(el).toExist();
+    });
+    it('view only mode', () => {
+        ReactDOM.render(<TextWidget canEdit={false} />, document.getElementById("container"));
+        const container = document.getElementById('container');
+        expect(container.querySelector('.glyphicon-pencil')).toNotExist();
+        expect(container.querySelector('.glyphicon-trash')).toNotExist();
+    });
+    it('onEdit callback', () => {
+        const actions = {
+            onEdit: () => {}
+        };
+        const spyonEdit = expect.spyOn(actions, 'onEdit');
+        ReactDOM.render(<TextWidget onEdit={actions.onEdit} />, document.getElementById("container"));
+        const container = document.getElementById('container');
+        const el = container.querySelector('.glyphicon-pencil');
+        ReactTestUtils.Simulate.click(el); // <-- trigger event callback
+        expect(spyonEdit).toHaveBeenCalled();
+    });
+    it('rendering text', () => {
+        const TEST_TEXT = '<div id="TEST_TEXT"> TEST </div>';
+        ReactDOM.render(<TextWidget text={TEST_TEXT} />, document.getElementById("container"));
+        const container = document.getElementById('container');
+        expect(container.querySelector('#TEST_TEXT')).toExist();
+    });
+});
diff --git a/web/client/epics/dashboard.js b/web/client/epics/dashboard.js
index d8da36ad1f..e49ad82372 100644
--- a/web/client/epics/dashboard.js
+++ b/web/client/epics/dashboard.js
@@ -37,7 +37,8 @@ const {
     QUERY_FORM_SEARCH
 } = require('../actions/queryform');
 const {
-    LOGIN_SUCCESS
+    LOGIN_SUCCESS,
+    LOGOUT
 } = require('../actions/security');
 const {
     isDashboardEditing,
@@ -153,12 +154,7 @@ module.exports = {
                             return Rx.Observable.of(error({
                                 title: "dashboard.errors.loading.title",
                                 message: "dashboard.errors.loading.pleaseLogin"
-                            }))
-                            .merge(action$
-                                .ofType(LOGIN_SUCCESS)
-                                .switchMap( () => Rx.Observable.of(loadDashboard(id)).delay(1000))
-                                .filter(() => isDashboardAvailable(getState()))
-                                .takeUntil(action$.ofType(LOCATION_CHANGE)));
+                            }));
                         } if (e.status === 404) {
                             return Rx.Observable.of(error({
                                 title: "dashboard.errors.loading.title",
@@ -172,6 +168,13 @@ module.exports = {
                     }
                 ))
         ),
+    reloadDashboardOnLoginLogout: (action$) =>
+        action$.ofType(LOAD_DASHBOARD).switchMap(
+            ({ id }) => action$
+                .ofType(LOGIN_SUCCESS, LOGOUT)
+                .switchMap(() => Rx.Observable.of(loadDashboard(id)).delay(1000))
+                .takeUntil(action$.ofType(LOCATION_CHANGE))
+        ),
     // saving dashboard flow (both creation and update)
     saveDashboard: action$ => action$
         .ofType(SAVE_DASHBOARD)
diff --git a/web/client/plugins/Dashboard.jsx b/web/client/plugins/Dashboard.jsx
index 2ae0be8a7a..6949d8b7f4 100644
--- a/web/client/plugins/Dashboard.jsx
+++ b/web/client/plugins/Dashboard.jsx
@@ -7,21 +7,20 @@
  */
 
 const React = require('react');
-const {get} = require('lodash');
-const {connect} = require('react-redux');
+const { get } = require('lodash');
+const { connect } = require('react-redux');
 const { compose, withProps, withHandlers } = require('recompose');
-const {createSelector} = require('reselect');
-const {mapIdSelector} = require('../selectors/map');
-const { getDashboardWidgets, dependenciesSelector, getDashboardWidgetsLayout, isWidgetSelectionActive, getEditingWidget, getWidgetsDependenciesGroups} = require('../selectors/widgets');
-const { editWidget, updateWidgetProperty, deleteWidget, changeLayout, exportCSV, exportImage, selectWidget} = require('../actions/widgets');
-const {showConnectionsSelector} = require('../selectors/dashboard');
+const { createSelector } = require('reselect');
+const { getDashboardWidgets, dependenciesSelector, getDashboardWidgetsLayout, isWidgetSelectionActive, getEditingWidget, getWidgetsDependenciesGroups } = require('../selectors/widgets');
+const { editWidget, updateWidgetProperty, deleteWidget, changeLayout, exportCSV, exportImage, selectWidget } = require('../actions/widgets');
+const { showConnectionsSelector, dashboardResource, isDashboardLoading } = require('../selectors/dashboard');
 const ContainerDimensions = require('react-container-dimensions').default;
 
 const PropTypes = require('prop-types');
 const WidgetsView = compose(
     connect(
         createSelector(
-            mapIdSelector,
+            dashboardResource,
             getDashboardWidgets,
             getDashboardWidgetsLayout,
             dependenciesSelector,
@@ -29,8 +28,11 @@ const WidgetsView = compose(
             (state) => get(getEditingWidget(state), "id"),
             getWidgetsDependenciesGroups,
             showConnectionsSelector,
-            (id, widgets, layouts, dependencies, selectionActive, editingWidgetId, groups, showGroupColor) => ({
-                id,
+            isDashboardLoading,
+            (resource, widgets, layouts, dependencies, selectionActive, editingWidgetId, groups, showGroupColor, loading) => ({
+                resource,
+                loading,
+                canEdit: (resource ? !!resource.canEdit : true),
                 widgets,
                 layouts,
                 dependencies,
@@ -63,14 +65,14 @@ const WidgetsView = compose(
 
 
 class Widgets extends React.Component {
-     static propTypes = {
-         enabled: PropTypes.bool
-     };
-     static defaultProps = {
-         enabled: true
-     };
+    static propTypes = {
+        enabled: PropTypes.bool
+    };
+    static defaultProps = {
+        enabled: true
+    };
     render() {
-        return this.props.enabled ? (<ContainerDimensions>{({width, height}) => <WidgetsView width={width} height={height}/>}</ContainerDimensions> ) : null;
+        return this.props.enabled ? (<ContainerDimensions>{({ width, height }) => <WidgetsView width={width} height={height} />}</ContainerDimensions>) : null;
 
     }
 }
diff --git a/web/client/plugins/DashboardEditor.jsx b/web/client/plugins/DashboardEditor.jsx
index f2cdb5996f..04903396dc 100644
--- a/web/client/plugins/DashboardEditor.jsx
+++ b/web/client/plugins/DashboardEditor.jsx
@@ -7,26 +7,27 @@
  */
 
 const React = require('react');
-const {withProps, compose} = require('recompose');
-const {createSelector} = require('reselect');
-const {connect} = require('react-redux');
+const { withProps, compose } = require('recompose');
+const { createSelector } = require('reselect');
+const { connect } = require('react-redux');
 const PropTypes = require('prop-types');
 
-const { isDashboardEditing} = require('../selectors/dashboard');
+const { isDashboardEditing } = require('../selectors/dashboard');
 const { isLoggedIn } = require('../selectors/security');
-const { dashboardHasWidgets } = require('../selectors/widgets');
-const { showConnectionsSelector, dashboardResource } = require('../selectors/dashboard');
-const {dashboardSelector} = require('./widgetbuilder/commons');
+const { dashboardHasWidgets, getWidgetsDependenciesGroups } = require('../selectors/widgets');
+const { showConnectionsSelector, dashboardResource, isDashboardLoading } = require('../selectors/dashboard');
+const { dashboardSelector } = require('./widgetbuilder/commons');
 
 const { createWidget, toggleConnection } = require('../actions/widgets');
 const { triggerShowConnections, triggerSave } = require('../actions/dashboard');
 
 const withDashboardExitButton = require('./widgetbuilder/enhancers/withDashboardExitButton');
+const LoadingSpinner = require('../components/misc/LoadingSpinner');
 
 const Builder =
     compose(
-        connect(dashboardSelector, { toggleConnection, triggerShowConnections}),
-        withProps(({ availableDependencies = []}) => ({
+        connect(dashboardSelector, { toggleConnection, triggerShowConnections }),
+        withProps(({ availableDependencies = [] }) => ({
             availableDependencies: availableDependencies.filter(d => d !== "map")
         })),
         withDashboardExitButton
@@ -39,11 +40,14 @@ const Toolbar = compose(
             isLoggedIn,
             dashboardResource,
             dashboardHasWidgets,
-            (showConnections, logged, resource, hasWidgets) => ({
+            getWidgetsDependenciesGroups,
+            (showConnections, logged, resource, hasWidgets, groups = []) => ({
                 showConnections,
+                hasConnections: groups.length > 0,
                 hasWidgets,
+                canEdit: (resource ? resource.canEdit : true),
                 canSave: logged && hasWidgets && (resource ? resource.canEdit : true)
-             })
+            })
         ),
         {
             onShowConnections: triggerShowConnections,
@@ -52,69 +56,74 @@ const Toolbar = compose(
         }
     ),
     withProps(({
-        onAddWidget = () => {},
-        onToggleSave = () => {},
+        onAddWidget = () => { },
+        onToggleSave = () => { },
         hasWidgets,
         canSave,
-        showConnections, onShowConnections = () => { }
-        }) => ({
-            buttons: [{
-                    glyph: 'plus',
-                    tooltipId: 'dashboard.editor.addACardToTheDashboard',
-                    bsStyle: 'primary',
-                    visible: true,
-                    onClick: () => onAddWidget()
-                }, {
-                    glyph: 'floppy-disk',
-                    tooltipId: 'dashboard.editor.save',
-                    bsStyle: 'primary',
-                    tooltipPosition: 'right',
-                    visible: !!canSave,
-                    onClick: () => onToggleSave(true)
-                }, {
-                    glyph: showConnections ? 'bulb-on' : 'bulb-off',
-                    tooltipId: showConnections ? 'dashboard.editor.hideConnections' : 'dashboard.editor.showConnections',
-                    bsStyle: showConnections ? 'success' : 'primary',
-                    visible: !!hasWidgets,
-                    onClick: () => onShowConnections(!showConnections)
-                }]
-        }))
+        canEdit,
+        hasConnections,
+        showConnections,
+        onShowConnections = () => { }
+    }) => ({
+        buttons: [{
+            glyph: 'plus',
+            tooltipId: 'dashboard.editor.addACardToTheDashboard',
+            bsStyle: 'primary',
+            visible: canEdit,
+            onClick: () => onAddWidget()
+        }, {
+            glyph: 'floppy-disk',
+            tooltipId: 'dashboard.editor.save',
+            bsStyle: 'primary',
+            tooltipPosition: 'right',
+            visible: !!canSave,
+            onClick: () => onToggleSave(true)
+        }, {
+            glyph: showConnections ? 'bulb-on' : 'bulb-off',
+            tooltipId: showConnections ? 'dashboard.editor.hideConnections' : 'dashboard.editor.showConnections',
+            bsStyle: showConnections ? 'success' : 'primary',
+            visible: !!hasWidgets && !!hasConnections,
+            onClick: () => onShowConnections(!showConnections)
+        }]
+    }))
 )(require('../components/misc/toolbar/Toolbar'));
 
 const SaveDialog = require('./dashboard/SaveDialog');
 
-const {setEditing, setEditorAvailable} = require('../actions/dashboard');
+const { setEditing, setEditorAvailable } = require('../actions/dashboard');
 
 
 class DashboardEditorComponent extends React.Component {
-     static propTypes = {
-         id: PropTypes.string,
-         editing: PropTypes.bool,
-         limitDockHeight: PropTypes.bool,
-         fluid: PropTypes.bool,
-         zIndex: PropTypes.number,
-         dockSize: PropTypes.number,
-         position: PropTypes.string,
-         onMount: PropTypes.func,
-         onUnmount: PropTypes.func,
-         setEditing: PropTypes.func,
-         dimMode: PropTypes.string,
-         src: PropTypes.string,
-         style: PropTypes.object
-     };
-     static defaultProps = {
-         id: "dashboard-editor",
-         editing: false,
-         dockSize: 500,
-         limitDockHeight: true,
-         zIndex: 10000,
-         fluid: false,
-         dimMode: "none",
-         position: "left",
-         onMount: () => {},
-         onUnmount: () => {},
-         setEditing: () => {}
-     };
+    static propTypes = {
+        id: PropTypes.string,
+        editing: PropTypes.bool,
+        loading: PropTypes.bool,
+        limitDockHeight: PropTypes.bool,
+        fluid: PropTypes.bool,
+        zIndex: PropTypes.number,
+        dockSize: PropTypes.number,
+        position: PropTypes.string,
+        onMount: PropTypes.func,
+        onUnmount: PropTypes.func,
+        setEditing: PropTypes.func,
+        dimMode: PropTypes.string,
+        src: PropTypes.string,
+        style: PropTypes.object
+    };
+    static defaultProps = {
+        id: "dashboard-editor",
+        editing: false,
+        dockSize: 500,
+        loading: true,
+        limitDockHeight: true,
+        zIndex: 10000,
+        fluid: false,
+        dimMode: "none",
+        position: "left",
+        onMount: () => { },
+        onUnmount: () => { },
+        setEditing: () => { }
+    };
     componentDidMount() {
         this.props.onMount();
     }
@@ -124,18 +133,20 @@ class DashboardEditorComponent extends React.Component {
     }
     render() {
         return this.props.editing
-                ? <div className="dashboard-editor de-builder"><Builder enabled={this.props.editing} onClose={() => this.props.setEditing(false)} catalog={this.props.catalog}/></div>
-                : (<div className="ms-vertical-toolbar dashboard-editor de-toolbar" id={this.props.id}>
-                    <SaveDialog />
-                    <Toolbar transitionProps={false} btnGroupProps={{vertical: true}} btnDefaultProps={{ tooltipPosition: 'right', className: 'square-button-md', bsStyle: 'primary'}} />
-                    </div>);
+            ? <div className="dashboard-editor de-builder"><Builder enabled={this.props.editing} onClose={() => this.props.setEditing(false)} catalog={this.props.catalog} /></div>
+            : (<div className="ms-vertical-toolbar dashboard-editor de-toolbar" id={this.props.id}>
+                <SaveDialog />
+                <Toolbar transitionProps={false} btnGroupProps={{ vertical: true }} btnDefaultProps={{ tooltipPosition: 'right', className: 'square-button-md', bsStyle: 'primary' }} />
+                {this.props.loading ? <LoadingSpinner style={{ position: 'fixed', bottom: 0}} /> : null}
+            </div>);
     }
 }
 
 const Plugin = connect(
     createSelector(
         isDashboardEditing,
-        (editing) => ({ editing }),
+        isDashboardLoading,
+        (editing, loading) => ({ editing, loading }),
     ), {
         setEditing,
         onMount: () => setEditorAvailable(true),