diff --git a/web/client/actions/__tests__/details-test.js b/web/client/actions/__tests__/details-test.js
index a0474fa074..254b876ada 100644
--- a/web/client/actions/__tests__/details-test.js
+++ b/web/client/actions/__tests__/details-test.js
@@ -28,13 +28,21 @@ describe('details actions tests', () => {
const a = closeDetailsPanel();
expect(a.type).toBe(CLOSE_DETAILS_PANEL);
});
- it('detailsLoaded', () => {
+ it('detailsLoaded for map', () => {
const mapId = 1;
const detailsUri = "sada/da/";
const a = detailsLoaded(mapId, detailsUri);
expect(a.type).toBe(DETAILS_LOADED);
expect(a.detailsUri).toBe(detailsUri);
- expect(a.mapId).toBe(mapId);
+ expect(a.id).toBe(mapId);
+ });
+ it('detailsLoaded for dashboard', () => {
+ const dashboardId = 1;
+ const detailsUri = "sada/da/";
+ const a = detailsLoaded(dashboardId, detailsUri);
+ expect(a.type).toBe(DETAILS_LOADED);
+ expect(a.detailsUri).toBe(detailsUri);
+ expect(a.id).toBe(dashboardId);
});
it('updateDetails', () => {
const a = updateDetails('text');
diff --git a/web/client/actions/details.js b/web/client/actions/details.js
index a5f4c288da..83de607bb7 100644
--- a/web/client/actions/details.js
+++ b/web/client/actions/details.js
@@ -17,9 +17,9 @@ export const NO_DETAILS_AVAILABLE = "NO_DETAILS_AVAILABLE";
* @memberof actions.details
* @return {action} type `UPDATE_DETAILS`
*/
-export const updateDetails = (detailsText) => ({
+export const updateDetails = (detailsText, resourceId) => ({
type: UPDATE_DETAILS,
- detailsText
+ detailsText, id: resourceId
});
/**
@@ -27,9 +27,9 @@ export const updateDetails = (detailsText) => ({
* @memberof actions.details
* @return {action} type `DETAILS_LOADED`
*/
-export const detailsLoaded = (mapId, detailsUri, detailsSettings) => ({
+export const detailsLoaded = (resourceId, detailsUri, detailsSettings) => ({
type: DETAILS_LOADED,
- mapId,
+ id: resourceId,
detailsUri,
detailsSettings
});
diff --git a/web/client/components/details/DetailsPanel.jsx b/web/client/components/details/DetailsPanel.jsx
index 90309fefb7..254d24d707 100644
--- a/web/client/components/details/DetailsPanel.jsx
+++ b/web/client/components/details/DetailsPanel.jsx
@@ -22,7 +22,8 @@ class DetailsPanel extends React.Component {
panelClassName: PropTypes.string,
style: PropTypes.object,
onClose: PropTypes.func,
- width: PropTypes.number
+ width: PropTypes.number,
+ isDashboard: PropTypes.bool
};
static contextTypes = {
@@ -41,14 +42,15 @@ class DetailsPanel extends React.Component {
},
active: false,
panelClassName: "details-panel",
- width: 550
+ width: 550,
+ isDashboard: false
};
render() {
return (
*
*/
-export default ({id, children, header, footer, columns, height, style = {}, className, bodyClassName = "ms2-border-layout-body"}) =>
+export default ({id, children, header, footer, columns, height, style = {}, className, bodyClassName = "ms2-border-layout-body" }) =>
(
{
.withLatestFrom(props$)
.switchMap(([resource, props]) =>
updateResource(resource)
- .flatMap(rid => resource.category === 'MAP' ? updateResourceAttribute({
- id: rid,
- name: 'detailsSettings',
- value: JSON.stringify(resource.attributes?.detailsSettings || {})
- }) : Rx.Observable.of(rid))
+ .flatMap(rid => (['MAP', 'DASHBOARD'].includes(resource.categoryName)) ?
+ updateResourceAttribute({
+ id: rid,
+ name: 'detailsSettings',
+ value: JSON.stringify(resource.attributes?.detailsSettings || {})
+ }) : Rx.Observable.of(rid))
.do(() => {
if (props) {
if (props.onClose) {
diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json
index 2d383ff1cf..18c1ed324a 100644
--- a/web/client/configs/localConfig.json
+++ b/web/client/configs/localConfig.json
@@ -676,7 +676,15 @@
"FeedbackMask"
],
"dashboard": [
- "BurgerMenu",
+ "Details",
+ "AddWidgetDashboard",
+ "MapConnectionDashboard",
+ {
+ "name": "SidebarMenu",
+ "cfg" : {
+ "containerPosition": "columns"
+ }
+ },
{
"name": "Dashboard",
"cfg": {
diff --git a/web/client/epics/__tests__/config-test.js b/web/client/epics/__tests__/config-test.js
index 75cc41207d..85f32afe66 100644
--- a/web/client/epics/__tests__/config-test.js
+++ b/web/client/epics/__tests__/config-test.js
@@ -8,7 +8,8 @@
import expect from 'expect';
import {head} from 'lodash';
-import {loadMapConfigAndConfigureMap, loadMapInfoEpic, storeDetailsInfoEpic, backgroundsListInitEpic} from '../config';
+import {loadMapConfigAndConfigureMap, loadMapInfoEpic, storeDetailsInfoDashboardEpic, storeDetailsInfoEpic, backgroundsListInitEpic} from '../config';
+
import {LOAD_USER_SESSION} from '../../actions/usersession';
import {
loadMapConfig,
@@ -31,6 +32,7 @@ import testConfigEPSG31468 from "raw-loader!../../test-resources/testConfigEPSG3
import ConfigUtils from "../../utils/ConfigUtils";
import { DETAILS_LOADED } from '../../actions/details';
import { EMPTY_RESOURCE_VALUE } from '../../utils/MapInfoUtils';
+import { dashboardLoaded } from '../../actions/dashboard';
const api = {
getResource: () => Promise.resolve({mapId: 1234})
@@ -346,7 +348,7 @@ describe('config epics', () => {
switch (action.type) {
case DETAILS_LOADED:
- expect(action.mapId).toBe(mapId);
+ expect(action.id).toBe(mapId);
expect(action.detailsUri).toBe("rest/geostore/data/1/raw?decode=datauri");
break;
default:
@@ -457,5 +459,107 @@ describe('config epics', () => {
}, {});
});
});
+
+ describe("storeDetailsInfoDashboardEpic", () => {
+ beforeEach(done => {
+ mockAxios = new MockAdapter(axios);
+ setTimeout(done);
+ });
+
+ afterEach(done => {
+ mockAxios.restore();
+ setTimeout(done);
+ });
+ const dashboardId = 1;
+ const dashboardAttributesEmptyDetails = {
+ "AttributeList": {
+ "Attribute": [
+ {
+ "name": "details",
+ "type": "STRING",
+ "value": EMPTY_RESOURCE_VALUE
+ }
+ ]
+ }
+ };
+
+ const dashboardAttributesWithoutDetails = {
+ "AttributeList": {
+ "Attribute": []
+ }
+ };
+
+ const dashboardAttributesWithDetails = {
+ AttributeList: {
+ Attribute: [
+ {
+ name: 'details',
+ type: 'STRING',
+ value: 'rest\/geostore\/data\/1\/raw?decode=datauri'
+ },
+ {
+ name: "thumbnail",
+ type: "STRING",
+ value: 'rest\/geostore\/data\/1\/raw?decode=datauri'
+ },
+ {
+ name: 'owner',
+ type: 'STRING',
+ value: 'admin'
+ }
+ ]
+ }
+ };
+ it('test storeDetailsInfoDashboardEpic', (done) => {
+ mockAxios.onGet().reply(200, dashboardAttributesWithDetails);
+ testEpic(addTimeoutEpic(storeDetailsInfoDashboardEpic), 1, dashboardLoaded("RES", "DATA"), actions => {
+ expect(actions.length).toBe(1);
+ actions.map((action) => {
+
+ switch (action.type) {
+ case DETAILS_LOADED:
+ expect(action.id).toBe(dashboardId);
+ expect(action.detailsUri).toBe("rest/geostore/data/1/raw?decode=datauri");
+ break;
+ default:
+ expect(true).toBe(false);
+ }
+ });
+ done();
+ }, {dashboard: {
+ resource: {
+ id: dashboardId,
+ attributes: {}
+ }
+ }});
+ });
+ it('test storeDetailsInfoDashboardEpic when api returns NODATA value', (done) => {
+ // const mock = new MockAdapter(axios);
+ mockAxios.onGet().reply(200, dashboardAttributesWithoutDetails);
+ testEpic(addTimeoutEpic(storeDetailsInfoDashboardEpic), 1, dashboardLoaded("RES", "DATA"), actions => {
+ expect(actions.length).toBe(1);
+ actions.map((action) => expect(action.type).toBe(TEST_TIMEOUT));
+ done();
+ }, {dashboard: {
+ resource: {
+ id: dashboardId,
+ attributes: {}
+ }
+ }});
+ });
+ it('test storeDetailsInfoDashboardEpic when api doesnt return details', (done) => {
+ mockAxios.onGet().reply(200, dashboardAttributesEmptyDetails);
+ testEpic(addTimeoutEpic(storeDetailsInfoDashboardEpic), 1, dashboardLoaded("RES", "DATA"), actions => {
+ expect(actions.length).toBe(1);
+ actions.map((action) => expect(action.type).toBe(TEST_TIMEOUT));
+ done();
+ }, {dashboard: {
+ resource: {
+ id: dashboardId,
+ attributes: {}
+ }
+ }});
+ });
+ });
});
diff --git a/web/client/epics/__tests__/dashboard-test.js b/web/client/epics/__tests__/dashboard-test.js
index 1d67d3beb8..4c224e0efb 100644
--- a/web/client/epics/__tests__/dashboard-test.js
+++ b/web/client/epics/__tests__/dashboard-test.js
@@ -338,16 +338,14 @@ describe('saveDashboard', () => {
const startActions = [saveDashboard(RESOURCE)];
testEpic(saveDashboardMethod, actionsCount, startActions, actions => {
- expect(actions.length).toBe(actionsCount);
- expect(actions[0].type).toBe(DASHBOARD_LOADING);
- expect(actions[0].value).toBe(true);
- expect(actions[1].type).toBe(SAVE_ERROR);
+ expect(actions.length).toBe(2);
+ expect(actions[0].type).toBe(SAVE_ERROR);
expect(
- actions[1].error.status === 403
- || actions[1].error.status === 404
+ actions[0].error.status === 403
+ || actions[0].error.status === 404
).toBeTruthy();
- expect(actions[2].type).toBe(DASHBOARD_LOADING);
- expect(actions[2].value).toBe(false);
+ expect(actions[1].type).toBe(DASHBOARD_LOADING);
+ expect(actions[1].value).toBe(false);
}, BASE_STATE, done);
});
@@ -364,13 +362,11 @@ describe('saveDashboard', () => {
const startActions = [saveDashboard(withoutMetadata)];
testEpic(saveDashboardMethod, actionsCount, startActions, actions => {
- expect(actions.length).toBe(3);
- expect(actions[0].type).toBe(DASHBOARD_LOADING);
- expect(actions[0].value).toBe(true);
- expect(actions[1].type).toBe(SAVE_ERROR);
- expect(typeof(actions[1].error) === 'string').toBeTruthy();
- expect(actions[2].type).toBe(DASHBOARD_LOADING);
- expect(actions[2].value).toBe(false);
+ expect(actions.length).toBe(2);
+ expect(actions[0].type).toBe(SAVE_ERROR);
+ expect(typeof(actions[0].error) === 'string').toBeTruthy();
+ expect(actions[1].type).toBe(DASHBOARD_LOADING);
+ expect(actions[1].value).toBe(false);
}, BASE_STATE, done);
});
});
diff --git a/web/client/epics/__tests__/details-test.js b/web/client/epics/__tests__/details-test.js
index 7c6ff90a85..29cd03de96 100644
--- a/web/client/epics/__tests__/details-test.js
+++ b/web/client/epics/__tests__/details-test.js
@@ -35,7 +35,7 @@ let map1 = {
id: mapId,
name: "name"
};
-const testState = {
+const mapTestState = {
mapInitialConfig: {
mapId
},
@@ -48,11 +48,116 @@ const testState = {
},
details: {}
};
+const dashboardTestState = {
+ dashboard: {
+ resource: {
+ id: "123",
+ attributes: {
+ details: encodeURIComponent(detailsUri)
+
+ }
+ }
+ }
+};
+
const rootEpic = combineEpics(closeDetailsPanelEpic);
const epicMiddleware = createEpicMiddleware(rootEpic);
const mockStore = configureMockStore([epicMiddleware]);
-describe('details epics tests', () => {
+describe('details epics tests for map', () => {
+ const oldGetDefaults = ConfigUtils.getDefaults;
+ let store;
+
+ beforeEach(() => {
+ store = mockStore();
+ ConfigUtils.getDefaults = () => ({
+ geoStoreUrl: baseUrl
+ });
+ });
+
+ afterEach(() => {
+ epicMiddleware.replaceEpic(rootEpic);
+ ConfigUtils.getDefaults = oldGetDefaults;
+ });
+
+ it('test closeDetailsPanel', (done) => {
+
+ store.dispatch(closeDetailsPanel());
+
+ setTimeout( () => {
+ try {
+ const actions = store.getActions();
+ expect(actions.length).toBe(2);
+ expect(actions[0].type).toBe(CLOSE_DETAILS_PANEL);
+ expect(actions[1].type).toBe(SET_CONTROL_PROPERTY);
+ } catch (e) {
+ done(e);
+ }
+ done();
+ }, 50);
+
+ });
+ it('test fetchDataForDetailsPanel', (done) => {
+ map1.details = encodeURIComponent(detailsUri);
+ testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 2, openDetailsPanel(), actions => {
+ expect(actions.length).toBe(2);
+ actions.map((action) => {
+ switch (action.type) {
+ case TOGGLE_CONTROL:
+ expect(action.control).toBe("details");
+ expect(action.property).toBe("enabled");
+ break;
+ case UPDATE_DETAILS:
+ expect(action.detailsText.indexOf(detailsText)).toNotBe(-1);
+ break;
+ default:
+ expect(true).toBe(false);
+ }
+ });
+ done();
+ }, mapTestState);
+ });
+ it('test fetchDataForDetailsPanel with Error', (done) => {
+ testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 2, openDetailsPanel(), actions => {
+ expect(actions.length).toBe(2);
+ actions.map((action) => {
+ switch (action.type) {
+ case TOGGLE_CONTROL:
+ expect(action.control).toBe("details");
+ expect(action.property).toBe("enabled");
+ break;
+ case SHOW_NOTIFICATION:
+ expect(action.message).toBe("maps.feedback.errorFetchingDetailsOfMap");
+ break;
+ default:
+ expect(true).toBe(false);
+ }
+ });
+ done();
+ }, {
+ locale: {
+ messages: {
+ maps: {
+ feedback: {
+ errorFetchingDetailsOfMap: "maps.feedback.errorFetchingDetailsOfMap"
+ }
+ }
+ }
+ },
+ mapInitialConfig: {
+ mapId
+ },
+ map: {
+ present: {
+ info: {}
+ }
+ }
+ });
+ });
+});
+
+
+describe('details epics tests for dashbaord', () => {
const oldGetDefaults = ConfigUtils.getDefaults;
let store;
@@ -103,7 +208,7 @@ describe('details epics tests', () => {
}
});
done();
- }, testState);
+ }, dashboardTestState);
});
it('test fetchDataForDetailsPanel with Error', (done) => {
testEpic(addTimeoutEpic(fetchDataForDetailsPanel), 2, openDetailsPanel(), actions => {
diff --git a/web/client/epics/__tests__/tutorial-test.js b/web/client/epics/__tests__/tutorial-test.js
index c3a026f54f..37bfe5c976 100644
--- a/web/client/epics/__tests__/tutorial-test.js
+++ b/web/client/epics/__tests__/tutorial-test.js
@@ -584,7 +584,7 @@ describe('tutorial Epics', () => {
});
});
describe('openDetailsPanelEpic tests', () => {
- it('should open the details panel if it has showAtStartup set to true', (done) => {
+ it('should open the details panel if it is a (Map) and it has showAtStartup set to true', (done) => {
const NUM_ACTIONS = 1;
testEpic(openDetailsPanelEpic, NUM_ACTIONS, closeTutorial(), (actions) => {
@@ -595,6 +595,7 @@ describe('tutorial Epics', () => {
}, {
map: {
present: {
+ mapId: "123",
info: {
detailsSettings: {
showAtStartup: true
@@ -604,7 +605,28 @@ describe('tutorial Epics', () => {
}
});
});
- it('should open the details panel if it has showAtStartup set to false', (done) => {
+ it('should open the details panel if it is a (Dashboard) and it has showAtStartup set to true', (done) => {
+ const NUM_ACTIONS = 1;
+
+ testEpic(openDetailsPanelEpic, NUM_ACTIONS, closeTutorial(), (actions) => {
+ expect(actions.length).toBe(NUM_ACTIONS);
+ const [action] = actions;
+ expect(action.type).toBe(OPEN_DETAILS_PANEL);
+ done();
+ }, {
+ dashboard: {
+ resource: {
+ id: "123",
+ attributes: {
+ detailsSettings: {
+ showAtStartup: true
+ }
+ }
+ }
+ }
+ });
+ });
+ it('should open the details panel if it is a(Map) and it has showAtStartup set to false', (done) => {
const NUM_ACTIONS = 1;
testEpic(addTimeoutEpic(openDetailsPanelEpic, 100), NUM_ACTIONS, closeTutorial(), (actions) => {
@@ -615,6 +637,7 @@ describe('tutorial Epics', () => {
}, {
map: {
present: {
+ mapId: "123",
info: {
detailsSettings: {
showAtStartup: false
@@ -624,5 +647,26 @@ describe('tutorial Epics', () => {
}
});
});
+ it('should open the details panel if it is a (Dashboard) and it has showAtStartup set to false', (done) => {
+ const NUM_ACTIONS = 1;
+
+ testEpic(addTimeoutEpic(openDetailsPanelEpic, 100), NUM_ACTIONS, closeTutorial(), (actions) => {
+ expect(actions.length).toBe(NUM_ACTIONS);
+ const [action] = actions;
+ expect(action.type).toBe(TEST_TIMEOUT);
+ done();
+ }, {
+ dashboard: {
+ resource: {
+ id: "123",
+ attributes: {
+ detailsSettings: {
+ showAtStartup: false
+ }
+ }
+ }
+ }
+ });
+ });
});
});
diff --git a/web/client/epics/config.js b/web/client/epics/config.js
index 619c438c46..914fd7a9b7 100644
--- a/web/client/epics/config.js
+++ b/web/client/epics/config.js
@@ -27,6 +27,8 @@ import Persistence from '../api/persistence';
import GeoStoreApi from '../api/GeoStoreDAO';
import { isLoggedIn, userSelector } from '../selectors/security';
import { mapIdSelector, projectionDefsSelector } from '../selectors/map';
+import { getDashboardId } from '../selectors/dashboard';
+import { DASHBOARD_LOADED } from '../actions/dashboard';
import {loadUserSession, USER_SESSION_LOADED, userSessionStartSaving, saveMapConfig} from '../actions/usersession';
import { detailsLoaded, openDetailsPanel } from '../actions/details';
import {userSessionEnabledSelector, buildSessionName} from "../selectors/usersession";
@@ -248,3 +250,33 @@ export const backgroundsListInitEpic = (action$) =>
...(currentBackground ? [setCurrentBackgroundLayer(currentBackground.id)] : [])
);
});
+
+export const storeDetailsInfoDashboardEpic = (action$, store) =>
+ action$.ofType(DASHBOARD_LOADED)
+ .switchMap(() => {
+ const dashboardId = getDashboardId(store.getState());
+ const isTutorialRunning = store.getState()?.tutorial?.run;
+ return !dashboardId
+ ? Observable.empty()
+ : Observable.fromPromise(
+ GeoStoreApi.getResourceAttributes(dashboardId)
+ ).switchMap((attributes) => {
+ let details = find(attributes, {name: 'details'});
+ const detailsSettingsAttribute = find(attributes, {name: 'detailsSettings'});
+ let detailsSettings = {};
+ if (!details || details.value === EMPTY_RESOURCE_VALUE) {
+ return Observable.empty();
+ }
+
+ try {
+ detailsSettings = JSON.parse(detailsSettingsAttribute.value);
+ } catch (e) {
+ detailsSettings = {};
+ }
+
+ return Observable.of(
+ detailsLoaded(dashboardId, details.value, detailsSettings),
+ ...(detailsSettings.showAtStartup && !isTutorialRunning ? [openDetailsPanel()] : [])
+ );
+ });
+ });
diff --git a/web/client/epics/dashboard.js b/web/client/epics/dashboard.js
index eaf304b836..5c5a85636d 100644
--- a/web/client/epics/dashboard.js
+++ b/web/client/epics/dashboard.js
@@ -6,6 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
import Rx from 'rxjs';
+import { mapValues, isObject, keys, isNil } from 'lodash';
import { NEW, INSERT, EDIT, OPEN_FILTER_EDITOR, editNewWidget, onEditorChange} from '../actions/widgets';
@@ -35,7 +36,7 @@ import { isLoggedIn } from '../selectors/security';
import { getEditingWidgetLayer, getEditingWidgetFilter, getWidgetFilterKey } from '../selectors/widgets';
import { pathnameSelector } from '../selectors/router';
import { download, readJson } from '../utils/FileUtils';
-import { createResource, updateResource, getResource } from '../api/persistence';
+import { createResource, updateResource, getResource, updateResourceAttribute } from '../api/persistence';
import { wrapStartStop } from '../observables/epics';
import { LOCATION_CHANGE, push } from 'connected-react-router';
import { convertDependenciesMappingForCompatibility } from "../utils/WidgetsUtils";
@@ -161,9 +162,32 @@ export const reloadDashboardOnLoginLogout = (action$) =>
// saving dashboard flow (both creation and update)
export const saveDashboard = action$ => action$
.ofType(SAVE_DASHBOARD)
- .exhaustMap(({resource} = {}) =>
- (!resource.id ? createResource(resource) : updateResource(resource))
- .switchMap(rid => Rx.Observable.of(
+ .exhaustMap(({resource} = {}) =>{
+ // convert to json if attribute is an object
+ const attributesFixed = mapValues(resource.attributes, attr => {
+ if (isObject(attr)) {
+ let json = null;
+ try {
+ json = JSON.stringify(attr);
+ } catch (e) {
+ json = null;
+ }
+ return json;
+ }
+ return attr;
+ });
+ // filter out invalid attributes
+ // thumbnails and details are handled separately(linked resources)
+ const validAttributesNames = keys(attributesFixed)
+ .filter(attrName => attrName !== 'thumbnail' && attrName !== 'details' && !isNil(attributesFixed[attrName]));
+ return Rx.Observable.forkJoin(
+ (!resource.id ? createResource(resource) : updateResource(resource)))
+ .switchMap(([rid]) => (validAttributesNames.length > 0 ?
+ Rx.Observable.forkJoin(validAttributesNames.map(attrName => updateResourceAttribute({
+ id: rid,
+ name: attrName,
+ value: attributesFixed[attrName]
+ }))) : Rx.Observable.of([])) .switchMap(() => Rx.Observable.of(
dashboardSaved(rid),
resource.id ? triggerSave(false) : triggerSaveAs(false),
!resource.id
@@ -175,15 +199,14 @@ export const saveDashboard = action$ => action$
title: "saveDialog.saveSuccessTitle",
message: "saveDialog.saveSuccessMessage"
})).delay(!resource.id ? 1000 : 0) // delay to allow loading
- )
- )
- .let(wrapStartStop(
- dashboardLoading(true, "saving"),
- dashboardLoading(false, "saving")
))
- .catch(
- ({ status, statusText, data, message, ...other } = {}) => Rx.Observable.of(dashboardSaveError(status ? { status, statusText, data } : message || other), dashboardLoading(false, "saving"))
- )
+ .let(wrapStartStop(
+ dashboardLoading(true, "saving"),
+ dashboardLoading(false, "saving")
+ )
+ ));
+ }).catch(
+ ({ status, statusText, data, message, ...other } = {}) => Rx.Observable.of(dashboardSaveError(status ? { status, statusText, data } : message || other), dashboardLoading(false, "saving"))
);
export const exportDashboard = action$ => action$
diff --git a/web/client/epics/details.js b/web/client/epics/details.js
index 2e9e246f12..48ef7e7f4b 100644
--- a/web/client/epics/details.js
+++ b/web/client/epics/details.js
@@ -18,26 +18,31 @@ import {
import { toggleControl, setControlProperty } from '../actions/controls';
import {
- mapInfoDetailsUriFromIdSelector
+ mapIdSelector
} from '../selectors/map';
+import { getDashboardId } from '../selectors/dashboard';
import GeoStoreApi from '../api/GeoStoreDAO';
import { getIdFromUri } from '../utils/MapUtils';
import { basicError } from '../utils/NotificationUtils';
import { VISUALIZATION_MODE_CHANGED } from '../actions/maptype';
+import { detailsUriSelector } from '../selectors/details';
export const fetchDataForDetailsPanel = (action$, store) =>
action$.ofType(OPEN_DETAILS_PANEL)
.switchMap(() => {
const state = store.getState();
- const detailsUri = mapInfoDetailsUriFromIdSelector(state);
+ const mapId = mapIdSelector(state);
+ const dashboardId = getDashboardId(state);
+ const detailsUri = detailsUriSelector(state);
const detailsId = getIdFromUri(detailsUri);
+ const resourceId = dashboardId || mapId;
return Rx.Observable.fromPromise(GeoStoreApi.getData(detailsId)
.then(data => data))
.switchMap((details) => {
return Rx.Observable.of(
- updateDetails(details)
+ updateDetails(details, resourceId)
);
}).startWith(toggleControl("details", "enabled"))
.catch(() => {
diff --git a/web/client/epics/tutorial.js b/web/client/epics/tutorial.js
index 7563764dc6..48c046e408 100644
--- a/web/client/epics/tutorial.js
+++ b/web/client/epics/tutorial.js
@@ -27,9 +27,9 @@ import { CONTEXT_TUTORIALS } from '../actions/contextcreator';
import { LOCATION_CHANGE } from 'connected-react-router';
import { isEmpty, isArray, isObject } from 'lodash';
import { getApi } from '../api/userPersistedStorage';
-import { mapSelector } from '../selectors/map';
import {REDUCERS_LOADED} from "../actions/storemanager";
import { VISUALIZATION_MODE_CHANGED } from '../actions/maptype';
+import { detailsSettingsSelector } from '../selectors/details';
const findTutorialId = path => path.match(/\/(viewer)\/(\w+)\/(\d+)/) && path.replace(/\/(viewer)\/(\w+)\/(\d+)/, "$2")
|| path.match(/\/(\w+)\/(\d+)/) && path.replace(/\/(\w+)\/(\d+)/, "$1")
@@ -168,7 +168,14 @@ export const getActionsFromStepEpic = (action$) =>
export const openDetailsPanelEpic = (action$, store) =>
action$.ofType(CLOSE_TUTORIAL)
- .filter(() => mapSelector(store.getState())?.info?.detailsSettings?.showAtStartup )
+ .filter(() => {
+ const state = store.getState();
+ let detailsSettings = detailsSettingsSelector(state);
+ if (detailsSettings && typeof detailsSettings === 'string') {
+ detailsSettings = JSON.parse(detailsSettings);
+ }
+ return detailsSettings?.showAtStartup;
+ })
.switchMap( () => {
return Rx.Observable.of(openDetailsPanel());
});
diff --git a/web/client/plugins/AddWidgetDashboard.jsx b/web/client/plugins/AddWidgetDashboard.jsx
new file mode 100644
index 0000000000..c70413f611
--- /dev/null
+++ b/web/client/plugins/AddWidgetDashboard.jsx
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+import React from 'react';
+
+import PropTypes from 'prop-types';
+import { createSelector } from 'reselect';
+import { connect } from 'react-redux';
+import ToolbarButton from '../components/misc/toolbar/ToolbarButton';
+import { buttonCanEdit, isDashboardEditing } from '../selectors/dashboard';
+import { setEditing } from '../actions/dashboard';
+import { createWidget } from '../actions/widgets';
+import { createPlugin } from '../utils/PluginsUtils';
+
+class AddWidgetDashboard extends React.Component {
+ static propTypes = {
+ canEdit: PropTypes.bool,
+ editing: PropTypes.bool,
+ onAddWidget: PropTypes.func,
+ setEditing: PropTypes.func
+ }
+
+ static defaultProps = {
+ editing: false,
+ canEdit: false
+ }
+
+ render() {
+ if (!this.props.canEdit && !this.props.editing) return false;
+ return (
{
+ if (this.props.editing) this.props.setEditing(false);
+ else {
+ this.props.onAddWidget();
+ }
+ }}
+ id={'ms-add-card-dashboard'}
+ tooltipPosition={'left'}
+ btnDefaultProps={{ tooltipPosition: 'right', className: 'square-button-md', bsStyle: this.props.editing ? 'primary' : 'tray' }}/>);
+ }
+}
+
+const ConnectedAddWidget = connect(
+ createSelector(
+ buttonCanEdit,
+ isDashboardEditing,
+ ( edit, editing ) => ({
+ canEdit: edit,
+ editing
+ })
+ ),
+ {
+ onAddWidget: createWidget,
+ setEditing: setEditing
+ }
+)(AddWidgetDashboard);
+
+export default createPlugin('AddWidgetDashboard', {
+ component: () => null,
+ containers: {
+ SidebarMenu: {
+ name: "AddWidgetDashboard",
+ position: 10,
+ tool: ConnectedAddWidget,
+ priority: 0
+ }
+ }
+});
diff --git a/web/client/plugins/Dashboard.jsx b/web/client/plugins/Dashboard.jsx
index e8e6964edd..04d30e5f87 100644
--- a/web/client/plugins/Dashboard.jsx
+++ b/web/client/plugins/Dashboard.jsx
@@ -47,6 +47,8 @@ import {
import dashboardReducers from '../reducers/dashboard';
import dashboardEpics from '../epics/dashboard';
import widgetsEpics from '../epics/widgets';
+import GlobalSpinner from '../components/misc/spinners/GlobalSpinner/GlobalSpinner';
+import { createPlugin } from '../utils/PluginsUtils';
const WidgetsView = compose(
connect(
@@ -183,14 +185,24 @@ class DashboardPlugin extends React.Component {
}
}
-export default {
- DashboardPlugin: withResizeDetector(DashboardPlugin),
+export default createPlugin("Dashboard", {
+ component: withResizeDetector(DashboardPlugin),
reducers: {
dashboard: dashboardReducers,
widgets: widgetsReducers
},
+ containers: {
+ SidebarMenu: {
+ name: "Dashboard-spinner",
+ alwaysVisible: true,
+ position: 2000,
+ tool: connect((state) => ({
+ loading: isDashboardLoading(state)
+ }))(GlobalSpinner)
+ }
+ },
epics: {
...dashboardEpics,
...widgetsEpics
}
-};
+});
diff --git a/web/client/plugins/DashboardEditor.jsx b/web/client/plugins/DashboardEditor.jsx
index c5649d1cf6..cfd3391b90 100644
--- a/web/client/plugins/DashboardEditor.jsx
+++ b/web/client/plugins/DashboardEditor.jsx
@@ -12,22 +12,17 @@ import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { createPlugin } from '../utils/PluginsUtils';
-
-
-import { dashboardHasWidgets, getWidgetsDependenciesGroups } from '../selectors/widgets';
-import { isDashboardEditing, showConnectionsSelector, isDashboardLoading, buttonCanEdit, isDashboardAvailable } from '../selectors/dashboard';
+import { isDashboardEditing, isDashboardLoading, isDashboardAvailable } from '../selectors/dashboard';
import { dashboardSelector, dashboardsLocalizedSelector } from './widgetbuilder/commons';
-import { createWidget, toggleConnection } from '../actions/widgets';
+import { toggleConnection } from '../actions/widgets';
import { setEditing, setEditorAvailable, triggerShowConnections } from '../actions/dashboard';
import withDashboardExitButton from './widgetbuilder/enhancers/withDashboardExitButton';
-import LoadingSpinner from '../components/misc/LoadingSpinner';
import WidgetTypeBuilder from './widgetbuilder/WidgetTypeBuilder';
import epics from '../epics/dashboard';
import dashboard from '../reducers/dashboard';
-import Toolbar from '../components/misc/toolbar/Toolbar';
const Builder =
compose(
@@ -38,51 +33,6 @@ const Builder =
})),
withDashboardExitButton
)(WidgetTypeBuilder);
-const EditorToolbar = compose(
- connect(
- createSelector(
- showConnectionsSelector,
- dashboardHasWidgets,
- buttonCanEdit,
- getWidgetsDependenciesGroups,
- (showConnections, hasWidgets, edit, groups = []) => ({
- showConnections,
- hasConnections: groups.length > 0,
- hasWidgets,
- canEdit: edit
- })
- ),
- {
- onShowConnections: triggerShowConnections,
- onAddWidget: createWidget
- }
- ),
- withProps(({
- onAddWidget = () => { },
- hasWidgets,
- canEdit,
- hasConnections,
- showConnections,
- onShowConnections = () => { }
- }) => ({
- buttons: [{
- glyph: 'plus',
- tooltipId: 'dashboard.editor.addACardToTheDashboard',
- bsStyle: 'primary',
- visible: canEdit,
- id: 'ms-add-card-dashboard',
- onClick: () => onAddWidget()
- },
- {
- glyph: showConnections ? 'bulb-on' : 'bulb-off',
- tooltipId: showConnections ? 'dashboard.editor.hideConnections' : 'dashboard.editor.showConnections',
- bsStyle: showConnections ? 'success' : 'primary',
- visible: !!hasWidgets && !!hasConnections || !canEdit,
- onClick: () => onShowConnections(!showConnections)
- }]
- }))
-)(Toolbar);
-
/**
* Side toolbar that allows to edit dashboard widgets.
@@ -98,7 +48,6 @@ class DashboardEditorComponent extends React.Component {
static propTypes = {
id: PropTypes.string,
editing: PropTypes.bool,
- loading: PropTypes.bool,
limitDockHeight: PropTypes.bool,
fluid: PropTypes.bool,
zIndex: PropTypes.number,
@@ -118,7 +67,6 @@ class DashboardEditorComponent extends React.Component {
id: "dashboard-editor",
editing: false,
dockSize: 500,
- loading: true,
limitDockHeight: true,
zIndex: 10000,
fluid: false,
@@ -141,10 +89,7 @@ class DashboardEditorComponent extends React.Component {
return this.props.editing
? this.props.setEditing(false)} catalog={this.props.catalog} />
- : (
-
- {this.props.loading ? : null}
-
);
+ : false;
}
}
@@ -153,7 +98,7 @@ const Plugin = connect(
isDashboardEditing,
isDashboardLoading,
isDashboardAvailable,
- (editing, loading, isDashboardOpened) => ({ editing, loading, isDashboardOpened })
+ (editing, isDashboardOpened) => ({ editing, isDashboardOpened })
), {
setEditing,
onMount: () => setEditorAvailable(true),
diff --git a/web/client/plugins/DashboardSave.jsx b/web/client/plugins/DashboardSave.jsx
index 25cdf61230..4afffdfe94 100644
--- a/web/client/plugins/DashboardSave.jsx
+++ b/web/client/plugins/DashboardSave.jsx
@@ -37,7 +37,8 @@ const SaveBaseDialog = compose(
onSave: saveDashboard
}),
withProps({
- category: "DASHBOARD"
+ category: "DASHBOARD",
+ enableDetails: true // to enable details in dashboard
}),
handleSaveModal
)(Save);
@@ -68,6 +69,23 @@ export const DashboardSave = createPlugin('DashboardSave', {
text: ,
icon: ,
action: triggerSave.bind(null, true),
+ tooltip: "saveDialog.saveTooltip",
+ // display the BurgerMenu button only if the map can be edited
+ selector: createSelector(
+ isLoggedIn,
+ dashboardResource,
+ (loggedIn, {canEdit, id} = {}) => ({
+ style: loggedIn && id && canEdit ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable
+ })
+ )
+ },
+ SidebarMenu: {
+ name: 'dashboardSave',
+ position: 30,
+ text: ,
+ icon: ,
+ action: triggerSave.bind(null, true),
+ tooltip: "saveDialog.saveTooltip",
// display the BurgerMenu button only if the map can be edited
selector: createSelector(
isLoggedIn,
@@ -120,6 +138,21 @@ export const DashboardSaveAs = createPlugin('DashboardSaveAs', {
style: loggedIn ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable
})
)
+ },
+ SidebarMenu: {
+ name: 'dashboardSaveAs',
+ position: 31,
+ tooltip: "saveAs",
+ text: ,
+ icon: ,
+ action: triggerSaveAs.bind(null, true),
+ // always display on the BurgerMenu button if logged in
+ selector: createSelector(
+ isLoggedIn,
+ (loggedIn) => ({
+ style: loggedIn ? {} : { display: "none" }// the resource is new (no resource) or if present, is editable
+ })
+ )
}
}
});
diff --git a/web/client/plugins/Details.jsx b/web/client/plugins/Details.jsx
index 113d89dd7b..90d80beb09 100644
--- a/web/client/plugins/Details.jsx
+++ b/web/client/plugins/Details.jsx
@@ -17,8 +17,7 @@ import {
NO_DETAILS_AVAILABLE
} from "../actions/details";
-import { mapIdSelector, mapInfoDetailsUriFromIdSelector, mapInfoDetailsSettingsFromIdSelector } from '../selectors/map';
-import { detailsTextSelector } from '../selectors/details';
+import { detailsTextSelector, detailsUriSelector, detailsSettingsSelector } from '../selectors/details';
import { mapLayoutValuesSelector } from '../selectors/maplayout';
import DetailsViewer from '../components/resources/modals/fragments/DetailsViewer';
@@ -32,6 +31,7 @@ import { createPlugin } from '../utils/PluginsUtils';
import details from '../reducers/details';
import * as epics from '../epics/details';
import {createStructuredSelector} from "reselect";
+import { getDashboardId } from '../selectors/dashboard';
/**
* Allow to show details for the map.
@@ -51,7 +51,8 @@ const DetailsPlugin = ({
dockStyle,
detailsText,
showAsModal = false,
- onClose = () => {}
+ onClose = () => {},
+ isDashboard
}) => {
const viewer = ();
- return showAsModal ?
+ return showAsModal && active ?
}
onClose={onClose}>
{viewer}
- :
+ : active &&
{
+ return getDashboardId(state) ? true : false;
+ },
active: state => get(state, "controls.details.enabled"),
- dockStyle: state => mapLayoutValuesSelector(state, { height: true, right: true }, true),
+ dockStyle: state => {
+ const isDashbaord = getDashboardId(state);
+ let layoutValues = mapLayoutValuesSelector(state, { height: true, right: true }, true);
+ if (isDashbaord) {
+ layoutValues = { ...layoutValues, right: 0, height: '100%'};
+ }
+ return layoutValues;
+ },
detailsText: detailsTextSelector,
- showAsModal: state => mapInfoDetailsSettingsFromIdSelector(state)?.showAsModal
+ showAsModal: state => {
+ let detailsSettings = detailsSettingsSelector(state);
+ if (detailsSettings && typeof detailsSettings === 'string') detailsSettings = JSON.parse(detailsSettings);
+ return detailsSettings?.showAsModal;
+ }
}), {
onClose: closeDetailsPanel
})(DetailsPlugin),
@@ -99,8 +115,7 @@ export default createPlugin('Details', {
icon: ,
action: openDetailsPanel,
selector: (state) => {
- const mapId = mapIdSelector(state);
- const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId);
+ const detailsUri = detailsUriSelector(state);
if (detailsUri) {
return {};
}
@@ -117,8 +132,7 @@ export default createPlugin('Details', {
icon: ,
action: openDetailsPanel,
selector: (state) => {
- const mapId = mapIdSelector(state);
- const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId);
+ const detailsUri = detailsUriSelector(state);
if (detailsUri) {
return {};
}
@@ -134,8 +148,7 @@ export default createPlugin('Details', {
icon: ,
action: openDetailsPanel,
selector: (state) => {
- const mapId = mapIdSelector(state);
- const detailsUri = mapId && mapInfoDetailsUriFromIdSelector(state, mapId);
+ const detailsUri = detailsUriSelector(state);
if (detailsUri) {
return {
bsStyle: state.controls.details && state.controls.details.enabled ? 'primary' : 'tray',
diff --git a/web/client/plugins/MapConnectionDashboard.jsx b/web/client/plugins/MapConnectionDashboard.jsx
new file mode 100644
index 0000000000..86da06303b
--- /dev/null
+++ b/web/client/plugins/MapConnectionDashboard.jsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+import React from 'react';
+
+import PropTypes from 'prop-types';
+import { createSelector } from 'reselect';
+import { connect } from 'react-redux';
+import ToolbarButton from '../components/misc/toolbar/ToolbarButton';
+import { buttonCanEdit, showConnectionsSelector } from '../selectors/dashboard';
+import { dashboardHasWidgets, getWidgetsDependenciesGroups } from '../selectors/widgets';
+import { triggerShowConnections } from '../actions/dashboard';
+import { createPlugin } from '../utils/PluginsUtils';
+
+class MapConnectionDashboard extends React.Component {
+ static propTypes = {
+ showConnections: PropTypes.bool,
+ canEdit: PropTypes.bool,
+ hasWidgets: PropTypes.bool,
+ hasConnections: PropTypes.bool,
+ onShowConnections: PropTypes.func
+ }
+
+ static defaultProps = {
+ onShowConnections: () => {}
+ }
+
+ render() {
+ const { showConnections, canEdit, hasConnections, hasWidgets, onShowConnections } = this.props;
+ if (!(!!hasWidgets && !!hasConnections || !canEdit)) return false;
+ return (onShowConnections(!showConnections)}
+ tooltipPosition={'left'}
+ id={'ms-map-connection-card-dashboard'}
+ btnDefaultProps={{ tooltipPosition: 'bottom', className: 'square-button-md', bsStyle: 'primary' }}/>);
+ }
+}
+
+const ConnectedMapAddWidget = connect(
+ createSelector(
+ showConnectionsSelector,
+ dashboardHasWidgets,
+ buttonCanEdit,
+ getWidgetsDependenciesGroups,
+ (showConnections, hasWidgets, edit, groups = []) => ({
+ showConnections,
+ hasConnections: groups.length > 0,
+ hasWidgets,
+ canEdit: edit
+ })
+ ),
+ {
+ onShowConnections: triggerShowConnections
+ }
+)(MapConnectionDashboard);
+
+export default createPlugin('MapConnectionDashboard', {
+ component: () => null,
+ containers: {
+ SidebarMenu: {
+ name: "MapConnectionDashboard",
+ tool: ConnectedMapAddWidget,
+ position: 10,
+ priority: 0
+ }
+ }
+});
diff --git a/web/client/plugins/SidebarMenu.jsx b/web/client/plugins/SidebarMenu.jsx
index 538b93b823..32bb6dcdf6 100644
--- a/web/client/plugins/SidebarMenu.jsx
+++ b/web/client/plugins/SidebarMenu.jsx
@@ -23,7 +23,7 @@ import {createPlugin} from "../utils/PluginsUtils";
import sidebarMenuReducer from "../reducers/sidebarmenu";
import './sidebarmenu/sidebarmenu.less';
-import {lastActiveToolSelector, sidebarIsActiveSelector} from "../selectors/sidebarmenu";
+import {lastActiveToolSelector, sidebarIsActiveSelector, isSidebarWithFullHeight} from "../selectors/sidebarmenu";
import {setLastActiveItem} from "../actions/sidebarmenu";
import Message from "../components/I18N/Message";
@@ -40,7 +40,7 @@ class SidebarMenu extends React.Component {
sidebarWidth: PropTypes.number,
state: PropTypes.object,
setLastActiveItem: PropTypes.func,
- lastActiveTool: PropTypes.oneOfType([PropTypes.string, PropTypes.bool])
+ isSidebarFullHeight: PropTypes.bool
};
static contextTypes = {
@@ -60,7 +60,8 @@ class SidebarMenu extends React.Component {
stateSelector: 'sidebarMenu',
tool: SidebarElement,
toolCfg: {},
- sidebarWidth: 40
+ sidebarWidth: 40,
+ isSidebarFullHeight: false
};
constructor() {
@@ -90,7 +91,8 @@ class SidebarMenu extends React.Component {
}
return prev;
}, []).length > 0 : false;
- return newSize || newItems || newVisibleItems || newHeight || burgerMenuState || markedAsInactive;
+ const nextIsSideBarFullHeight = !this.props.isSidebarFullHeight && nextProps.isSidebarFullHeight;
+ return newSize || newItems || newVisibleItems || newHeight || burgerMenuState || markedAsInactive || nextIsSideBarFullHeight;
}
componentDidUpdate(prevProps) {
@@ -229,7 +231,7 @@ class SidebarMenu extends React.Component {
render() {
return this.state.hidden ? false : (
-