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"/> <Message msgId="widgets.widget.menu.showChartData" /></MenuItem> - <MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil"/> <Message msgId="widgets.widget.menu.edit" /></MenuItem> - <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/> <Message msgId="widgets.widget.menu.delete" /></MenuItem> + {canEdit + ? [ + <MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil"/> <Message msgId="widgets.widget.menu.edit" /></MenuItem>, + <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/> <Message msgId="widgets.widget.menu.delete" /></MenuItem>] + : null} <MenuItem onClick={() => exportCSV({data, title})} eventKey="4"><Glyphicon className="exportCSV" glyph="download"/> <Message msgId="widgets.widget.menu.downloadData" /></MenuItem> <MenuItem onClick={() => exportImage({widgetDivId: `widget-chart-${id}`, title})} eventKey="4"><Glyphicon className="exportImage" glyph="download"/> <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"/> <Message msgId="widgets.widget.menu.edit" /></MenuItem> <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/> <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"/> <Message msgId="widgets.widget.menu.edit" /></MenuItem> <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/> <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" /> <Message msgId="widgets.widget.menu.edit" /></MenuItem> <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash" /> <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" /> <Message msgId="widgets.widget.menu.edit" /></MenuItem> <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash" /> <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"/> <Message msgId="widgets.widget.menu.edit" /></MenuItem> <MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/> <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),