diff --git a/web/client/actions/__tests__/dashboard-test.js b/web/client/actions/__tests__/dashboard-test.js
index 7fc11224ee..15fc864694 100644
--- a/web/client/actions/__tests__/dashboard-test.js
+++ b/web/client/actions/__tests__/dashboard-test.js
@@ -9,16 +9,86 @@ var expect = require('expect');
const {
setEditing, SET_EDITING,
- setEditorAvailable, SET_EDITOR_AVAILABLE
+ setEditorAvailable, SET_EDITOR_AVAILABLE,
+ triggerShowConnections, SHOW_CONNECTIONS,
+ triggerSave, TRIGGER_SAVE_MODAL,
+ saveDashboard, SAVE_DASHBOARD,
+ dashboardSaveError, SAVE_ERROR,
+ dashboardSaved, DASHBOARD_SAVED,
+ loadDashboard, LOAD_DASHBOARD,
+ dashboardLoaded, DASHBOARD_LOADED,
+ dashboardLoading, DASHBOARD_LOADING
} = require('../dashboard');
-it('setEditing', () => {
- const retval = setEditing();
- expect(retval).toExist();
- expect(retval.type).toBe(SET_EDITING);
-});
-it('setEditorAvailable', () => {
- const retval = setEditorAvailable();
- expect(retval).toExist();
- expect(retval.type).toBe(SET_EDITOR_AVAILABLE);
+describe('Test correctness of the dashboard actions', () => {
+ it('setEditing', () => {
+ const retval = setEditing();
+ expect(retval).toExist();
+ expect(retval.type).toBe(SET_EDITING);
+ });
+ it('setEditorAvailable', () => {
+ const retval = setEditorAvailable();
+ expect(retval).toExist();
+ expect(retval.type).toBe(SET_EDITOR_AVAILABLE);
+ });
+ it('triggerShowConnections', () => {
+ const retval = triggerShowConnections(true);
+ expect(retval).toExist();
+ expect(retval.type).toBe(SHOW_CONNECTIONS);
+ expect(retval.show).toBe(true);
+ });
+ it('triggerShowConnections', () => {
+ const retval = triggerShowConnections();
+ expect(retval).toExist();
+ expect(retval.type).toBe(SHOW_CONNECTIONS);
+ });
+ it('triggerSave', () => {
+ const retval = triggerSave();
+ expect(retval).toExist();
+ expect(retval.type).toBe(TRIGGER_SAVE_MODAL);
+ });
+ it('saveDashboard', () => {
+ const retval = saveDashboard({TEST: "TEST"});
+ expect(retval).toExist();
+ expect(retval.type).toBe(SAVE_DASHBOARD);
+ expect(retval.resource.TEST).toBe("TEST");
+ });
+ it('dashboardSaveError', () => {
+ const retval = dashboardSaveError("ERROR");
+ expect(retval).toExist();
+ expect(retval.type).toBe(SAVE_ERROR);
+ expect(retval.error).toBe("ERROR");
+ });
+ it('dashboardSaved', () => {
+ const retval = dashboardSaved();
+ expect(retval).toExist();
+ expect(retval.type).toBe(DASHBOARD_SAVED);
+ });
+ it('loadDashboard', () => {
+ const retval = loadDashboard(1);
+ expect(retval).toExist();
+ expect(retval.type).toBe(LOAD_DASHBOARD);
+ expect(retval.id).toBe(1);
+ });
+ it('dashboardLoaded', () => {
+ const retval = dashboardLoaded("RES", "DATA");
+ expect(retval).toExist();
+ expect(retval.type).toBe(DASHBOARD_LOADED);
+ expect(retval.resource).toBe("RES");
+ expect(retval.data).toBe("DATA");
+ });
+ it('dashboardLoading default', () => {
+ const retval = dashboardLoading(false);
+ expect(retval).toExist();
+ expect(retval.type).toBe(DASHBOARD_LOADING);
+ expect(retval.name).toBe("loading");
+ expect(retval.value).toBe(false);
+ });
+ it('dashboardLoading', () => {
+ const retval = dashboardLoading(true, "saving");
+ expect(retval).toExist();
+ expect(retval.type).toBe(DASHBOARD_LOADING);
+ expect(retval.name).toBe("saving");
+ expect(retval.value).toBe(true);
+ });
});
diff --git a/web/client/actions/dashboard.js b/web/client/actions/dashboard.js
index 43a6471e77..f4a01498b7 100644
--- a/web/client/actions/dashboard.js
+++ b/web/client/actions/dashboard.js
@@ -1,11 +1,43 @@
const SET_EDITOR_AVAILABLE = "DASHBOARD:SET_AVAILABLE";
const SET_EDITING = "DASHBOARD:SET_EDITING";
const SHOW_CONNECTIONS = "DASHBOARD:SHOW_CONNECTIONS";
+const TRIGGER_SAVE_MODAL = "DASHBOARD:TRIGGER_SAVE_MODAL";
+
+const SAVE_DASHBOARD = "DASHBOARD:SAVE_DASHBOARD";
+const SAVE_ERROR = "DASHBOARD:SAVE_ERROR";
+const DASHBOARD_SAVED = "DASHBOARD:DASHBOARD_SAVED";
+
+const LOAD_DASHBOARD = "DASHBOARD:LOAD_DASHBOARD";
+const DASHBOARD_LOADED = "DASHBOARD:DASHBOARD_LOADED";
+const DASHBOARD_LOADING = "DASHBOARD:DASHBOARD_LOADING";
+
module.exports = {
SET_EDITING,
setEditing: (editing) => ({type: SET_EDITING, editing }),
SET_EDITOR_AVAILABLE,
setEditorAvailable: available => ({type: SET_EDITOR_AVAILABLE, available}),
SHOW_CONNECTIONS,
- triggerShowConnections: show => ({ type: SHOW_CONNECTIONS, show})
+ triggerShowConnections: show => ({ type: SHOW_CONNECTIONS, show}),
+ TRIGGER_SAVE_MODAL,
+ triggerSave: show => ({ type: TRIGGER_SAVE_MODAL, show}),
+ SAVE_DASHBOARD,
+ saveDashboard: resource => ({ type: SAVE_DASHBOARD, resource}),
+ SAVE_ERROR,
+ dashboardSaveError: error => ({type: SAVE_ERROR, error}),
+ DASHBOARD_SAVED,
+ dashboardSaved: id => ({type: DASHBOARD_SAVED, id}),
+ LOAD_DASHBOARD,
+ loadDashboard: id => ({ type: LOAD_DASHBOARD, id}),
+ DASHBOARD_LOADED,
+ dashboardLoaded: (resource, data) => ({ type: DASHBOARD_LOADED, resource, data}),
+ DASHBOARD_LOADING,
+ /**
+ * @param {boolean} value the value of the flag
+ * @param {string} [name] the name of the flag to set. loading is anyway always triggered
+ */
+ dashboardLoading: (value, name = "loading") => ({
+ type: DASHBOARD_LOADING,
+ name,
+ value
+ })
};
diff --git a/web/client/api/GeoStoreDAO.js b/web/client/api/GeoStoreDAO.js
index 8f2af0cf14..b1b168203b 100644
--- a/web/client/api/GeoStoreDAO.js
+++ b/web/client/api/GeoStoreDAO.js
@@ -74,6 +74,11 @@ const Api = {
"resources/resource/" + resourceId,
this.addBaseUrl(parseOptions(options))).then(function(response) {return response.data; });
},
+ getShortResource: function(resourceId, options) {
+ return axios.get(
+ "extjs/resource/" + resourceId,
+ this.addBaseUrl(parseOptions(options))).then(function(response) { return response.data; });
+ },
getResourcesByCategory: function(category, query, options) {
const q = query || "*";
const url = "extjs/search/category/" + category + "/*" + q + "*/thumbnail,details,featured"; // comma-separated list of wanted attributes
@@ -142,6 +147,26 @@ const Api = {
}
}, options)));
},
+ getResourceAttributes: function(resourceId, options = {}) {
+ return axios.get(
+ "resources/resource/" + resourceId + "/attributes",
+ this.addBaseUrl({
+ headers: {
+ 'Accept': "application/json"
+ },
+ ...options
+ })).then(({ data } = {}) => data)
+ .then(data => _.castArray(_.get(data, "AttributeList.Attribute")))
+ .then(attributes => (attributes && attributes[0] && attributes[0] !== "") ? attributes : []);
+ },
+ /**
+ * same of getPermissions but clean data properly and returns only the array of rules.
+ */
+ getResourcePermissions: function(resourceId, options) {
+ return Api.getPermissions(resourceId, options)
+ .then(rl => _.castArray(_.get(rl, 'SecurityRuleList.SecurityRule')))
+ .then(rules => (rules && rules[0] && rules[0] !== "") ? rules : []);
+ },
putResourceMetadata: function(resourceId, newName, newDescription, options) {
return axios.put(
"resources/resource/" + resourceId,
diff --git a/web/client/components/dashboard/forms/Metadata.jsx b/web/client/components/dashboard/forms/Metadata.jsx
new file mode 100644
index 0000000000..cfdedfb733
--- /dev/null
+++ b/web/client/components/dashboard/forms/Metadata.jsx
@@ -0,0 +1,85 @@
+/**
+ * Copyright 2016, 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.
+ */
+/**
+ * Copyright 2016, 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 PropTypes = require('prop-types');
+const {FormControl: BFormControl, FormGroup, ControlLabel} = require('react-bootstrap');
+const FormControl = require('../../misc/enhancers/localizedProps')('placeholder')(BFormControl);
+
+/**
+ * A DropDown menu for user details:
+ */
+class Metadata extends React.Component {
+ static propTypes = {
+ resource: PropTypes.object,
+ // CALLBACKS
+ onChange: PropTypes.func,
+
+ // I18N
+ nameFieldText: PropTypes.node,
+ descriptionFieldText: PropTypes.node,
+ namePlaceholderText: PropTypes.string,
+ descriptionPlaceholderText: PropTypes.string
+ };
+
+ static defaultProps = {
+ // CALLBACKS
+ onChange: () => {},
+ resource: {},
+ // I18N
+ nameFieldText: "Name",
+ descriptionFieldText: "Description",
+ namePlaceholderText: "Map Name",
+ descriptionPlaceholderText: "Map Description"
+ };
+
+ render() {
+ return (
);
+ }
+
+ changeName = (e) => {
+ this.props.onChange('metadata.name', e.target.value);
+ };
+
+ changeDescription = (e) => {
+ this.props.onChange('metadata.description', e.target.value);
+ };
+}
+
+
+module.exports = Metadata;
diff --git a/web/client/components/dashboard/forms/Thumbnail.jsx b/web/client/components/dashboard/forms/Thumbnail.jsx
new file mode 100644
index 0000000000..d9b55074aa
--- /dev/null
+++ b/web/client/components/dashboard/forms/Thumbnail.jsx
@@ -0,0 +1,136 @@
+const PropTypes = require('prop-types');
+/**
+ * Copyright 2016, 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 {Glyphicon} = require('react-bootstrap');
+const Dropzone = require('react-dropzone');
+const Spinner = require('react-spinkit');
+const Message = require('../../../components/I18N/Message');
+/**
+ * A Dropzone area for a thumbnail.
+ */
+
+class Thumbnail extends React.Component {
+ static propTypes = {
+ glyphiconRemove: PropTypes.string,
+ style: PropTypes.object,
+ loading: PropTypes.bool,
+ resource: PropTypes.object,
+ onError: PropTypes.func,
+ onUpdate: PropTypes.func,
+ onRemove: PropTypes.func,
+ // I18N
+ message: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
+ suggestion: PropTypes.oneOfType([PropTypes.string, PropTypes.element])
+ };
+
+ static contextTypes = {
+ messages: PropTypes.object
+ };
+
+ static defaultProps = {
+ loading: false,
+ glyphiconRemove: "remove-circle",
+ resource: {},
+ // CALLBACKS
+ onError: () => {},
+ onUpdate: () => {},
+ onSaveAll: () => {},
+ onRemove: () => {},
+ // I18N
+ message: ,
+ suggestion:
+ };
+
+ state = {};
+
+ onRemoveThumbnail = (event) => {
+ if (event !== null) {
+ event.stopPropagation();
+ }
+
+ this.files = null;
+ this.props.onError([]);
+ this.props.onRemove();
+ };
+
+ getThumbnailUrl = () => {
+ return this.props.thumbnail && this.props.thumbnail !== "NODATA" ? decodeURIComponent(this.props.thumbnail) : null;
+ };
+
+ isImage = (images) => {
+ return images && images[0].type === "image/png" || images && images[0].type === "image/jpeg" || images && images[0].type === "image/jpg";
+ };
+
+ getDataUri = (images, callback) => {
+ let filesSelected = images;
+ if (filesSelected && filesSelected.length > 0) {
+ let fileToLoad = filesSelected[0];
+ let fileReader = new FileReader();
+ fileReader.onload = (event) => callback(event.target.result);
+ return fileReader.readAsDataURL(fileToLoad);
+ }
+ return callback(null);
+ };
+
+ onDrop = (images) => {
+ // check formats and sizes
+ const isAnImage = this.isImage(images);
+ let errors = [];
+
+ this.getDataUri(images, (data) => {
+ if (isAnImage && data && data.length < 500000) {
+ // without errors
+ this.props.onError([], this.props.resource.id);
+ this.files = images;
+ this.props.onUpdate(data, images && images[0].preview);
+ } else {
+ // with at least one error
+ if (!isAnImage) {
+ errors.push("FORMAT");
+ }
+ if (data && data.length >= 500000) {
+ errors.push("SIZE");
+ }
+ this.props.onError(errors, this.props.resource.id);
+ this.files = images;
+ this.props.onUpdate(null, null);
+ }
+ });
+ };
+ getThumbnailDataUri = (callback) => {
+ this.getDataUri(this.files, callback);
+ };
+ render() {
+ return (
+ this.props.loading ?
:
+
+
+
+
+ { this.getThumbnailUrl()
+ ?
+
![]({this.getThumbnailUrl()})
+
{this.props.message}
{this.props.suggestion}
+
+
+
+
+ : {this.props.message}
{this.props.suggestion}
+ }
+
+
+
+ );
+ }
+}
+
+module.exports = Thumbnail;
diff --git a/web/client/components/dashboard/forms/__tests__/Metadata-test.jsx b/web/client/components/dashboard/forms/__tests__/Metadata-test.jsx
new file mode 100644
index 0000000000..4351f7eacb
--- /dev/null
+++ b/web/client/components/dashboard/forms/__tests__/Metadata-test.jsx
@@ -0,0 +1,56 @@
+/*
+ * 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 Metadata = require('../Metadata');
+describe('Metadata component', () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+ it('Metadata rendering with defaults', () => {
+ ReactDOM.render(, document.getElementById("container"));
+ const container = document.getElementById('container');
+ const el = container.querySelectorAll('input');
+ expect(el.length).toBe(2);
+ });
+ it('Metadata rendering with meta-data', () => {
+ const resource = {
+ metadata: {
+ name: "NAME",
+ description: "DESCRIPTION"
+ }
+ };
+ ReactDOM.render(, document.getElementById("container"));
+ const container = document.getElementById('container');
+ const el = container.querySelectorAll('input');
+ expect(el.length).toBe(2);
+ expect(el[0].value).toBe("NAME");
+ expect(el[1].value).toBe("DESCRIPTION");
+ });
+ it('Test Metadata onChange', () => {
+ const actions = {
+ onChange: () => {}
+ };
+ const spyonChange = expect.spyOn(actions, 'onChange');
+ ReactDOM.render(, document.getElementById("container"));
+ const container = document.getElementById('container');
+ const input = container.querySelector('input');
+ input.value = "test";
+ ReactTestUtils.Simulate.change(input); // <-- trigger event callback
+ expect(spyonChange).toHaveBeenCalled();
+ });
+});
diff --git a/web/client/components/dashboard/forms/__tests__/Thumbnail-test.jsx b/web/client/components/dashboard/forms/__tests__/Thumbnail-test.jsx
new file mode 100644
index 0000000000..238b5bb7e7
--- /dev/null
+++ b/web/client/components/dashboard/forms/__tests__/Thumbnail-test.jsx
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2016, 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.
+ */
+var React = require('react');
+var ReactDOM = require('react-dom');
+var Thumbnail = require('../Thumbnail.jsx');
+var expect = require('expect');
+const TestUtils = require('react-dom/test-utils');
+
+describe('This test for Thumbnail', () => {
+
+
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ // test DEFAULTS
+ it('creates the component with defaults, loading=true', () => {
+ const thumbnailItem = ReactDOM.render(, document.getElementById("container"));
+ expect(thumbnailItem).toExist();
+
+ const thumbnailItemDom = ReactDOM.findDOMNode(thumbnailItem);
+ expect(thumbnailItemDom).toExist();
+
+ expect(thumbnailItemDom.className).toBe('btn btn-info');
+ });
+
+ it('creates the component with defaults, loading=false', () => {
+ const thumbnailItem = ReactDOM.render(, document.getElementById("container"));
+ expect(thumbnailItem).toExist();
+
+ const thumbnailItemDom = ReactDOM.findDOMNode(thumbnailItem);
+ expect(thumbnailItemDom).toExist();
+
+ expect(thumbnailItemDom.className).toBe('dropzone-thumbnail-container');
+ });
+
+ it('creates the component without a thumbnail', () => {
+ let thumbnail = "http://localhost:8081/%2Fgeostore%2Frest%2Fdata%2F2214%2Fraw%3Fdecode%3Ddatauri";
+ let map = {
+ thumbnail: thumbnail,
+ id: 123,
+ canWrite: true,
+ errors: []
+ };
+ const thumbnailItem = ReactDOM.render(, document.getElementById("container"));
+ expect(thumbnailItem).toExist();
+
+ const thumbnailItemDom = ReactDOM.findDOMNode(thumbnailItem);
+ expect(thumbnailItemDom).toExist();
+
+ const content = TestUtils.findRenderedDOMComponentWithClass(thumbnailItem, 'dropzone-content-image');
+ expect(content).toExist();
+ });
+
+ it('creates the component with a thumbnail', () => {
+ let thumbnail = "http://localhost:8081/%2Fgeostore%2Frest%2Fdata%2F2214%2Fraw%3Fdecode%3Ddatauri";
+ const thumbnailItem = ReactDOM.render(, document.getElementById("container"));
+ expect(thumbnailItem).toExist();
+
+ const thumbnailItemDom = ReactDOM.findDOMNode(thumbnailItem);
+ expect(thumbnailItemDom).toExist();
+
+ const content = TestUtils.findRenderedDOMComponentWithClass(thumbnailItem, 'dropzone-content-image-added');
+ expect(content).toExist();
+ });
+
+});
diff --git a/web/client/components/dashboard/modals/Confirm.jsx b/web/client/components/dashboard/modals/Confirm.jsx
new file mode 100644
index 0000000000..eda7a1cf39
--- /dev/null
+++ b/web/client/components/dashboard/modals/Confirm.jsx
@@ -0,0 +1,93 @@
+const PropTypes = require('prop-types');
+/**
+ * Copyright 2016, 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 {Button} = require('react-bootstrap');
+const Modal = require('../../misc/Modal');
+const Spinner = require('react-spinkit');
+
+/**
+ * A Modal window to show a confirmation dialog
+ */
+class ConfirmModal extends React.Component {
+ static propTypes = {
+ // props
+ className: PropTypes.string,
+ show: PropTypes.bool,
+ options: PropTypes.object,
+ onConfirm: PropTypes.func,
+ onClose: PropTypes.func,
+ closeGlyph: PropTypes.string,
+ style: PropTypes.object,
+ buttonSize: PropTypes.string,
+ includeCloseButton: PropTypes.bool,
+ body: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
+ titleText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
+ confirmText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
+ cancelText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
+ running: PropTypes.bool
+ };
+
+ static defaultProps = {
+ onConfirm: ()=> {},
+ onClose: () => {},
+ options: {
+ animation: false
+ },
+ className: "",
+ closeGlyph: "",
+ style: {},
+ includeCloseButton: true,
+ body: "",
+ titleText: "Confirm Delete",
+ confirmText: "Delete",
+ cancelText: "Cancel"
+ };
+
+ onConfirm = () => {
+ this.props.onConfirm();
+ };
+
+ render() {
+ const footer = (
+
+ {this.props.includeCloseButton ? : }
+ );
+ const body = this.props.body;
+ return (
+
+
+ {this.props.titleText}
+
+
+ {body}
+
+
+ {footer}
+
+ );
+ }
+}
+
+module.exports = ConfirmModal;
diff --git a/web/client/components/dashboard/modals/DetailSheet.jsx b/web/client/components/dashboard/modals/DetailSheet.jsx
new file mode 100644
index 0000000000..4b9f883785
--- /dev/null
+++ b/web/client/components/dashboard/modals/DetailSheet.jsx
@@ -0,0 +1,84 @@
+/*
+ * 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 ReactQuill = require('react-quill');
+const Spinner = require('react-spinkit');
+
+const Message = require('../../I18N/Message');
+const Portal = require('../../misc/Portal');
+const ResizableModal = require('../../misc/ResizableModal');
+require('react-quill/dist/quill.snow.css');
+
+// NOTE: partial porting of details sheet from map, still to be tested and added to the save form
+module.exports = ({
+ readOnly,
+ showDetailEditor,
+ modules = {
+ toolbar: [
+ [{ 'size': ['small', false, 'large', 'huge'] }, 'bold', 'italic', 'underline', 'blockquote'],
+ [{ 'list': 'bullet' }, { 'align': [] }],
+ [{ 'color': [] }, { 'background': [] }, 'clean'], ['image', 'video', 'link']
+ ]
+ },
+ detailsText,
+ detailsBackup,
+ onSaveDetails = () => {},
+ onResetCurrentMap = () => {},
+ onBackDetails = () => {},
+ onUpdateDetails = () => {}}
+ ) => {
+ return (
+
+ {readOnly ? (
+ {
+ onResetCurrentMap();
+ }}
+ title={}
+ show
+ >
+
+
+ ) : (}
+ bodyClassName="ms-modal-quill-container"
+ size="lg"
+ clickOutEnabled={false}
+ showFullscreen
+ fullscreenType="full"
+ onClose={() => { onBackDetails(detailsBackup); }}
+ buttons={[{
+ text: ,
+ onClick: () => {
+ onBackDetails(detailsBackup);
+ }
+ }, {
+ text: ,
+ onClick: () => {
+ onSaveDetails(detailsText);
+ }
+ }]}>
+
+
{
+ if (details && details !== '
') {
+ onUpdateDetails(details, false);
+ }
+ }}
+ modules={modules} />
+
+ )}
+ );
+ };
diff --git a/web/client/components/dashboard/modals/Save.jsx b/web/client/components/dashboard/modals/Save.jsx
new file mode 100644
index 0000000000..eb2029a450
--- /dev/null
+++ b/web/client/components/dashboard/modals/Save.jsx
@@ -0,0 +1,125 @@
+/*
+* Copyright 2017, 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 PropTypes = require('prop-types');
+const React = require('react');
+const {get} = require('lodash');
+
+const Portal = require('../../misc/Portal');
+const ResizableModal = require('../../misc/ResizableModal');
+// require('./css/modals.css');
+const {Grid} = require('react-bootstrap');
+const Message = require('../../I18N/Message');
+const ErrorBox = require('./fragments/ErrorBox');
+const MainForm = require('./fragments/MainForm');
+const ruleEditor = require('./enhancers/ruleEditor');
+const PermissionEditor = ruleEditor(require('./fragments/PermissionEditor'));
+
+/**
+ * A Modal window to show map metadata form
+*/
+class SaveModal extends React.Component {
+ static propTypes = {
+ show: PropTypes.bool,
+ loading: PropTypes.bool,
+ errors: PropTypes.array,
+ rules: PropTypes.array,
+ onSave: PropTypes.func,
+ onUpdateRules: PropTypes.func,
+ closeGlyph: PropTypes.string,
+ resource: PropTypes.object,
+ linkedResources: PropTypes.object,
+ style: PropTypes.object,
+ modalSize: PropTypes.string,
+ // CALLBACKS
+ onError: PropTypes.func,
+ onUpdate: PropTypes.func,
+ onUpdateLinkedResource: PropTypes.func,
+ onClose: PropTypes.func,
+ metadataChanged: PropTypes.func,
+ availablePermissions: PropTypes.arrayOf(PropTypes.string),
+ availableGroups: PropTypes.arrayOf(PropTypes.object)
+ };
+
+ static contextTypes = {
+ messages: PropTypes.object
+ };
+
+ static defaultProps = {
+ id: "MetadataModal",
+ modalSize: "",
+ resource: {},
+ linkedResources: {},
+ onUpdateRules: ()=> {},
+ metadataChanged: ()=> {},
+ metadata: {name: "", description: ""},
+ options: {},
+ closeGlyph: "",
+ style: {},
+ // CALLBACKS
+ onError: ()=> {},
+ onUpdate: ()=> {},
+ onUpdateLinkedResource: () => {},
+ onSaveAll: () => {},
+ onSave: ()=> {},
+ onReset: () => {},
+ availablePermissions: ["canRead", "canWrite"],
+ availableGroups: []
+ };
+ onCloseMapPropertiesModal = () => {
+ this.props.onClose();
+ }
+
+ onSave = () => {
+ this.props.onSave({...this.props.resource, permission: this.props.rules});
+ };
+
+ /**
+ * @return the modal for unsaved changes
+ */
+ render() {
+ return (
+ {}
+ show={this.props.show}
+ clickOutEnabled
+ bodyClassName="ms-flex modal-properties-container"
+ buttons={[{
+ text: ,
+ onClick: this.onCloseMapPropertiesModal,
+ disabled: this.props.resource.loading
+ }, {
+ text: ,
+ onClick: () => { this.onSave(); },
+ disabled: !this.isValidForm() || this.props.loading
+ }]}
+ showClose={!this.props.resource.loading}
+ onClose={this.onCloseMapPropertiesModal}>
+
+
+ }
+ );
+ }
+ isValidForm = () => get(this.props.resource, "metadata.name");
+}
+
+module.exports = SaveModal;
diff --git a/web/client/components/dashboard/modals/__tests__/Confirm-test.jsx b/web/client/components/dashboard/modals/__tests__/Confirm-test.jsx
new file mode 100644
index 0000000000..742504b20c
--- /dev/null
+++ b/web/client/components/dashboard/modals/__tests__/Confirm-test.jsx
@@ -0,0 +1,34 @@
+var expect = require('expect');
+var React = require('react');
+var ReactDOM = require('react-dom');
+var ConfirmDialog = require('../Confirm');
+
+describe("ConfirmDialog component", () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('creates component with defaults', () => {
+ const cmp = ReactDOM.render(, document.getElementById("container"));
+ expect(cmp).toExist();
+ });
+
+ it('creates component with content', () => {
+ const cmp = ReactDOM.render(some content
, document.getElementById("container"));
+ expect(cmp).toExist();
+
+ let background = document.getElementsByClassName("modal").item(0);
+ let dialog = document.getElementsByClassName("modal-dialog").item(0);
+ expect(background).toExist();
+ expect(dialog).toExist();
+ expect(document.querySelectorAll('button').length).toBe(3); // close, confirm, cancel
+ });
+
+});
diff --git a/web/client/components/dashboard/modals/__tests__/Save-test.jsx b/web/client/components/dashboard/modals/__tests__/Save-test.jsx
new file mode 100644
index 0000000000..cacc7bacc0
--- /dev/null
+++ b/web/client/components/dashboard/modals/__tests__/Save-test.jsx
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2016, 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.
+ */
+var React = require('react');
+var ReactDOM = require('react-dom');
+var MetadataModal = require('../Save.jsx');
+var expect = require('expect');
+
+describe('This test for dashboard save form', () => {
+
+
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ // test DEFAULTS
+ it('creates the component with defaults, show=false', () => {
+ const metadataModalItem = ReactDOM.render(, document.getElementById("container"));
+ expect(metadataModalItem).toExist();
+
+ const metadataModalItemDom = ReactDOM.findDOMNode(metadataModalItem);
+ expect(metadataModalItemDom).toNotExist();
+
+ const getModals = function() {
+ return document.getElementsByTagName("body")[0].getElementsByClassName('modal-dialog');
+ };
+ expect(getModals().length).toBe(0);
+
+ });
+
+
+ it('creates the component with a format error', () => {
+ const metadataModalItem = ReactDOM.render(, document.getElementById("container"));
+ expect(metadataModalItem).toExist();
+
+ const getModals = function() {
+ return document.getElementsByTagName("body")[0].getElementsByClassName('modal-dialog');
+ };
+
+ expect(getModals().length).toBe(1);
+
+ const modalDivList = document.getElementsByClassName("modal-content");
+ const closeBtnList = modalDivList.item(0).querySelectorAll('.modal-footer button');
+
+ expect(closeBtnList.length).toBe(2);
+
+ const errorFORMAT = modalDivList.item(0).querySelector('.errorFORMAT');
+ expect(errorFORMAT).toExist();
+ });
+
+ it('creates the component with a size error', () => {
+ const metadataModalItem = ReactDOM.render(, document.getElementById("container"));
+ expect(metadataModalItem).toExist();
+
+ const getModals = function() {
+ return document.getElementsByTagName("body")[0].getElementsByClassName('modal-dialog');
+ };
+
+ expect(getModals().length).toBe(1);
+
+ const modalDivList = document.getElementsByClassName("modal-content");
+ const closeBtnList = modalDivList.item(0).querySelectorAll('.modal-footer button');
+ expect(closeBtnList.length).toBe(2);
+
+ const errorSIZE = modalDivList.item(0).querySelector('.errorSIZE');
+ expect(errorSIZE).toExist();
+ });
+
+});
diff --git a/web/client/components/dashboard/modals/enhancers/__tests__/handlePermission-test.jsx b/web/client/components/dashboard/modals/enhancers/__tests__/handlePermission-test.jsx
new file mode 100644
index 0000000000..169c45d52e
--- /dev/null
+++ b/web/client/components/dashboard/modals/enhancers/__tests__/handlePermission-test.jsx
@@ -0,0 +1,71 @@
+/*
+ * 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 { createSink, setObservableConfig } = require('recompose');
+const rxjsConfig = require('recompose/rxjsObservableConfig').default;
+setObservableConfig(rxjsConfig);
+const expect = require('expect');
+const { Promise } = require('es6-promise');
+
+
+const handlePermission = require('../handlePermission');
+
+describe('handlePermission enhancer', () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+ it('handlePermission rendering with defaults', (done) => {
+ const DUMMY_API = {
+ getAvailableGroups: () => {
+ return new Promise(resolve => resolve([]));
+ }
+ };
+ const Sink = handlePermission(DUMMY_API)(createSink( props => {
+ if (props.availableGroups) {
+ done();
+ }
+ }));
+ ReactDOM.render(, document.getElementById("container"));
+ });
+ it('Test Sink callback', (done) => {
+ const DUMMY_API = {
+ getAvailableGroups: () => {
+ return new Promise(resolve => resolve([]));
+ },
+ getResourcePermissions: () => {
+ const result = [
+ {
+ "canRead": true,
+ "canWrite": true,
+ "user": {
+ "id": 534,
+ "name": "testuser"
+ }
+
+ }];
+
+ return new Promise(resolve => resolve(result));
+ }
+ };
+
+ const Sink = handlePermission(DUMMY_API)(createSink(props => {
+ if (props.rules && props.rules.length > 0) {
+ expect(props.rules.length).toBe(1);
+ done();
+ }
+ }));
+ ReactDOM.render(, document.getElementById("container"));
+ });
+});
diff --git a/web/client/components/dashboard/modals/enhancers/__tests__/handleResourceData-test.jsx b/web/client/components/dashboard/modals/enhancers/__tests__/handleResourceData-test.jsx
new file mode 100644
index 0000000000..d48a15f6cb
--- /dev/null
+++ b/web/client/components/dashboard/modals/enhancers/__tests__/handleResourceData-test.jsx
@@ -0,0 +1,82 @@
+/*
+ * 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 {createSink} = require('recompose');
+const expect = require('expect');
+const handleResourceData = require('../handleResourceData');
+
+describe('handleResourceData enhancer', () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+ it('handleResourceData rendering with defaults', (done) => {
+ const Sink = handleResourceData(createSink( props => {
+ expect(props.onSave).toExist();
+ done();
+ }));
+ ReactDOM.render(, document.getElementById("container"));
+ });
+ it('handleResourceData onUpdate', (done) => {
+ const Sink = handleResourceData(createSink( props => {
+ expect(props).toExist();
+ expect(props.onUpdate).toExist();
+ expect(props.metadata.name).toBe("TEST");
+ if (props.resource.metadata.name === "TEST") {
+ props.onUpdate("metadata.name", "NEW_VALUE");
+ } else {
+ expect(props.resource.metadata.name).toBe("NEW_VALUE");
+ done();
+ }
+
+ }));
+ ReactDOM.render(, document.getElementById("container"));
+ });
+ it('handleResourceData onUpdateLinkedResource', (done) => {
+ const Sink = handleResourceData(createSink(props => {
+ expect(props).toExist();
+ expect(props.onUpdateLinkedResource).toExist();
+ expect(props.metadata.name).toBe("TEST");
+ if (!props.linkedResources) {
+ props.onUpdateLinkedResource("thumbnail", "DATA", "CATEGORY", {tail: "TEST"});
+ } else {
+ const thumb = props.linkedResources.thumbnail;
+ expect(thumb.category).toExist("CATEGORY");
+ expect(thumb.data).toExist("DATA");
+ expect(thumb.tail).toBe("TEST");
+ done();
+ }
+
+ }));
+ ReactDOM.render(, document.getElementById("container"));
+ });
+ it('handleResourceData confirm dialog', (done) => {
+ const Sink = handleResourceData(createSink(props => {
+ expect(props).toExist();
+ expect(props.onClose).toExist();
+ if (props.resource.metadata.name === "TEST") {
+ props.onUpdate("metadata.name", "TEST");
+ props.onClose();
+ setTimeout(() => {
+ const m = document.querySelector('.modal-dialog');
+ expect(m).toExist();
+ done();
+ }, 10);
+ }
+
+
+ }));
+ ReactDOM.render(, document.getElementById("container"));
+ });
+});
diff --git a/web/client/components/dashboard/modals/enhancers/handlePermission.jsx b/web/client/components/dashboard/modals/enhancers/handlePermission.jsx
new file mode 100644
index 0000000000..737c8277c8
--- /dev/null
+++ b/web/client/components/dashboard/modals/enhancers/handlePermission.jsx
@@ -0,0 +1,76 @@
+/*
+ * 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 Rx = require('rxjs');
+const { mapPropsStream, compose, withStateHandlers } = require('recompose');
+const GeoStoreDAO = require('../../../../api/GeoStoreDAO');
+
+/**
+ * retrieves groups for permission handling and returns as props
+ * @param {Object} API the API to use
+ */
+const retrieveGroups = (API) =>
+ mapPropsStream(props$ =>
+ props$.combineLatest(
+ props$
+ .take(1)
+ .switchMap(({ user }) =>
+ Rx.Observable.defer(() => API.getAvailableGroups(user))
+ .map(availableGroups => ({ availableGroups }))
+ .startWith({ loading: true })
+ )
+ .startWith({})
+ .catch( () => Rx.Observable.of({})),
+ (props, overrides) => ({
+ ...props,
+ ...overrides
+ })
+ )
+);
+
+/**
+ * retrieves permission for the resource
+ * @param {object} API the API to use
+ */
+const retrievePermission = (API) =>
+ mapPropsStream(props$ =>
+ props$.combineLatest(
+ props$
+ // trigger when resource changes
+ .distinctUntilKeyChanged('resource')
+ .pluck('resource')
+ .filter(resource => resource.id)
+ .pluck('id')
+ .distinctUntilChanged()
+ .switchMap(id =>
+ Rx.Observable.defer(() => API.getResourcePermissions(id))
+ .map(rules => ({ rules }))
+ .startWith({ loading: true })
+ )
+ .startWith({})
+ .catch(() => Rx.Observable.of({})),
+ (props, overrides) => ({
+ ...props,
+ ...overrides
+ })
+ )
+ );
+const manageLocalPermissionChanges = withStateHandlers(
+ () => ({}),
+ {
+ onUpdateRules: () => (rules) => ({
+ rules: rules
+ })
+ }
+);
+
+module.exports = ( API = GeoStoreDAO ) => compose(
+ retrieveGroups(API),
+ retrievePermission(API),
+ manageLocalPermissionChanges
+);
+
diff --git a/web/client/components/dashboard/modals/enhancers/handleResourceData.jsx b/web/client/components/dashboard/modals/enhancers/handleResourceData.jsx
new file mode 100644
index 0000000000..cf2d9e2950
--- /dev/null
+++ b/web/client/components/dashboard/modals/enhancers/handleResourceData.jsx
@@ -0,0 +1,81 @@
+/*
+ * 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 { compose, withStateHandlers, withState, branch, withHandlers, renderComponent} = require('recompose');
+const {set} = require('../../../../utils/ImmutableUtils');
+const Message = require('../../../I18N/Message');
+const ConfirmDialog = require('../Confirm');
+
+/**
+ * Enhancer to manage resource data for a Save dialog.
+ * Stores the original data to handle changes.
+ */
+module.exports = compose(
+ withStateHandlers(
+ ({resource = {}}) => ({
+ originalData: resource,
+ metadata: {
+ name: resource.name,
+ description: resource.description
+ },
+ resource: {
+ id: resource.id,
+ attributes: resource.attributes,
+ metadata: {
+ name: resource.name,
+ description: resource.description
+ }
+ }
+
+ }),
+ {
+ onUpdate: ({resource}) => (key, value) => ({
+ hasChanges: true,
+ resource: set(key, value, resource)
+ }),
+ onUpdateLinkedResource: ({ linkedResources = {} }) => (key, data, category, options = {}) => ({
+ linkedResources: set(key, {
+ category,
+ data,
+ ...options
+ }, linkedResources)
+ })
+ }
+ ),
+ withState('confirmClose', 'onCloseConfirm', false),
+ branch(
+ ({ confirmClose }) => confirmClose,
+ renderComponent(({ onCloseConfirm, onClose }) =>
+ (}
+ cancelText={}
+ onConfirm={() => onClose()}
+ onClose={() => onCloseConfirm(false)}
+ body={
}
+ >))
+ ),
+ withHandlers({
+ onClose: ({
+ hasChanges,
+ onClose = () => {},
+ onCloseConfirm = () => {}}
+ ) => () =>
+ hasChanges
+ ? onCloseConfirm(true)
+ : onClose()
+ }),
+ withHandlers({
+ onSave: ({onSave = () => {}, category = "DASHBOARD", data, linkedResources}) => resource => onSave({
+ category,
+ linkedResources,
+ data,
+ ...resource
+ })
+ })
+);
diff --git a/web/client/components/dashboard/modals/enhancers/ruleEditor.js b/web/client/components/dashboard/modals/enhancers/ruleEditor.js
new file mode 100644
index 0000000000..d363b4d72e
--- /dev/null
+++ b/web/client/components/dashboard/modals/enhancers/ruleEditor.js
@@ -0,0 +1,15 @@
+/*
+ * 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 {compose, withState} = require('recompose');
+
+
+module.exports = compose(
+ withState('newGroup', 'onNewGroupChoose'),
+ withState('newPermission', 'onNewPermissionChoose')
+);
diff --git a/web/client/components/dashboard/modals/fragments/DetailsRow.jsx b/web/client/components/dashboard/modals/fragments/DetailsRow.jsx
new file mode 100644
index 0000000000..bdf8bff354
--- /dev/null
+++ b/web/client/components/dashboard/modals/fragments/DetailsRow.jsx
@@ -0,0 +1,87 @@
+const React = require('react');
+const {Row, Col} = require('react-bootstrap');
+const Spinner = require('react-spinkit');
+const { isNil } = require('lodash');
+const Toolbar = require('../../misc/toolbar/Toolbar');
+
+const Message = require('../../I18N/Message');
+const { NO_DETAILS_AVAILABLE } = require('../../../actions/maps');
+
+
+module.exports = ({
+ saving,
+ hideGroupProperties,
+ editDetailsDisabled,
+ detailsText,
+ detailsBackup,
+ onToggleGroupProperties = () => { },
+ onUndoDetails = () => { },
+ onToggleDetailsSheet = () => { },
+ onUpdateDetails = () => { }
+}) => {
+ return (
+
+
+
+
+
+ {detailsText === "" ? : }
+
+
+
+
+
+ {saving ? : null}
+ {isNil(detailsText) ? : { onToggleGroupProperties(); },
+ disabled: saving,
+ tooltipId: !hideGroupProperties ? "map.details.showPreview" : "map.details.hidePreview"
+ }, {
+ glyph: 'undo',
+ tooltipId: "map.details.undo",
+ visible: !!detailsBackup,
+ onClick: () => { onUndoDetails(detailsBackup); },
+ disabled: saving
+ }, {
+ glyph: 'pencil-add',
+ tooltipId: "map.details.add",
+ visible: !detailsText,
+ onClick: () => {
+ onToggleDetailsSheet(false);
+ },
+ disabled: saving
+ }, {
+ glyph: 'pencil',
+ tooltipId: "map.details.edit",
+ visible: !!detailsText && !editDetailsDisabled,
+ onClick: () => {
+ onToggleDetailsSheet(false);
+ if (detailsText) {
+ onUpdateDetails(detailsText, true);
+ }
+ },
+ disabled: saving
+ }, {
+ glyph: 'trash',
+ tooltipId: "map.details.delete",
+ visible: !!detailsText,
+ onClick: () => { this.props.detailsSheetActions.onDeleteDetails(); },
+ disabled: saving
+ }]} />}
+
+
+
+
+
+ {detailsText &&
+ {detailsText !== NO_DETAILS_AVAILABLE ?
+ :
}
+
}
+
+ );
+};
diff --git a/web/client/components/dashboard/modals/fragments/ErrorBox.jsx b/web/client/components/dashboard/modals/fragments/ErrorBox.jsx
new file mode 100644
index 0000000000..495285e72b
--- /dev/null
+++ b/web/client/components/dashboard/modals/fragments/ErrorBox.jsx
@@ -0,0 +1,33 @@
+/*
+ * 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 DEFAULT_MESSAGES = { "FORMAT": "map.errorFormat", "SIZE": "map.errorSize", 409: "dashboard.errors.resourceAlreadyExists"};
+
+const Message = require('../../../I18N/Message');
+const { Row } = require('react-bootstrap');
+const errorString = err => typeof err === 'string' ? err : err.statusText;
+const errorCode = err => typeof err === 'string' ? err : err.status;
+const errorData = err => typeof err === 'string' ? undefined : err;
+const errorMessage = error => {
+ const code = errorCode(error);
+ return ;
+};
+
+module.exports = ({ errors = []}) => {
+ return (
+ {errors.length > 0 ?
+
+ {errors.map(error =>
+ (
+ {errorMessage(error)}
+
))}
+
+ : null}
+
);
+};
diff --git a/web/client/components/dashboard/modals/fragments/MainForm.jsx b/web/client/components/dashboard/modals/fragments/MainForm.jsx
new file mode 100644
index 0000000000..058410026a
--- /dev/null
+++ b/web/client/components/dashboard/modals/fragments/MainForm.jsx
@@ -0,0 +1,54 @@
+/*
+ * 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 Message = require('../../../I18N/Message');
+const {Row, Col} = require('react-bootstrap');
+const Metadata = require('../../forms/Metadata');
+const Thumbnail = require('../../forms/Thumbnail');
+
+module.exports = class MainForm extends React.Component {
+ render() {
+ const {
+ resource,
+ linkedResources={},
+ onError = () => { },
+ onUpdate = () => { },
+ onUpdateLinkedResource = () => { }
+ } = this.props;
+ return (
+
+ onUpdateLinkedResource("thumbnail", "NODATA", "THUMBNAIL", {
+ tail: '/raw?decode=datauri'
+ })}
+ onUpdate={(data) => onUpdateLinkedResource("thumbnail", data, "THUMBNAIL", {
+ tail: '/raw?decode=datauri'
+ })} />
+
+
+ }
+ descriptionFieldText={}
+ namePlaceholderText={"dashboard.saveDialog.namePlaceholder"}
+ descriptionPlaceholderText={"dashboard.saveDialog.descriptionPlaceholder"}
+ />
+
+
);
+ }
+};
+
+
diff --git a/web/client/components/dashboard/modals/fragments/PermissionEditor.jsx b/web/client/components/dashboard/modals/fragments/PermissionEditor.jsx
new file mode 100644
index 0000000000..380d05105e
--- /dev/null
+++ b/web/client/components/dashboard/modals/fragments/PermissionEditor.jsx
@@ -0,0 +1,238 @@
+/**
+* Copyright 2016, 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 PropTypes = require('prop-types');
+const assign = require('object-assign');
+const _ = require('lodash');
+const Select = require('react-select');
+const Spinner = require('react-spinkit');
+const { Table, Button, Glyphicon } = require('react-bootstrap');
+const Message = require('../../../I18N/Message');
+const LocaleUtils = require('../../../../utils/LocaleUtils');
+
+require('react-select/dist/react-select.css');
+
+class PermissionEditor extends React.Component {
+ static propTypes = {
+ // props
+ id: PropTypes.string,
+ user: PropTypes.object,
+ loading: PropTypes.bool,
+ onUpdateRules: PropTypes.func,
+ buttonSize: PropTypes.string,
+ disabled: PropTypes.bool,
+ style: PropTypes.object,
+ fluid: PropTypes.bool,
+ // CALLBACKS
+ onErrorCurrentMap: PropTypes.func,
+ onUpdateCurrentMap: PropTypes.func,
+ onNewGroupChoose: PropTypes.func,
+ onNewPermissionChoose: PropTypes.func,
+ availablePermissions: PropTypes.arrayOf(PropTypes.string),
+ availableGroups: PropTypes.arrayOf(PropTypes.object),
+ updatePermissions: PropTypes.func,
+ rules: PropTypes.arrayOf(PropTypes.object),
+ newGroup: PropTypes.object,
+ newPermission: PropTypes.string
+ };
+
+ static contextTypes = {
+ messages: PropTypes.object
+ };
+
+ static defaultProps = {
+ disabled: false,
+ id: "PermissionEditor",
+ onUpdateRules: () => { },
+ onNewGroupChoose: () => { },
+ onNewPermissionChoose: () => { },
+ user: {
+ name: "Guest"
+ },
+ style: {},
+ buttonSize: "small",
+ // CALLBACKS
+ onErrorCurrentMap: () => { },
+ onUpdateCurrentMap: () => { },
+ availablePermissions: ["canRead", "canWrite"],
+ availableGroups: [],
+ updatePermissions: () => { },
+ rules: []
+ };
+
+ onGroupChange = (selected) => {
+ // TODO: use _.find(this.props.availableGroups,['id', _.toInteger(id)]) when lodash will be updated to version 4
+ this.props.onNewGroupChoose(_.find(this.props.availableGroups, (o) => o.id === selected.value));
+ };
+
+ onAddPermission = () => {
+ // Check if the new permission will edit ad existing one
+ if (this.isPermissionPresent(this.props.newGroup.groupName)) {
+ this.props.onUpdateRules(this.props.rules.map(
+ (rule) => {
+ if (rule.group && rule.group.groupName === this.props.newGroup.groupName) {
+ if (this.props.newPermission === "canWrite") {
+ return assign({}, rule, { canRead: true, canWrite: true });
+ }
+ return assign({}, rule, { canRead: true, canWrite: false });
+ }
+ return rule;
+ }, this
+ ).filter(rule => rule.canRead || rule.canWrite));
+
+ } else {
+ this.props.onUpdateRules(this.props.rules.concat([{
+ canRead: true,
+ canWrite: this.props.newPermission === "canWrite",
+ group: this.props.newGroup
+ }]));
+ }
+ };
+
+ onChangePermission = (groupName, input) => {
+ this.props.onUpdateRules(this.props.rules
+ // remove items when delete
+ .filter(rule => !(input === 'delete') || !(rule.group) || !(rule.group.groupName === groupName))
+ // change rules
+ .map(
+ (rule) => {
+ if (rule.group && rule.group.groupName === groupName) {
+ if (input === "canWrite") {
+ return assign({}, rule, { canRead: true, canWrite: true });
+ } else if (input === "canRead") {
+ return assign({}, rule, { canRead: true, canWrite: false });
+ }
+ return assign({}, rule, { canRead: false, canWrite: false });
+ }
+ return rule;
+ }
+ ).filter(rule => rule.canRead || rule.canWrite));
+ };
+
+ getSelectableGroups = () => {
+ return this.props.availableGroups && this.props.availableGroups.filter((group) => {
+ return !this.isPermissionPresent(group.groupName);
+ }).map((group) => ({ label: group.groupName, value: group.id }));
+ };
+
+ getPermissionLabel = (perm) => {
+ switch (perm) {
+ case "canRead":
+ return LocaleUtils.getMessageById(this.context.messages, "map.permissions.canView");
+ case "canWrite":
+ return LocaleUtils.getMessageById(this.context.messages, "map.permissions.canWrite");
+ default:
+ return perm;
+ }
+ };
+
+ getAvailablePermissions = () => {
+ return this.props.availablePermissions.map((perm) => ({ value: perm, label: this.getPermissionLabel(perm) }));
+ };
+
+ renderPermissionRows = () => {
+ if (this.props.rules.length === 0) {
+ return |
;
+ }
+ return this.props.rules
+ .filter(rule => rule.group)
+ .map(({canWrite, group}, index) => {
+ return (
+
+ {group.groupName} |
+
+ |
+ {
+ // | TODO: Add a Group Editor
+ }
+ {
+ //
+ }
+
+ |
+
+ );
+ });
+ }
+ render() {
+
+ return (
+
+
+
+
+ |
+
+
+
+ {this.props.loading ?
+
|
+ : this.renderPermissionRows()}
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+
+
+ );
+ }
+
+ disablePermission(a, b) {
+ return a || !b;
+ }
+ isPermissionPresent = (group) => {
+ return this.props.rules && _.findIndex(this.props.rules, (o) => o.group && o.group.groupName === group) >= 0;
+ };
+}
+
+module.exports = PermissionEditor;
diff --git a/web/client/epics/dashboard.js b/web/client/epics/dashboard.js
index 4d8cdf3f83..d8da36ad1f 100644
--- a/web/client/epics/dashboard.js
+++ b/web/client/epics/dashboard.js
@@ -11,7 +11,15 @@ const {
editNewWidget, onEditorChange
} = require('../actions/widgets');
const {
- setEditing
+ setEditing,
+ dashboardSaved,
+ dashboardLoaded,
+ dashboardLoading,
+ triggerSave,
+ loadDashboard,
+ dashboardSaveError,
+ SAVE_DASHBOARD,
+ LOAD_DASHBOARD
} = require('../actions/dashboard');
const {
setControlProperty,
@@ -20,20 +28,39 @@ const {
const {
featureTypeSelected
} = require('../actions/wfsquery');
+const {
+ show,
+ error
+} = require('../actions/notifications');
const {
loadFilter,
QUERY_FORM_SEARCH
} = require('../actions/queryform');
+const {
+ LOGIN_SUCCESS
+} = require('../actions/security');
const {
isDashboardEditing,
isDashboardAvailable = () => true
} = require('../selectors/dashboard');
+
+const {
+ isLoggedIn
+} = require('../selectors/security');
const {
getEditingWidgetLayer,
getEditingWidgetFilter
} = require('../selectors/widgets');
-const {LOCATION_CHANGE} = require('react-router-redux');
+const {
+ createResource,
+ updateResource,
+ getResource
+} = require('../observables/geostore');
+const {
+ wrapStartStop
+} = require('../observables/epics');
+const { LOCATION_CHANGE, push} = require('react-router-redux');
const getFTSelectedArgs = (state) => {
let layer = getEditingWidgetLayer(state);
let url = layer.search && layer.search.url;
@@ -42,14 +69,19 @@ const getFTSelectedArgs = (state) => {
};
module.exports = {
+
+ // Basic interactions with dashboard editor
openDashboardWidgetEditor: (action$, {getState = () => {}} = {}) => action$.ofType(NEW, EDIT)
.filter( () => isDashboardAvailable(getState()))
.switchMap(() => Rx.Observable.of(
setEditing(true)
)),
+ // Basic interactions with dashboard editor
closeDashboardWidgetEditorOnFinish: (action$, {getState = () => {}} = {}) => action$.ofType(INSERT)
.filter( () => isDashboardAvailable(getState()))
.switchMap(() => Rx.Observable.of(setEditing(false))),
+
+ // Basic interactions with dashboard editor
initDashboardEditorOnNew: (action$, {getState = () => {}} = {}) => action$.ofType(NEW)
.filter( () => isDashboardAvailable(getState()))
.switchMap((w) => Rx.Observable.of(editNewWidget({
@@ -59,6 +91,7 @@ module.exports = {
// override action's type
type: undefined
}, {step: 0}))),
+ // Basic interactions with dashboard editor
closeDashboardEditorOnExit: (action$, {getState = () => {}} = {}) => action$.ofType(LOCATION_CHANGE)
.filter( () => isDashboardAvailable(getState()))
.filter( () => isDashboardEditing(getState()) )
@@ -99,6 +132,71 @@ module.exports = {
setControlProperty('queryPanel', "enabled", false)
)
)
- )
-
+ ),
+ // dashboard loading from resource ID.
+ loadDashboardStream: (action$, {getState = () => {}}) => action$
+ .ofType(LOAD_DASHBOARD)
+ .switchMap( ({id}) =>
+ getResource(id)
+ .map(({ data, ...resource }) => dashboardLoaded(resource, data))
+ .let(wrapStartStop(
+ dashboardLoading(true, "loading"),
+ dashboardLoading(false, "loading"),
+ e => {
+ if (e.status === 403 ) {
+ if ( isLoggedIn(getState())) {
+ return Rx.Observable.of(error({
+ title: "dashboard.errors.loading.title",
+ message: "dashboard.errors.loading.dashboardNotAccessible"
+ }));
+ }
+ 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",
+ message: "dashboard.errors.loading.dashboardDoesNotExist"
+ }));
+ }
+ return Rx.Observable.of(error({
+ title: "dashboard.errors.loading.title",
+ message: "dashboard.errors.loading.unknownError"
+ }));
+ }
+ ))
+ ),
+ // saving dashboard flow (both creation and update)
+ saveDashboard: action$ => action$
+ .ofType(SAVE_DASHBOARD)
+ .exhaustMap(({resource} = {}) =>
+ (!resource.id ? createResource(resource) : updateResource(resource))
+ .switchMap(rid => Rx.Observable.of(
+ dashboardSaved(rid),
+ triggerSave(false),
+ !resource.id
+ ? push(`/dashboard/${rid}`)
+ : loadDashboard(rid),
+ ).merge(
+ Rx.Observable.of(show({
+ id: "DASHBOARD_SAVE_SUCCESS",
+ title: "dashboard.saveDialog.saveSuccessTitle",
+ message: "dashboard.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"))
+ )
+ )
};
diff --git a/web/client/epics/widgets.js b/web/client/epics/widgets.js
index e3ae167ef5..2f646d2165 100644
--- a/web/client/epics/widgets.js
+++ b/web/client/epics/widgets.js
@@ -7,6 +7,7 @@ const {
} = require('../actions/config');
const { availableDependenciesSelector, isWidgetSelectionActive, getDependencySelectorConfig } = require('../selectors/widgets');
const { MAP_CREATED, SAVING_MAP, MAP_ERROR } = require('../actions/maps');
+const { DASHBOARD_LOADED } = require('../actions/dashboard');
const {LOCATION_CHANGE} = require('react-router-redux');
const {saveAs} = require('file-saver');
const FileUtils = require('../utils/FileUtils');
@@ -71,7 +72,7 @@ module.exports = {
* Then re-configures the dependencies to it.
*/
alignDependenciesToWidgets: (action$, { getState = () => { } } = {}) =>
- action$.ofType(MAP_CONFIG_LOADED, INSERT)
+ action$.ofType(MAP_CONFIG_LOADED, DASHBOARD_LOADED, INSERT)
.map(() => availableDependenciesSelector(getState()))
.pluck('availableDependencies')
.distinctUntilChanged( (oldMaps = [], newMaps = []) => isEqual([...oldMaps], [...newMaps]))
diff --git a/web/client/localConfig.json b/web/client/localConfig.json
index 05db76b800..80c923fc1d 100644
--- a/web/client/localConfig.json
+++ b/web/client/localConfig.json
@@ -509,7 +509,7 @@
"cfg": {
"containerPosition": "columns"
}
- }, "Dashboard"],
- "manager": ["Header", "Redirect", "UserManager", "GroupManager", "Home", "Manager", "Footer"]
+ }, "Dashboard", "Notifications"],
+ "manager": ["Header", "Redirect", "Manager", "Home", "UserManager", "GroupManager", "Footer"]
}
}
diff --git a/web/client/observables/epics.js b/web/client/observables/epics.js
new file mode 100644
index 0000000000..400475622b
--- /dev/null
+++ b/web/client/observables/epics.js
@@ -0,0 +1,38 @@
+/*
+ * 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 Rx = require('rxjs');
+const {castArray} = require('lodash');
+const start = (stream$, actions = []) => stream$
+ .startWith(...actions);
+/**
+ * wraps an epic with start/stop action. Useful shortcut for loading actions.
+ * Accepts also an exception stream, that gets error before to emit loading stop.
+ * @memberof observables.epics
+ * @param {object|object[]} startAction start action(s)
+ * @param {object|object[]} endAction end action(s)
+ * @param {Observable} exceptionStream$ an optional stream for exception.
+ */
+const wrapStartStop = (startAction, endAction, exceptionStream$) => stream$ =>
+(exceptionStream$ ?
+ start(stream$, castArray(startAction))
+ .catch(exceptionStream$)
+ : start(stream$, castArray(startAction))
+).concat(
+ Rx.Observable.from(castArray(endAction))
+);
+
+
+/**
+ * Utility stream manipulation for epics
+ * @module observables.epics
+ */
+
+module.exports = {
+ wrapStartStop
+};
diff --git a/web/client/observables/geostore.js b/web/client/observables/geostore.js
new file mode 100644
index 0000000000..c2f10b9ec5
--- /dev/null
+++ b/web/client/observables/geostore.js
@@ -0,0 +1,261 @@
+/*
+ * 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 Rx = require('rxjs');
+const uuid = require('uuid/v1');
+const { includes } = require('lodash');
+const GeoStoreDAO = require('../api/GeoStoreDAO');
+
+const createLinkedResourceURL = (id, tail = "") => encodeURIComponent(encodeURIComponent(`rest/geostore/data/${id}${tail}`));
+const LINKED_RESOURCE_REGEX = /rest\/geostore\/data\/(\d+)/;
+const getResourceIdFromURL = path => {
+ const decodedUrl = decodeURIComponent(decodeURIComponent(path));
+ const res = LINKED_RESOURCE_REGEX.exec(decodedUrl);
+ return res && !!res[0] && res[1];
+};
+/**
+ * Merges the permission. Now conflicts are not possible (default permission are for users, configured for groups)
+ * For the future we should remove duplicates.
+ * @param {array} p1 permission
+ * @param {array} p2 permission
+ */
+const mergePermission = (p1 = [], p2 = []) => p1.concat(p2);
+/**
+ *
+ * @param {number} id the id of the resource
+ * @param {array} permission the permission to assign
+ * @param {*} API
+ */
+const updateResourcePermissions = (id, permission, API = GeoStoreDAO) =>
+ permission
+ ? Rx.Observable.defer( () => API.updateResourcePermissions(id, {
+ SecurityRuleList: {
+ SecurityRule: permission
+ }
+ }))
+ : Rx.Observable.empty()
+;
+/**
+ * If the resource is "NODATA", the resource will be deleted and the attribute link will contain NODATA.
+ * This is the current convention used for maps. Gor the future, when geostore implements API to delete attribute,
+ * we could improve this procedure to simply delete the attribute.
+ * @param {number} id the id of the original resource
+ * @param {string} attributeName name of the linked resource
+ * @param {object} linkedResource the linked resource to update
+ * @param {number} resourceId the id of the linked resource
+ * @param {array} permissions to assign
+ * @param {object} API the API to use
+ */
+const updateOrDeleteLinkedResource = (id, attributeName, linkedResource = {}, resourceId, permission, API) =>
+ linkedResource.data === "NODATA"
+ // cancellation flow
+ ? Rx.Observable.fromPromise(API.deleteResource(resourceId))
+ // even if the resource is not present(i.e. due to a previous cancellation not completed for network errors)
+ // continue setting the attribute to NODATA
+ .catch(() => Rx.Observable.of("DUMMY"))
+ .switchMap( () => Rx.Observable.fromPromise( API.updateResourceAttribute(id, attributeName, "NODATA")))
+ // update flow.
+ : Rx.Observable.forkJoin([
+ API.putResource(resourceId, linkedResource.data),
+ updateResourcePermissions(resourceId, permission, API)
+ ]);
+
+
+/**
+ * Creates a resource on GeoStore and link it to the current resource. Can be used for thumbnails, details and so on
+ * @param {number} id the id of the resource
+ * @param {string} attributeName the name of the attribute to link
+ * @param {object} linkedResource the resource object of the resource to link. This resource have a tail option that can be used to add options URL of the link
+ * @param {object} API the API to use (default GeoStoreDAO)
+ */
+const createLinkedResource = (id, attributeName, linkedResource, permission, API = GeoStoreDAO) =>
+ Rx.Observable.defer(() =>
+ API.createResource({
+ name: `${id}-${attributeName}-${uuid()}`
+ },
+ linkedResource.data,
+ linkedResource.category
+ ))
+ .pluck('data')
+ .switchMap( (linkedResourceId) =>
+ Rx.Observable.forkJoin([
+ // update URL of the main resource
+ Rx.Observable.defer( () => API.updateResourceAttribute(id, attributeName, createLinkedResourceURL(linkedResourceId, linkedResource.tail))),
+ // set permission
+ updateResourcePermissions(linkedResourceId, permission, API)
+
+ ]).map(() => linkedResourceId)
+ );
+
+/**
+ * Updates a linked resource. Check if the resource already exists as attribute.
+ * If exists it will simply update the resource, otherwise the resource will be created and the attribute that link the resource updated
+ *
+ * @param {number} id the id of the resource
+ * @param {string} attributeName the name of the attribute to link
+ * @param {object} linkedResource the resource object of the resource to link. This resource have a tail option that can be used to add options URL of the link
+ * @param {array} permission permission to assign
+ * @param {object} API the API to use (default GeoStoreDAO)
+ * @return Observable
+ */
+const updateLinkedResource = (id, attributeName, linkedResource, permission, API = GeoStoreDAO) =>
+ Rx.Observable.defer(
+ () => API.getResourceAttribute(id, attributeName)
+ ).pluck('data')
+ .switchMap(
+ attributeValue => getResourceIdFromURL(attributeValue)
+ ? updateOrDeleteLinkedResource(id, attributeName, linkedResource, getResourceIdFromURL(attributeValue), permission, API)
+ : createLinkedResource(id, attributeName, linkedResource, permission, API)
+ ).catch(
+ /* if the attribute doesn't exists or if the linked resource update gave an error
+ * you have to create a new resource for the linked resource.
+ * This error can occur if:
+ * - The resource is new
+ * - The resource URL is present as attribute of the main resource but the linked resource doesn't exist anymore.
+ * ( for instance it may happen if the creation procedure gives an error )
+ * - The resource is not writable by the user. It happens when a user changes the permission of a resource and doesn't update
+ * the resource permission.
+ */
+ (e) => createLinkedResource(id, attributeName, linkedResource, permission, API, e)
+ );
+/**
+ * Updates the permission of the linkedResources that are not modified.
+ * It checks the resource's attribute to find out resources that have to be updated.
+ * @param {number} id id of the main resource
+ * @param {object} linkedResources linked resources that are updating
+ * @param {array} permission array of permission
+ * @param {object} API the API to use
+ */
+const updateOtherLinkedResourcesPermissions = (id, linkedResources, permission, API = GeoStoreDAO) =>
+ Rx.Observable.defer( () => API.getResourceAttributes(id))
+ .map( attributes => attributes
+ // excludes resources that are going to be updated
+ .filter(({ name } = {}) => !includes(Object.keys(linkedResources), name))
+ // find out which attributes match the resource ids
+ .map(({ value }) => getResourceIdFromURL(value))
+ .filter(rid => rid !== undefined)
+ )
+ .switchMap((ids = []) =>
+ ids.length === 0
+ ? Rx.Observable.of([])
+ : Rx.Observable.forkJoin(
+ ids.map(rid => updateResourcePermissions(rid, permission, API))
+
+ ));
+/**
+ * Retrieves a resource with data with all information about user's permission on that resource, attributes and data.
+ * @param {number} id the id of the resource to get
+ * @param {options} param1 `includeAttributes` and `withData` flags, both true by default
+ * @param {object} API the API to use
+ * @return and observable that emits the resource
+ */
+const getResource = (id, { includeAttributes = true, withData = true } = {}, API = GeoStoreDAO) =>
+ Rx.Observable.forkJoin([
+ Rx.Observable.defer(() => API.getShortResource(id)).pluck("ShortResource"),
+ Rx.Observable.defer(() => includeAttributes ? API.getResourceAttributes(id) : Rx.Observable.empty()),
+ Rx.Observable.defer(() => withData ? API.getData(id) : Rx.Observable.empty())
+ ]).map(([resource, attributes, data]) => ({
+ ...resource,
+ attributes: (attributes || []).reduce((acc, curr) => ({
+ ...acc,
+ [curr.name]: curr.value
+ }), {}),
+ data
+ }));
+
+
+/**
+ * Returns an observable for saving a "Resource" and it's linked resources.
+ * Linked resources are geostore resources like thumbnail or details. The main resource contains a link
+ * to that resources as attributes. (the URL is double encoded to avoid issues with conversions in other pieces of the API)
+ * Required format of the resource object is:
+ * ```
+ * {
+ * id: "id", // if present. Otherwise a new item will be created
+ * category: "string",
+ * metadata: {
+ * name: "name",
+ * description: "description",
+ * attribute1: "value1",
+ * attribute2: "value2"
+ * }
+ * permission: [{}] // permissions to save
+ * data: {}
+ * linkedResources: {
+ * thumbnail: {
+ * tail: '/raw?decode=datauri' // for thumbnails, this will be appended to the resource URL in the main resource
+ * data: {}
+ *
+ * }
+ * }
+ * ```
+ * }
+ * @param {resource} param0 resource content
+ * @param {object} API the API to use
+ * @return an observable that emits the id of the resource
+ */
+const createResource = ({ data, category, metadata, permission: configuredPermission, linkedResources = {} }, API = GeoStoreDAO) =>
+ // create resource
+ Rx.Observable.defer(
+ () => API.createResource(metadata, data, category)
+ )
+ .pluck('data') // get the id
+ // set resource permission
+ .switchMap(id =>
+ // on creation owner some permission are assigned by default to the resources.
+ // only on creation they have to be merged with configuredPermission.
+ Rx.Observable.defer( () => API.getResourcePermissions(id))
+ .map(defaultPermission => mergePermission(defaultPermission, configuredPermission))
+ .switchMap( permission =>
+ updateResourcePermissions(id, permission, API)
+ .map(() => ({ id, permission})))
+ )
+ // create linkedResources
+ .switchMap(({id, permission}) =>
+ Object.keys(linkedResources).length > 0
+ ? Rx.Observable.forkJoin(
+ Object.keys(linkedResources)
+ .filter(k => linkedResources[k].data && linkedResources[k].data !== "NODATA")
+ .map(attributeName =>
+ createLinkedResource(id, attributeName, linkedResources[attributeName], permission, API)
+ )
+ ).map(() => id)
+ : Rx.Observable.of(id)
+ );
+/**
+ * Updates a resource setting up permission and linked resources
+ * @param {resource} param0 the resource to update (must contain the id)
+ * @param {object} API the API to use
+ */
+const updateResource = ({ id, data, category, permission, metadata, linkedResources = {} } = {}, API = GeoStoreDAO) =>
+ Rx.Observable.forkJoin([
+ // update metadata
+ Rx.Observable.defer(
+ () => API.putResourceMetadata(id, metadata.name, metadata.description)
+ ),
+ // update data
+ data
+ ? Rx.Observable.defer(
+ () => API.putResource(id, data)
+ )
+ : Rx.Observable.empty(),
+ // update permission
+ updateResourcePermissions(id, permission, API),
+ updateOtherLinkedResourcesPermissions(id, linkedResources, permission, API),
+ // update linkedResources
+ ...(
+ Object.keys(linkedResources).map(
+ attributeName => updateLinkedResource(id, attributeName, linkedResources[attributeName], permission, API)
+ )
+ )
+ ]).map(() => id);
+module.exports = {
+ getResource,
+ createResource,
+ updateResource
+};
diff --git a/web/client/plugins/DashboardEditor.jsx b/web/client/plugins/DashboardEditor.jsx
index 25dc278c3e..f2cdb5996f 100644
--- a/web/client/plugins/DashboardEditor.jsx
+++ b/web/client/plugins/DashboardEditor.jsx
@@ -13,11 +13,16 @@ const {connect} = require('react-redux');
const PropTypes = require('prop-types');
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 { createWidget, toggleConnection } = require('../actions/widgets');
-const { triggerShowConnections } = require('../actions/dashboard');
-const { showConnectionsSelector } = require('../selectors/dashboard');
+const { triggerShowConnections, triggerSave } = require('../actions/dashboard');
+
const withDashboardExitButton = require('./widgetbuilder/enhancers/withDashboardExitButton');
+
const Builder =
compose(
connect(dashboardSelector, { toggleConnection, triggerShowConnections}),
@@ -31,15 +36,26 @@ const Toolbar = compose(
connect(
createSelector(
showConnectionsSelector,
- showConnections => ({showConnections})
+ isLoggedIn,
+ dashboardResource,
+ dashboardHasWidgets,
+ (showConnections, logged, resource, hasWidgets) => ({
+ showConnections,
+ hasWidgets,
+ canSave: logged && hasWidgets && (resource ? resource.canEdit : true)
+ })
),
{
onShowConnections: triggerShowConnections,
+ onToggleSave: triggerSave,
onAddWidget: createWidget
}
),
withProps(({
onAddWidget = () => {},
+ onToggleSave = () => {},
+ hasWidgets,
+ canSave,
showConnections, onShowConnections = () => { }
}) => ({
buttons: [{
@@ -48,15 +64,24 @@ const Toolbar = compose(
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)
}]
}))
)(require('../components/misc/toolbar/Toolbar'));
+const SaveDialog = require('./dashboard/SaveDialog');
const {setEditing, setEditorAvailable} = require('../actions/dashboard');
@@ -98,11 +123,10 @@ class DashboardEditorComponent extends React.Component {
this.props.onUnmount();
}
render() {
-
-
return this.props.editing
? this.props.setEditing(false)} catalog={this.props.catalog}/>
: (
+
);
}
@@ -111,7 +135,7 @@ class DashboardEditorComponent extends React.Component {
const Plugin = connect(
createSelector(
isDashboardEditing,
- editing => ({editing})
+ (editing) => ({ editing }),
), {
setEditing,
onMount: () => setEditorAvailable(true),
diff --git a/web/client/plugins/dashboard/SaveDialog.jsx b/web/client/plugins/dashboard/SaveDialog.jsx
new file mode 100644
index 0000000000..3ca6bc3ee0
--- /dev/null
+++ b/web/client/plugins/dashboard/SaveDialog.jsx
@@ -0,0 +1,56 @@
+import { withStateHandlers } from 'recompose';
+
+/*
+ * 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 { connect } = require('react-redux');
+const { compose, withProps, branch, renderNothing } = require('recompose');
+const { createSelector } = require('reselect');
+
+const { userSelector } = require('../../selectors/security');
+const { widgetsConfig } = require('../../selectors/widgets');
+const { isShowSaveOpen, dashboardResource, isDashboardLoading, getDashboardSaveErrors } = require('../../selectors/dashboard');
+const { saveDashboard, triggerSave } = require('../../actions/dashboard');
+const handleResourceData = require('../../components/dashboard/modals/enhancers/handleResourceData');
+const handlePermission = require('../../components/dashboard/modals/enhancers/handlePermission');
+
+/**
+ * Save dialog component enhanced for dashboard
+ *
+ */
+module.exports = compose(
+ connect(createSelector(
+ isShowSaveOpen,
+ dashboardResource,
+ widgetsConfig,
+ userSelector,
+ isDashboardLoading,
+ getDashboardSaveErrors,
+ (show, resource, data, user, loading, errors ) => ({ show, resource, data, user, loading, errors })
+ ), {
+ onClose: () => triggerSave(false),
+ onSave: saveDashboard
+ }),
+ branch(
+ ({show}) => !show,
+ renderNothing
+ ),
+ handleResourceData,
+ handlePermission(),
+ withStateHandlers(
+ () => ({}),
+ {
+ onError: () => (formErrors) => ({ formErrors })
+ }
+ ),
+ withProps(
+ ({errors = [], formErrors = []}) => ({
+ errors: [...errors, ...formErrors]
+ })
+ )
+
+)(require('../../components/dashboard/modals/Save'));
diff --git a/web/client/product/appConfig.js b/web/client/product/appConfig.js
index 10e006e9fe..892159d9f9 100644
--- a/web/client/product/appConfig.js
+++ b/web/client/product/appConfig.js
@@ -35,7 +35,11 @@ module.exports = {
name: "dashboard",
path: "/dashboard",
component: require('./pages/Dashboard')
- }],
+ }, {
+ name: "dashboard",
+ path: "/dashboard/:did",
+ component: require('./pages/Dashboard')
+ }],
initialState: {
defaultState: {
mousePosition: {enabled: false},
diff --git a/web/client/product/pages/Dashboard.jsx b/web/client/product/pages/Dashboard.jsx
index 5c7faa810e..e46c4472ef 100644
--- a/web/client/product/pages/Dashboard.jsx
+++ b/web/client/product/pages/Dashboard.jsx
@@ -9,23 +9,23 @@ const PropTypes = require('prop-types');
const React = require('react');
const {connect} = require('react-redux');
-
+const { get } = require('lodash');
const url = require('url');
const urlQuery = url.parse(window.location.href, true).query;
const ConfigUtils = require('../../utils/ConfigUtils');
-const {loadMapConfig} = require('../../actions/config');
+const { loadDashboard } = require('../../actions/dashboard');
const {resetControls} = require('../../actions/controls');
const HolyGrail = require('../../containers/HolyGrail');
-class MapsPage extends React.Component {
+class DashboardPage extends React.Component {
static propTypes = {
name: PropTypes.string,
mode: PropTypes.string,
match: PropTypes.object,
- loadMaps: PropTypes.func,
+ loadResource: PropTypes.func,
reset: PropTypes.func,
plugins: PropTypes.object
};
@@ -38,23 +38,27 @@ class MapsPage extends React.Component {
};
componentWillMount() {
- if (this.props.match.params.mapType && this.props.match.params.mapId) {
- if (this.props.mode === 'mobile') {
- require('../assets/css/mobile.css');
- }
+ const id = get(this.props, "match.params.did");
+ if (id) {
this.props.reset();
- this.props.loadMaps(ConfigUtils.getDefaults().geoStoreUrl, ConfigUtils.getDefaults().initialMapFilter || "*");
+ this.props.loadResource(id);
+ }
+ }
+ componentDidUpdate(oldProps) {
+ const id = get(this.props, "match.params.did");
+ if (get(oldProps, "match.params.did") !== get(this.props, "match.params.did")) {
+ this.props.reset();
+ this.props.loadResource(id);
}
}
-
render() {
let plugins = ConfigUtils.getConfigProp("plugins") || {};
let pagePlugins = {
- "desktop": [], // TODO mesh page plugins with other plugins
+ "desktop": [],
"mobile": []
};
let pluginsConfig = {
- "desktop": plugins[this.props.name] || [], // TODO mesh page plugins with other plugins
+ "desktop": plugins[this.props.name] || [],
"mobile": plugins[this.props.name] || []
};
@@ -72,6 +76,6 @@ module.exports = connect((state) => ({
mode: urlQuery.mobile || state.browser && state.browser.mobile ? 'mobile' : 'desktop'
}),
{
- loadMapConfig,
+ loadResource: loadDashboard,
reset: resetControls
- })(MapsPage);
+ })(DashboardPage);
diff --git a/web/client/reducers/__tests__/dashboard-test.js b/web/client/reducers/__tests__/dashboard-test.js
index 634ed7f285..718520f46f 100644
--- a/web/client/reducers/__tests__/dashboard-test.js
+++ b/web/client/reducers/__tests__/dashboard-test.js
@@ -6,7 +6,15 @@
* LICENSE file in the root directory of this source tree.
*/
const expect = require('expect');
-const { setEditorAvailable, setEditing, triggerShowConnections } = require('../../actions/dashboard');
+const {
+ setEditorAvailable,
+ setEditing,
+ triggerShowConnections,
+ triggerSave,
+ dashboardLoaded,
+ dashboardSaved,
+ dashboardSaveError,
+ dashboardLoading } = require('../../actions/dashboard');
const { insertWidget, updateWidget, deleteWidget } = require('../../actions/widgets');
const dashboard = require('../dashboard');
describe('Test the dashboard reducer', () => {
@@ -27,5 +35,37 @@ describe('Test the dashboard reducer', () => {
const state = dashboard({}, triggerShowConnections(true));
expect(state.showConnections).toBe(true);
});
+ it('dashboard triggerSave', () => {
+ const action = triggerSave(true);
+ const state = dashboard( undefined, action);
+ expect(state).toExist();
+ expect(state.showSaveModal).toBe(true);
+ });
+ it('dashboard dashboardLoaded', () => {
+ const action = dashboardLoaded("TEST");
+ const state = dashboard( undefined, action);
+ expect(state).toExist();
+ expect(state.resource).toBe("TEST");
+ });
+ it('dashboard dashboardSaveError', () => {
+ const action = dashboardSaveError(["error1"]);
+ const state = dashboard( undefined, action);
+ expect(state).toExist();
+ expect(state.saveErrors.length).toBe(1);
+ });
+ it('dashboard dashboardSaved', () => {
+ const action = dashboardSaved();
+ const state = dashboard( {saveErrors: ["error"]}, action);
+ expect(state.saveErrors).toNotExist();
+ expect(state).toExist();
+ });
+ it('dashboard dashboardLoading', () => {
+ const action = dashboardLoading(true, "saving");
+ const state = dashboard( undefined, action);
+ expect(state).toExist();
+ expect(state.loading).toBe(true);
+ expect(state.loadFlags.saving).toBe(true);
+ });
+
});
diff --git a/web/client/reducers/__tests__/widgets-test.js b/web/client/reducers/__tests__/widgets-test.js
index c1497a3981..f759d4a025 100644
--- a/web/client/reducers/__tests__/widgets-test.js
+++ b/web/client/reducers/__tests__/widgets-test.js
@@ -21,6 +21,7 @@ const {
DEFAULT_TARGET
} = require('../../actions/widgets');
const {configureMap} = require('../../actions/config');
+const {dashboardLoaded} = require('../../actions/dashboard');
const widgets = require('../widgets');
const expect = require('expect');
@@ -128,4 +129,12 @@ describe('Test the widgets reducer', () => {
expect(state.dependencies.center).toBe("map.center");
expect(state.dependencies.zoom).toBe("map.zoom");
});
+ it('widgets dashboardLoaded', () => {
+ const widgetsData = { widgets: [{}] };
+ const action = dashboardLoaded("RESOURCE", widgetsData);
+ const state = widgets( undefined, action);
+ expect(state).toExist();
+ expect(state.containers[DEFAULT_TARGET].widgets).toExist();
+ expect(state.containers[DEFAULT_TARGET].widgets.length).toBe(1);
+ });
});
diff --git a/web/client/reducers/dashboard.js b/web/client/reducers/dashboard.js
index da7788118e..992607dd43 100644
--- a/web/client/reducers/dashboard.js
+++ b/web/client/reducers/dashboard.js
@@ -6,9 +6,10 @@
* LICENSE file in the root directory of this source tree.
*/
-const { SET_EDITING, SET_EDITOR_AVAILABLE, SHOW_CONNECTIONS} = require('../actions/dashboard');
+const { SET_EDITING, SET_EDITOR_AVAILABLE, SHOW_CONNECTIONS, TRIGGER_SAVE_MODAL, DASHBOARD_LOADING, DASHBOARD_LOADED, DASHBOARD_SAVED, SAVE_ERROR} = require('../actions/dashboard');
const {INSERT, UPDATE, DELETE} = require('../actions/widgets');
const {set} = require('../utils/ImmutableUtils');
+const {castArray} = require('lodash');
function dashboard(state = {}, action) {
switch (action.type) {
case SET_EDITOR_AVAILABLE: {
@@ -23,6 +24,23 @@ function dashboard(state = {}, action) {
return set("editing", action.editing, state);
case SHOW_CONNECTIONS:
return set("showConnections", action.show, state);
+ case TRIGGER_SAVE_MODAL:
+ return set("showSaveModal", action.show, set('saveErrors', undefined, state));
+ case DASHBOARD_LOADED: {
+ return set("resource", action.resource, state);
+ }
+ case SAVE_ERROR: {
+ return set('saveErrors', castArray(action.error), state);
+ }
+ case DASHBOARD_SAVED: {
+ return set('saveErrors', undefined, state);
+ }
+ case DASHBOARD_LOADING: {
+ // anyway sets loading to true
+ return set(action.name === "loading" ? "loading" : `loadFlags.${action.name}`, action.value, set(
+ "loading", action.value, state
+ ));
+ }
default:
return state;
}
diff --git a/web/client/reducers/widgets.js b/web/client/reducers/widgets.js
index 076b28fd1e..21e59eb32d 100644
--- a/web/client/reducers/widgets.js
+++ b/web/client/reducers/widgets.js
@@ -11,6 +11,9 @@ ADD_DEPENDENCY, REMOVE_DEPENDENCY, LOAD_DEPENDENCIES, RESET_DEPENDENCIES} = requ
const {
MAP_CONFIG_LOADED
} = require('../actions/config');
+const {
+ DASHBOARD_LOADED
+} = require('../actions/dashboard');
const set = require('lodash/fp/set');
const { get, find} = require('lodash');
@@ -101,8 +104,13 @@ function widgetsReducer(state = emptyState, action) {
return arrayDelete(`containers[${action.target}].widgets`, {
id: action.widget.id
}, state);
+ case DASHBOARD_LOADED:
+ const { data } = action;
+ return set(`containers[${DEFAULT_TARGET}]`, {
+ ...data
+ }, state);
case MAP_CONFIG_LOADED:
- const {widgetsConfig} = (action.config || {});
+ const { widgetsConfig } = (action.config || {});
return set(`containers[${DEFAULT_TARGET}]`, {
...widgetsConfig
}, state);
diff --git a/web/client/selectors/__tests__/dashboard-test.js b/web/client/selectors/__tests__/dashboard-test.js
index 4a435bce4a..8149636d97 100644
--- a/web/client/selectors/__tests__/dashboard-test.js
+++ b/web/client/selectors/__tests__/dashboard-test.js
@@ -10,7 +10,11 @@ const expect = require('expect');
const {
isDashboardAvailable,
isDashboardEditing,
- showConnectionsSelector
+ showConnectionsSelector,
+ isShowSaveOpen,
+ dashboardResource,
+ isDashboardLoading,
+ getDashboardSaveErrors
} = require('../dashboard');
describe('dashboard selectors', () => {
it('test isDashboardAvailable selector', () => {
@@ -28,4 +32,32 @@ describe('dashboard selectors', () => {
}
})).toBe(true);
});
+ it('isShowOpen', () => {
+ expect(isShowSaveOpen({
+ dashboard: {
+ showSaveModal: true
+ }
+ })).toBe(true);
+ });
+ it('dashboardResource', () => {
+ expect(dashboardResource({
+ dashboard: {
+ resource: {}
+ }
+ })).toExist();
+ });
+ it('isDashboardLoading', () => {
+ expect(isDashboardLoading({
+ dashboard: {
+ loading: true
+ }
+ })).toBe(true);
+ });
+ it('getDashboardSaveErrors', () => {
+ expect(getDashboardSaveErrors({
+ dashboard: {
+ saveErrors: [{}]
+ }
+ }).length).toBe(1);
+ });
});
diff --git a/web/client/selectors/dashboard.js b/web/client/selectors/dashboard.js
index 5dba3cec00..b8e5584199 100644
--- a/web/client/selectors/dashboard.js
+++ b/web/client/selectors/dashboard.js
@@ -1,9 +1,16 @@
const isDashboardAvailable = state => state && state.dashboard && state.dashboard.editor && state.dashboard.editor.available;
+const isShowSaveOpen = state => state && state.dashboard && state.dashboard.showSaveModal;
const isDashboardEditing = state => state && state.dashboard && state.dashboard.editing;
const showConnectionsSelector = state => state && state.dashboard && state.dashboard.showConnections;
-
+const dashboardResource = state => state && state.dashboard && state.dashboard.resource;
+const isDashboardLoading = state => state && state.dashboard && state.dashboard.loading;
+const getDashboardSaveErrors = state => state && state.dashboard && state.dashboard.saveErrors;
module.exports = {
isDashboardAvailable,
+ isShowSaveOpen,
isDashboardEditing,
- showConnectionsSelector
+ showConnectionsSelector,
+ dashboardResource,
+ isDashboardLoading,
+ getDashboardSaveErrors
};
diff --git a/web/client/selectors/security.js b/web/client/selectors/security.js
index 6fda7509b4..b0040db810 100644
--- a/web/client/selectors/security.js
+++ b/web/client/selectors/security.js
@@ -44,6 +44,7 @@ module.exports = {
rulesSelector,
userSelector,
userParamsSelector,
+ isLoggedIn: state => state && state.security && state.security.user,
userRoleSelector,
securityTokenSelector: state => state.security && state.security.token,
userGroupSecuritySelector,
diff --git a/web/client/selectors/widgets.js b/web/client/selectors/widgets.js
index 6764638b24..938602ab74 100644
--- a/web/client/selectors/widgets.js
+++ b/web/client/selectors/widgets.js
@@ -5,7 +5,7 @@ const {DEFAULT_TARGET, DEPENDENCY_SELECTOR_KEY, WIDGETS_REGEX} = require('../act
const { getWidgetsGroups, getWidgetDependency} = require('../utils/WidgetsUtils');
const {isDashboardAvailable, isDashboardEditing} = require('./dashboard');
-const {defaultMemoize, createSelector, createSelectorCreator} = require('reselect');
+const { defaultMemoize, createSelector, createSelectorCreator, createStructuredSelector} = require('reselect');
const isShallowEqual = (el1, el2) => {
if (Array.isArray(el1) && Array.isArray(el2)) {
@@ -58,12 +58,16 @@ const getWidgetsDependenciesGroups = createSelector(
getFloatingWidgets,
widgets => getWidgetsGroups(widgets)
);
+const getFloatingWidgetsLayout = state => get(state, `widgets.containers[${DEFAULT_TARGET}].layouts`);
+
+const getDashboardWidgets = state => get(state, `widgets.containers[${DEFAULT_TARGET}].widgets`);
module.exports = {
getFloatingWidgets,
- getFloatingWidgetsLayout: state => get(state, `widgets.containers[${DEFAULT_TARGET}].layouts`),
+ getFloatingWidgetsLayout,
// let's use the same container for the moment
- getDashboardWidgets: state => get(state, `widgets.containers[${DEFAULT_TARGET}].widgets`),
+ getDashboardWidgets,
+ dashboardHasWidgets: state => (getDashboardWidgets(state) || []).length > 0,
getDashboardWidgetsLayout: state => get(state, `widgets.containers[${DEFAULT_TARGET}].layouts`),
getEditingWidget,
getEditingWidgetLayer: state => get(getEditingWidget(state), "layer"),
@@ -98,5 +102,9 @@ module.exports = {
),
isWidgetSelectionActive,
getDependencySelectorConfig,
- getWidgetsDependenciesGroups
+ getWidgetsDependenciesGroups,
+ widgetsConfig: createStructuredSelector({
+ widgets: getFloatingWidgets,
+ layouts: getFloatingWidgetsLayout
+ })
};
diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE
index 39fa5f6adc..dbf0aca7a4 100644
--- a/web/client/translations/data.de-DE
+++ b/web/client/translations/data.de-DE
@@ -1257,7 +1257,30 @@
},
"dashboard": {
+ "saveDialog": {
+ "title": "Dashboardeigenschaften bearbeiten",
+ "name": "Name",
+ "description": "Beschreibung",
+ "namePlaceholder": "Geben Sie einen Namen ein ...",
+ "descriptionPlaceholder": "Geben Sie eine Beschreibung ein ...",
+ "confirmCloseText": "Es gibt ausstehende Änderungen, sind Sie sicher, dass Sie schließen möchten, ohne zu speichern?",
+ "close": "Schließen",
+ "cancel": "Abbrechen",
+ "saveSuccessTitle": "Erfolg",
+ "saveSuccessMessage": "Dashboard erfolgreich gespeichert"
+ },
+ "errors":{
+ "loading": {
+ "title": "Fehler beim Laden des Dashboards",
+ "dashboardNotAccessible": "Sie sind nicht berechtigt, auf dieses Dashboard zuzugreifen. Wenden Sie sich an den Ressourcenbesitzer",
+ "pleaseLogin": "Dieses Dashboard ist nicht öffentlich. Bitte versuchen Sie sich einzuloggen",
+ "dashboardDoesNotExist": "Das Dashboard, auf das Sie zugreifen möchten, existiert nicht oder wurde entfernt",
+ "unknownError": "Beim Laden des Dashboards ist ein Fehler aufgetreten. Bitte wenden Sie sich an den Administrator."
+ },
+ "resourceAlreadyExists": "Eine Ressource mit diesem Namen existiert bereits. Versuchen Sie '{data}'"
+ },
"editor": {
+ "save": "Speichern Sie das Dashboard",
"addACardToTheDashboard": "Ein Widget zum Dashboard hinzufügen",
"showConnections": "Verbindungen anzeigen",
"hideConnections": "Verbindungen ausblenden"
diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US
index 73c3ce00f1..8ebf89311e 100644
--- a/web/client/translations/data.en-US
+++ b/web/client/translations/data.en-US
@@ -1258,7 +1258,30 @@
},
"dashboard": {
+ "saveDialog": {
+ "title": "Edit dashboard properties",
+ "name": "Name",
+ "description": "Description",
+ "namePlaceholder": "Type a name...",
+ "descriptionPlaceholder": "Type a description...",
+ "confirmCloseText": "There are pending changes, are you sure that you want to close without saving?",
+ "close": "Close",
+ "cancel": "Cancel",
+ "saveSuccessTitle": "Success",
+ "saveSuccessMessage": "Dashboard saved successfully"
+ },
+ "errors":{
+ "loading": {
+ "title": "Error loading dashboard",
+ "dashboardNotAccessible": "You don't have permission to access this dashboard. Please contact the resource owner",
+ "pleaseLogin": "This dashboard is not public. Please try to login",
+ "dashboardDoesNotExist": "The dashboard you are trying to access doesn't exist or has been removed",
+ "unknownError": "There was an error loading the dashboard. Please contact the administrator"
+ },
+ "resourceAlreadyExists": "A resource with this name already exists. try '{data}'"
+ },
"editor": {
+ "save": "Save the dashboard",
"addACardToTheDashboard": "Add a widget to the dashboard",
"showConnections": "Show connections",
"hideConnections": "HideConnections"
diff --git a/web/client/translations/data.es-ES b/web/client/translations/data.es-ES
index 122b800d40..a123ccd2b1 100644
--- a/web/client/translations/data.es-ES
+++ b/web/client/translations/data.es-ES
@@ -1257,7 +1257,30 @@
},
"dashboard": {
+ "saveDialog": {
+ "title": "Editar propiedades del tablero",
+ "name": "Nombre",
+ "description": "Descripción",
+ "namePlaceholder": "Escriba un nombre ...",
+ "descriptionPlaceholder": "Escriba una descripción ...",
+ "confirmCloseText": "Hay cambios pendientes, ¿está seguro de que quiere cerrar sin guardar?",
+ "close": "Cerca",
+ "cancel": "Cancelar",
+ "saveSuccessTitle": "éxito",
+ "saveSuccessMessage": "Panel de control guardado con éxito"
+ },
+ "errors":{
+ "loading": {
+ "title": "Error al cargar el tablero",
+ "dashboardNotAccessible": "No tiene permiso para acceder a este panel. Póngase en contacto con el propietario del recurso",
+ "pleaseLogin": "Este panel no es público. Intente iniciar sesión",
+ "dashboardDoesNotExist": "El panel al que está intentando acceder no existe o se ha eliminado",
+ "unknownError": "Hubo un error al cargar el panel. Póngase en contacto con el administrador"
+ },
+ "resourceAlreadyExists": "Ya existe un recurso con este nombre. try '{data}'"
+ },
"editor": {
+ "save": "Guardar el tablero",
"addACardToTheDashboard": "Agregar un widget al tablero",
"showConnections": "Mostrar conexiones",
"hideConnections": "Ocultar conexiones"
diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR
index 6eac4e65b2..c46eebd3d7 100644
--- a/web/client/translations/data.fr-FR
+++ b/web/client/translations/data.fr-FR
@@ -1257,7 +1257,30 @@
}
},
"dashboard": {
- "editor": {
+ "saveDialog": {
+ "title": "Modifier les propriétés du tableau de bord",
+ "name": "Nom",
+ "description": "Description",
+ "namePlaceholder": "Tapez un nom ...",
+ "descriptionPlaceholder": "Tapez une description ...",
+ "confirmCloseText": "Des modifications sont en attente, êtes-vous sûr de vouloir fermer sans enregistrer?",
+ "close": "Fermer",
+ "cancel": "Annuler",
+ "saveSuccessTitle": "Succès",
+ "saveSuccessMessage": "Tableau de bord enregistré avec succès"
+ },
+ "errors":{
+ "loading": {
+ "title": "Erreur lors du chargement du tableau de bord",
+ "dashboardNotAccessible": "Vous n'êtes pas autorisé à accéder à ce tableau de bord. Veuillez contacter le propriétaire de la ressource",
+ "pleaseLogin": "Ce tableau de bord n'est pas public. Veuillez essayer de vous connecter",
+ "dashboardDoesNotExist": "Le tableau de bord auquel vous essayez d'accéder n'existe pas ou a été supprimé",
+ "unknownError": "Une erreur est survenue lors du chargement du tableau de bord. Veuillez contacter l'administrateur"
+ },
+ "resourceAlreadyExists": "Une ressource avec ce nom existe déjà. try '{data}'"
+ },
+ "editor": {
+ "save": "Sauvegarder le tableau de bord",
"addACardToTheDashboard": "Ajouter un widget au tableau de bord",
"showConnections": "Afficher les connexions",
"hideConnections": "Masquer les connexions"
diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT
index b6bd60ec7f..2c8f04d1ab 100644
--- a/web/client/translations/data.it-IT
+++ b/web/client/translations/data.it-IT
@@ -1257,7 +1257,30 @@
},
"dashboard": {
- "editor": {
+ "saveDialog": {
+ "title": "Modifica proprietà dashboard",
+ "name": "Nome",
+ "description": "Descrizione",
+ "namePlaceholder": "Inserisci un nome..",
+ "descriptionPlaceholder": "Inserisci una descrizione...",
+ "confirmCloseText": "Ci sono modifiche non salvate, sei sicuro di voler chiudere senza salvare?",
+ "close": "Chiudi",
+ "cancel": "Annulla",
+ "saveSuccessTitle": "Successo",
+ "saveSuccessMessage": "Dashboard salvata"
+ },
+ "errors":{
+ "loading": {
+ "title": "Errore di caricamento della dashboard",
+ "dashboardNotAccessible": "Non hai i permessi per accedere a questa dashboard. Contatta il properietario",
+ "pleaseLogin": "Questa risorsa non è pubblica. Effettuare il login",
+ "dashboardDoesNotExist": "La dashboard richiesta non esiste o è stata cancellata",
+ "unknownError": "C'è stato un errore durante il caricamento della dashboard. Contattare l'amministratore"
+ },
+ "resourceAlreadyExists": "Una risorsa con questo nome esiste già. Provare con '{data}'"
+ },
+ "editor": {
+ "save": "Save the dashboard",
"addACardToTheDashboard": "Aggiungi un widget alla dashboard",
"showConnections": "Mostra connessioni",
"hideConnections": "Nascondi connessioni"