From d5eb5a2816e72dc970dca894c9bd0eb7dcacfb71 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 7 May 2018 18:21:08 +0200 Subject: [PATCH] Added details editor styles filters and attributes (#2867) --- web/client/api/geoserver/GeoFence.js | 17 +- .../rulesmanager/AttributeAccessSelect.jsx | 24 ++ .../manager/rulesmanager/AvailableStyles.jsx | 49 ++++ .../manager/rulesmanager/DefaultStyle.jsx | 36 +++ .../manager/rulesmanager/StylesList.jsx | 32 +++ .../rulesmanager/enhancers/autoComplete.js | 27 +- .../rulesmanager/enhancers/filterstyles.js | 24 ++ .../rulesmanager/enhancers/fixedOptions.js | 33 ++- .../ruleseditor/AttributesEditor.jsx | 60 +++++ .../rulesmanager/ruleseditor/EditMain.jsx | 7 + .../ruleseditor/FiltersEditor.jsx | 55 ++++ .../rulesmanager/ruleseditor/Header.jsx | 17 +- .../rulesmanager/ruleseditor/StylesEditor.jsx | 63 +++++ .../attributeselectors/IpAddress.jsx | 2 +- .../ruleseditor/attributeselectors/Layer.jsx | 3 +- .../ruleseditor/enhancers/filters.js | 21 ++ .../ruleseditor/enhancers/style.js | 19 ++ .../ruleseditor/enhancers/switch.js | 21 ++ .../misc/combobox/PagedCombobox.jsx | 2 +- web/client/epics/rulesmanager.js | 5 +- web/client/observables/rulesmanager.js | 48 +++- web/client/plugins/RulesDataGrid.jsx | 2 +- web/client/plugins/RulesEditor.jsx | 26 +- web/client/plugins/manager/EditorEnhancer.js | 59 ++++- web/client/plugins/manager/ManagerMenu.jsx | 8 +- web/client/plugins/manager/ModalDialog.jsx | 2 +- web/client/plugins/manager/RulesEditor.jsx | 67 ++++- web/client/plugins/manager/RulesToolbar.jsx | 3 +- web/client/product/pages/RulesManager.jsx | 37 ++- web/client/reducers/rulesmanager.js | 5 +- web/client/selectors/rulesmanager.js | 7 +- .../themes/default/less/rulesmanager.less | 238 +++++++++++++++--- web/client/translations/data.de-DE | 14 +- web/client/translations/data.en-US | 19 +- web/client/translations/data.es-ES | 14 +- web/client/translations/data.fr-FR | 14 +- web/client/translations/data.it-IT | 16 +- web/client/utils/RulesEditor.js | 32 +++ web/client/utils/RulesGridUtils.js | 9 +- 39 files changed, 972 insertions(+), 165 deletions(-) create mode 100644 web/client/components/manager/rulesmanager/AttributeAccessSelect.jsx create mode 100644 web/client/components/manager/rulesmanager/AvailableStyles.jsx create mode 100644 web/client/components/manager/rulesmanager/DefaultStyle.jsx create mode 100644 web/client/components/manager/rulesmanager/StylesList.jsx create mode 100644 web/client/components/manager/rulesmanager/enhancers/filterstyles.js create mode 100644 web/client/components/manager/rulesmanager/ruleseditor/AttributesEditor.jsx create mode 100644 web/client/components/manager/rulesmanager/ruleseditor/FiltersEditor.jsx create mode 100644 web/client/components/manager/rulesmanager/ruleseditor/StylesEditor.jsx create mode 100644 web/client/components/manager/rulesmanager/ruleseditor/enhancers/filters.js create mode 100644 web/client/components/manager/rulesmanager/ruleseditor/enhancers/style.js create mode 100644 web/client/components/manager/rulesmanager/ruleseditor/enhancers/switch.js create mode 100644 web/client/utils/RulesEditor.js diff --git a/web/client/api/geoserver/GeoFence.js b/web/client/api/geoserver/GeoFence.js index 6ea166969e..f61f9d4e26 100644 --- a/web/client/api/geoserver/GeoFence.js +++ b/web/client/api/geoserver/GeoFence.js @@ -11,7 +11,9 @@ const assign = require('object-assign'); const ConfigUtils = require('../../utils/ConfigUtils'); const EMPTY_RULE = { - constraints: {}, + constraints: { + allowedStyles: {style: []} + }, ipaddress: "", layer: "", request: "", @@ -20,6 +22,15 @@ const EMPTY_RULE = { username: "", workspace: "" }; +const cleanConstraints = (rule) => { + if (!rule.constraints ) { + return rule; + } + let constraints = {...rule.constraints}; + constraints.allowedStyles = constraints.allowedStyles && constraints.allowedStyles.style || []; + constraints.attributes = constraints.attributes && constraints.attributes.attribute || []; + return {...rule, constraints}; +}; var Api = { loadRules: function(page, rulesFiltersValues, entries = 10) { @@ -72,7 +83,7 @@ var Api = { if (!newRule.grant) { newRule.grant = "ALLOW"; } - return axios.post('geofence/rest/rules', newRule, this.addBaseUrl({ + return axios.post('geofence/rest/rules', cleanConstraints(newRule), this.addBaseUrl({ 'headers': { 'Content': 'application/json' } @@ -83,7 +94,7 @@ var Api = { // id, priority and grant aren't updatable const {id, priority, grant, position, ...others} = rule; const newRule = {...EMPTY_RULE, ...others}; - return axios.put(`geofence/rest/rules/id/${id}`, newRule, this.addBaseUrl({ + return axios.put(`geofence/rest/rules/id/${id}`, cleanConstraints(newRule), this.addBaseUrl({ 'headers': { 'Content': 'application/json' } diff --git a/web/client/components/manager/rulesmanager/AttributeAccessSelect.jsx b/web/client/components/manager/rulesmanager/AttributeAccessSelect.jsx new file mode 100644 index 0000000000..c285e22763 --- /dev/null +++ b/web/client/components/manager/rulesmanager/AttributeAccessSelect.jsx @@ -0,0 +1,24 @@ +/** +* 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 Select = require('react-select'); +const defaultOptions = [ + {value: 'NONE', label: "NONE"}, + {value: 'READONLY', label: 'READ ONLY'}, + {value: 'READWRITE', label: 'READ WRITE'} +]; + +module.exports = ({options = defaultOptions, attribute, value, onChange = () => {}}) => ( + + + + ); + })} + +); +}; diff --git a/web/client/components/manager/rulesmanager/ruleseditor/EditMain.jsx b/web/client/components/manager/rulesmanager/ruleseditor/EditMain.jsx index 48529728db..c302097fc6 100644 --- a/web/client/components/manager/rulesmanager/ruleseditor/EditMain.jsx +++ b/web/client/components/manager/rulesmanager/ruleseditor/EditMain.jsx @@ -1,3 +1,10 @@ +/** +* 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 {Grid} = require('react-bootstrap'); const Selectors = require("./attributeselectors"); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/FiltersEditor.jsx b/web/client/components/manager/rulesmanager/ruleseditor/FiltersEditor.jsx new file mode 100644 index 0000000000..4b9873e430 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/FiltersEditor.jsx @@ -0,0 +1,55 @@ +/** +* 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 {Grid} = require('react-bootstrap'); +const ContainerDimensions = require('react-container-dimensions').default; +const {Controlled: Codemirror} = require('react-codemirror2'); +require('codemirror/lib/codemirror.css'); +require('codemirror/mode/sql/sql'); +const switchEnhancer = require("./enhancers/switch"); +// const filtersEnhancer = require("./enhancers/filters"); +const SwitchPanel = switchEnhancer(require("../../../misc/switch/SwitchPanel")); +const Message = require("../../../I18N/Message"); + +module.exports = ({constraints = {}, active = false, setOption= () => {}}) => { + return ( + + }> +
+ + {({width}) =>
+ setOption({key: "cqlFilterRead", value})} + options={{ + mode: {name: "sql"}, + lineNumbers: true, + lineWrapping: true + }}/> +
} +
+
+
+ }> +
+ + {({width}) =>
+ setOption({key: "cqlFilterWrite", value})} + options={{ + mode: {name: "sql"}, + lineNumbers: true, + lineWrapping: true + }}/> +
} +
+
+
+
); +}; diff --git a/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx b/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx index 7fc8fe0bb0..8ce88face9 100644 --- a/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx +++ b/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx @@ -1,29 +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 React = require('react'); const Toolbar = require('../../../misc/toolbar/Toolbar'); const {NavItem, Nav} = require('react-bootstrap'); const Message = require('../../../I18N/Message'); -module.exports = ({onNavChange = () => {}, onExit = () => {}, disableSave = true, onSave = () => {}, activeTab = "1", detailsActive = false}) => { +module.exports = ({onNavChange = () => {}, onExit = () => {}, disableSave = true, onSave = () => {}, activeTab = "1", loading = false, detailsActive = false, type = ""}) => { const buttons = [{ glyph: '1-close', tooltipId: 'rulesmanager.tooltip.close', - onClick: onExit + onClick: onExit, + disabled: loading }, { glyph: 'floppy-disk', tooltipId: 'rulesmanager.tooltip.save', onClick: onSave, - disabled: disableSave + disabled: disableSave || loading }]; return (
+
); }; diff --git a/web/client/components/manager/rulesmanager/ruleseditor/StylesEditor.jsx b/web/client/components/manager/rulesmanager/ruleseditor/StylesEditor.jsx new file mode 100644 index 0000000000..3c7385d2cd --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/StylesEditor.jsx @@ -0,0 +1,63 @@ +/** +* 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 {Grid, Row, Col, Button, Glyphicon} = require('react-bootstrap'); + +const filterEnhancer = require("../enhancers/filterstyles"); +const DefaultStyle = filterEnhancer(require("../DefaultStyle")); +const AvailableStyles = filterEnhancer(require("../AvailableStyles")); + +const StylesList = require("../StylesList"); +const Message = require("../../../I18N/Message"); + +const enhancer = require("./enhancers/style"); +const getAvailables = (styles, {allowedStyles = {}}) => { + const allow = allowedStyles.style || []; + return styles.filter(s => allow.indexOf(s.name) !== -1); +}; + +module.exports = enhancer(({styles = [], constraints = {}, setOption= () => {}, active = false, toggleModal = () => {}, modal}) => { + return ( +
+ + + + + + + + s.name === constraints.defaultStyle)}/> + + + + + + +
+ +
+ setOption({key: "defaultStyle", value: sel[sel.length - 1]})}/> + setOption({key: "allowedStyles", value: {style: sel}})} + clearAll={() => setOption({key: "allowedStyles", value: {style: []}})} + selectAll={() => setOption({key: "allowedStyles", value: {style: styles.map(s => s.name)}})}/> +
+
+ ); +}); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/IpAddress.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/IpAddress.jsx index a54b806faa..ca87690170 100644 --- a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/IpAddress.jsx +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/IpAddress.jsx @@ -9,7 +9,7 @@ const React = require("react"); const PropTypes = require("prop-types"); const {FormControl, FormGroup, Row, Col} = require("react-bootstrap"); const Message = require('../../../../I18N/Message'); -const {checkIp} = require('../../../../../utils/RulesGridUtils'); +const {checkIp} = require('../../../../../utils/RulesEditor'); const withLocalized = require("../../../../misc/enhancers/localizedProps"); const {compose, defaultProps} = require("recompose"); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Layer.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Layer.jsx index 45a6459ba0..919e5819a0 100644 --- a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Layer.jsx +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Layer.jsx @@ -29,8 +29,7 @@ const LayerSelector = (props) => ( module.exports = compose( connect(() => ({}), {onError: error}), defaultProps({ - paginated: false, - emitOnReset: true, + paginated: true, size: 5, textField: "name", valueField: "name", diff --git a/web/client/components/manager/rulesmanager/ruleseditor/enhancers/filters.js b/web/client/components/manager/rulesmanager/ruleseditor/enhancers/filters.js new file mode 100644 index 0000000000..aa70765243 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/enhancers/filters.js @@ -0,0 +1,21 @@ +/** +* 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, withStateHandlers} = require("recompose"); + + +module.exports = compose( + withStateHandlers(() => ({ + panels: {} + }), + { + onSwitch: ({panels}) => (panel, expanded) => ({ + panels: {...panels, [panel]: expanded} + }) + + }) +); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/enhancers/style.js b/web/client/components/manager/rulesmanager/ruleseditor/enhancers/style.js new file mode 100644 index 0000000000..613058819f --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/enhancers/style.js @@ -0,0 +1,19 @@ +/** +* 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, withStateHandlers} = require("recompose"); + + +module.exports = compose( + withStateHandlers(() => ({}), + { + toggleModal: () => (modal) => ({ + modal + }) + + }) +); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/enhancers/switch.js b/web/client/components/manager/rulesmanager/ruleseditor/enhancers/switch.js new file mode 100644 index 0000000000..aa565357ea --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/enhancers/switch.js @@ -0,0 +1,21 @@ +/** +* 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, withStateHandlers} = require("recompose"); + + +module.exports = compose( + withStateHandlers(() => ({ + expanded: false + }), + { + onSwitch: () => (expanded) => ({ + expanded + }) + + }) +); diff --git a/web/client/components/misc/combobox/PagedCombobox.jsx b/web/client/components/misc/combobox/PagedCombobox.jsx index 80ed8533fa..72bd9f8c3a 100644 --- a/web/client/components/misc/combobox/PagedCombobox.jsx +++ b/web/client/components/misc/combobox/PagedCombobox.jsx @@ -35,7 +35,7 @@ class PagedCombobox extends React.Component { itemComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), label: PropTypes.string, loading: PropTypes.bool, - filter: PropTypes.string, + filter: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), messages: PropTypes.object, onChange: PropTypes.func, onFocus: PropTypes.func, diff --git a/web/client/epics/rulesmanager.js b/web/client/epics/rulesmanager.js index e2276634c5..708ed7e881 100644 --- a/web/client/epics/rulesmanager.js +++ b/web/client/epics/rulesmanager.js @@ -7,8 +7,9 @@ const {updateRule, createRule, deleteRule} = require("../observables/rulesmanage const {get} = require("lodash"); const saveRule = stream$ => stream$ .mapTo({type: RULE_SAVED}) - .catch(() => { - return Rx.Observable.of(error({title: "rulesmanager.errorTitle", message: "rulesmanager.errorUpdatingRule"})); + .catch(({data}) => { + const isDuplicate = data.indexOf("Duplicate Rule") === 0; + return Rx.Observable.of(error({title: "rulesmanager.errorTitle", message: isDuplicate ? "rulesmanager.errorDuplicateRule" : "rulesmanager.errorUpdatingRule"})); }) .startWith(setLoading(true)) .concat(Rx.Observable.of(setLoading(false))); diff --git a/web/client/observables/rulesmanager.js b/web/client/observables/rulesmanager.js index 5ef6f567b2..92296929cc 100644 --- a/web/client/observables/rulesmanager.js +++ b/web/client/observables/rulesmanager.js @@ -1,10 +1,16 @@ const Rx = require('rxjs'); + + const {parseString} = require('xml2js'); const {stripPrefix} = require('xml2js/lib/processors'); const CatalogAPI = require('../api/CSW'); const GeoFence = require('../api/geoserver/GeoFence'); const ConfigUtils = require('../utils/ConfigUtils'); +const {trim} = require("lodash"); + +const {getLayerCapabilities, describeLayer} = require("./wms"); +const {describeFeatureType} = require("./wfs"); const xmlToJson$ = response => Rx.Observable.bindNodeCallback( (data, callback) => parseString(data, { tagNameProcessors: [stripPrefix], @@ -13,9 +19,29 @@ const xmlToJson$ = response => Rx.Observable.bindNodeCallback( (data, callback) }, callback))(response); +const fixUrl = (url) => { + const u = trim(url, "/"); + return u.indexOf("http") === 0 ? `${u}/` : `/${u}/`; +}; +const getUpdateType = (o, n) => { + if (o.priority !== n.priority) { + return 'full'; + }else if (o.grant !== n.grant || o.ipaddress !== n.ipaddress) { + return 'grant'; + } + return "simple"; +}; const loadSinglePage = (page = 0, filters = {}, size = 10) => Rx.Observable.defer(() => GeoFence.loadRules(page, filters, size)) .switchMap( response => xmlToJson$(response) - .map(({RuleList = {}}) => ({ page, rules: [].concat(RuleList.rule || [])})) + .map(({RuleList = {}}) => ({ page, rules: ([].concat(RuleList.rule || [])).map(r => { + if (!r.constraints) { + return r; + } + const style = [].concat(r.constraints.allowedStyles.style || []); + r.constraints.allowedStyles = {style}; + return r; + } + )})) ); const countUsers = (filter = "") => Rx.Observable.defer(() => GeoFence.getUsersCount(filter)); const loadUsers = (filter = "", page = 0, size = 10) => Rx.Observable.defer(() => GeoFence.getUsers(filter, page, size)) @@ -31,7 +57,7 @@ const loadRoles = (filter = "", page = 0, size = 10) => Rx.Observable.defer(() = ); const deleteRule = (id) => Rx.Observable.defer(() => GeoFence.deleteRule(id)); // Full update we need to delete, save and move -const fullUpdate = (update$) => update$.filter(({rule: r, origRule: oR}) => r.priority !== oR.priority) +const fullUpdate = (update$) => update$.filter(({rule: r, origRule: oR}) =>getUpdateType(oR, r) === 'full') .switchMap(({rule, origRule}) => deleteRule(rule.id) .switchMap(() => { const {priority, id, ...newRule} = rule; @@ -47,7 +73,7 @@ const fullUpdate = (update$) => update$.filter(({rule: r, origRule: oR}) => r.pr return Rx.Observable.defer(() => GeoFence.moveRules(rule.priority, [{id}])); } )); -const grantUpdate = (update$) => update$.filter(({rule: r, origRule: oR}) => r.priority === oR.priority && (r.grant !== oR.grant || r.ipaddress !== oR.ipaddress)) +const grantUpdate = (update$) => update$.filter(({rule: r, origRule: oR}) => getUpdateType(oR, r) === 'grant') .switchMap(({rule, origRule}) => deleteRule(rule.id) .switchMap(() => { const {priority, id, ...newRule} = rule; @@ -62,7 +88,7 @@ const grantUpdate = (update$) => update$.filter(({rule: r, origRule: oR}) => r.p }) ); // if priority and grant are the same we just need to update new rule -const justUpdate = (update$) => update$.filter(({rule: r, origRule: oR}) => r.priority === oR.priority && r.grant === oR.grant && r.ipaddress === oR.ipaddress) +const justUpdate = (update$) => update$.filter(({rule: r, origRule: oR}) => getUpdateType(oR, r) === 'simple') .switchMap(({rule}) => Rx.Observable.defer(() => GeoFence.updateRule(rule))); module.exports = { loadRules: (pages = [], filters = {}, size) => @@ -100,6 +126,18 @@ module.exports = { return fullUp.merge(simpleUpdate, grant); }, createRule: (rule) => Rx.Observable.defer(() => GeoFence.addRule(rule)), - deleteRule + deleteRule, + getStylesAndAttributes: (layer, workspace) => { + const {url} = ConfigUtils.getDefaults().geoFenceGeoServerInstance || {}; + const l = {url: `${fixUrl(url)}ows`, name: `${workspace}:${layer}`}; + return Rx.Observable.combineLatest(getLayerCapabilities(l) + .map(({style}) => ({style})), + describeLayer(l).map(({data}) => data.layerDescriptions[0]) + .switchMap(({owsType}) => { + return owsType === "WCS" ? Rx.Observable.of({properties: [], type: "RASTER"}) : describeFeatureType({layer: l}) + .map(({data}) => ({properties: data.featureTypes[0] && data.featureTypes[0].properties || [], type: "VECTOR"})); + }), ({style}, {properties, type}) => ({styles: style || [], properties, type})); + + } }; diff --git a/web/client/plugins/RulesDataGrid.jsx b/web/client/plugins/RulesDataGrid.jsx index 507eb080fe..7a310c75e8 100644 --- a/web/client/plugins/RulesDataGrid.jsx +++ b/web/client/plugins/RulesDataGrid.jsx @@ -55,7 +55,7 @@ class RulesDataGrid extends React.Component { return ({({width, height}) => (
{!this.props.enabled && (
)} - +
) } ); diff --git a/web/client/plugins/RulesEditor.jsx b/web/client/plugins/RulesEditor.jsx index 60cd6b371e..41b1a6eada 100644 --- a/web/client/plugins/RulesEditor.jsx +++ b/web/client/plugins/RulesEditor.jsx @@ -12,7 +12,7 @@ const {createSelector} = require('reselect'); const {connect} = require('react-redux'); const PropTypes = require('prop-types'); -const { isEditorActive} = require('../selectors/rulesmanager'); +const { isEditorActive, isLoading} = require('../selectors/rulesmanager'); const Editor = require('./manager/RulesEditor'); const Toolbar = require('./manager/RulesToolbar'); @@ -38,7 +38,8 @@ class RulesEditorComponent extends React.Component { setEditing: PropTypes.func, dimMode: PropTypes.string, src: PropTypes.string, - style: PropTypes.object + style: PropTypes.object, + loading: PropTypes.bool }; static defaultProps = { id: "rules-editor", @@ -49,36 +50,25 @@ class RulesEditorComponent extends React.Component { fluid: false, dimMode: "none", position: "left", - onMount: () => {}, - onUnmount: () => {}, setEditing: () => {} }; - componentDidMount() { - this.props.onMount(); - } - - componentWillUnmount() { - this.props.onUnmount(); - } render() { return this.props.editing - ?
this.props.setEditing(false)} catalog={this.props.catalog}/>
+ ?
this.props.setEditing(false)} catalog={this.props.catalog}/>
: (
- +
); } } const Plugin = connect( createSelector( - isEditorActive, - editing => ({editing}) + [isEditorActive, + isLoading], + (editing, loading) => ({editing, loading}) ), { - // setEditing, - // onMount: () => setEditorAvailable(true), - // onUnmount: () => setEditorAvailable(false) } )(RulesEditorComponent); module.exports = { diff --git a/web/client/plugins/manager/EditorEnhancer.js b/web/client/plugins/manager/EditorEnhancer.js index 6ad9924f4e..4ad52b7376 100644 --- a/web/client/plugins/manager/EditorEnhancer.js +++ b/web/client/plugins/manager/EditorEnhancer.js @@ -1,21 +1,46 @@ -const {compose, withStateHandlers} = require('recompose'); - +const {compose, withStateHandlers, defaultProps} = require('recompose'); +const propsStreamFactory = require('../../components/misc/enhancers/propsStreamFactory'); +const Rx = require("rxjs"); +const sameLayer = ({activeRule: f1}, {activeRule: f2}) => f1.layer === f2.layer; +const emitStop = stream$ => stream$.filter(() => false).startWith({}); +const {getStylesAndAttributes} = require("../../observables/rulesmanager"); +const dataStreamFactory = prop$ => { + return prop$.distinctUntilChanged((oP, nP) => sameLayer(oP, nP)) + .filter(({activeRule}) => activeRule.layer) + .switchMap(({activeRule, optionsLoaded, onError = () => {}, setLoading}) => { + const {workspace, layer} = activeRule; + setLoading(true); + return getStylesAndAttributes(layer, workspace).do(opts => optionsLoaded(opts)) + .catch(() => { + setLoading(false); + return Rx.Observable.of(onError({ + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoading" + })); + }).do(() => setLoading(false)); + }).let(emitStop); +}; module.exports = compose( - // defaultProps({dataStreamFactory}), + defaultProps({dataStreamFactory}), withStateHandlers(({activeRule: initRule}) => ({ activeRule: initRule, initRule, - activeEditor: "1" + activeEditor: "1", + styles: [], + properties: [] }), { setOption: ({activeRule}) => ({key, value}) => { // Add some reference logic here - if (key === "workspace" && !value) { - const {layer, workspace, ...newActive} = activeRule; - return {activeRule: newActive}; - }else if (key === "service" && !value) { + if (key === "workspace") { + const {layer, workspace, constraints, ...newActive} = activeRule; + return {activeRule: { ...newActive, workspace: value}}; + }else if (key === "layer") { + const {layer, constraints, ...newActive} = activeRule; + return {activeRule: { ...newActive, layer: value}}; + }else if (key === "service") { const {request, service, ...newActive} = activeRule; - return {activeRule: newActive}; + return {activeRule: {...newActive, service: value}}; }else if (!value) { const {[key]: omit, ...newActive} = activeRule; return {activeRule: newActive}; @@ -24,8 +49,20 @@ module.exports = compose( activeRule: {...activeRule, [key]: value} }; }, + setConstraintsOption: ({activeRule, type}) => ({key, value}) => { + const constraints = {...(activeRule.constraints || {}), type, [key]: value}; + return {activeRule: {...activeRule, constraints}}; + }, onNavChange: () => activeEditor => ({ activeEditor - }) - }) + }), + cleanConstraints: ({activeRule}) => () => { + const {constraints, ...rule} = activeRule; + return {activeRule: rule}; + }, + optionsLoaded: () => ({styles, properties, type}) => { + return {styles, properties, type}; + } + }), + propsStreamFactory ); diff --git a/web/client/plugins/manager/ManagerMenu.jsx b/web/client/plugins/manager/ManagerMenu.jsx index 142afd3426..ada662eda2 100644 --- a/web/client/plugins/manager/ManagerMenu.jsx +++ b/web/client/plugins/manager/ManagerMenu.jsx @@ -11,7 +11,7 @@ const {connect} = require('react-redux'); const { itemSelected } = require('../../actions/manager'); const assign = require('object-assign'); - +const {isRulesManagerConfigured} = require("../../selectors/rulesmanager"); const {DropdownButton, Glyphicon, MenuItem} = require('react-bootstrap'); const Container = connect(() => ({ @@ -38,7 +38,8 @@ class ManagerMenu extends React.Component { controls: PropTypes.object, mapType: PropTypes.string, panelStyle: PropTypes.object, - panelClassName: PropTypes.string + panelClassName: PropTypes.string, + enableRulesManager: PropTypes.bool }; static contextTypes = { @@ -75,7 +76,7 @@ class ManagerMenu extends React.Component { }; getTools = () => { - return [{element: {this.props.title}}, ...this.props.entries.sort((a, b) => a.position - b.position).map((entry) => { + return [{element: {this.props.title}}, ...this.props.entries.filter(e => this.props.enableRulesManager || e.path !== "/rules-manager").sort((a, b) => a.position - b.position).map((entry) => { return { action: (context) => {context.router.history.push(entry.path); return this.props.itemSelected(entry.id); }, text: entry.msgId ? : entry.text, @@ -106,6 +107,7 @@ class ManagerMenu extends React.Component { module.exports = { ManagerMenuPlugin: assign(connect((state) => ({ + enableRulesManager: isRulesManagerConfigured(state), controls: state.controls, role: state.security && state.security.user && state.security.user.role }), { diff --git a/web/client/plugins/manager/ModalDialog.jsx b/web/client/plugins/manager/ModalDialog.jsx index abc3c25b02..9ca9528fef 100644 --- a/web/client/plugins/manager/ModalDialog.jsx +++ b/web/client/plugins/manager/ModalDialog.jsx @@ -21,7 +21,7 @@ module.exports = ({title = "", showDialog = false, buttons = [], closeAction = ( onClose={closeAction} buttons={buttons}>
-
+
diff --git a/web/client/plugins/manager/RulesEditor.jsx b/web/client/plugins/manager/RulesEditor.jsx index b351c2c182..90b787a053 100644 --- a/web/client/plugins/manager/RulesEditor.jsx +++ b/web/client/plugins/manager/RulesEditor.jsx @@ -12,17 +12,19 @@ const {connect} = require('react-redux'); const {createSelector} = require("reselect"); const {compose} = require('recompose'); const enhancer = require("./EditorEnhancer"); -const {cleanEditing, saveRule} = require("../../actions/rulesmanager"); +const {cleanEditing, saveRule, setLoading} = require("../../actions/rulesmanager"); const {activeRuleSelector} = require("../../selectors/rulesmanager"); -const {isEqual} = require("lodash"); - +const {isSaveDisabled, areDetailsActive, isRulePristine, isRuleValid, askConfirm} = require("../../utils/RulesEditor"); const Message = require('../../components/I18N/Message'); const BorderLayout = require("../../components/layout/BorderLayout"); const Header = require("../../components/manager/rulesmanager/ruleseditor/Header"); const MainEditor = require("../../components/manager/rulesmanager/ruleseditor/EditMain"); +const StylesEditor = require("../../components/manager/rulesmanager/ruleseditor/StylesEditor"); +const FiltersEditor = require("../../components/manager/rulesmanager/ruleseditor/FiltersEditor"); +const AttributesEditor = require("../../components/manager/rulesmanager/ruleseditor/AttributesEditor"); const ModalDialog = require("./ModalDialog"); -const {isRuleValid} = require("../../utils/RulesGridUtils"); + class RuleEditor extends React.Component { static propTypes = { @@ -31,9 +33,15 @@ class RuleEditor extends React.Component { activeEditor: PropTypes.string, onNavChange: PropTypes.func, setOption: PropTypes.func, + setConstraintsOption: PropTypes.func, onExit: PropTypes.func, onSave: PropTypes.func, - onDelete: PropTypes.func + onDelete: PropTypes.func, + styles: PropTypes.array, + type: PropTypes.string, + properties: PropTypes.array, + loading: PropTypes.bool, + cleanConstraints: PropTypes.func } static defaultProps = { activeEditor: "1", @@ -41,22 +49,34 @@ class RuleEditor extends React.Component { setOption: () => {}, onExit: () => {}, onSave: () => {}, - onDelete: () => {} + onDelete: () => {}, + type: "" } render() { - const {activeRule, activeEditor, onNavChange, setOption, initRule} = this.props; + const {loading, activeRule, activeEditor, onNavChange, initRule, styles = [], setConstraintsOption, type, properties} = this.props; const {modalProps} = this.state || {}; return ( }> - + header={
} + > + + + + ); } cancelEditing = () => { const {activeRule, initRule, onExit} = this.props; - if (!isEqual(activeRule, initRule)) { + if (!isRulePristine(activeRule, initRule)) { this.setState( () => ({modalProps: {title: "featuregrid.toolbar.saveChanges", showDialog: true, buttons: [{ text: , @@ -91,11 +111,36 @@ class RuleEditor extends React.Component { ], closeAction: this.cancel, msg: "rulesmanager.invalidForm"}})); } } + setOption = ({key, value}) => { + if (askConfirm(this.props.activeRule, key, value)) { + this.setState( () => ({modalProps: {title: "rulesmanager.resetconstraints", + showDialog: true, buttons: [{ + text: , + bsStyle: 'primary', + onClick: this.cancel + }, + { + text: , + bsStyle: 'primary', + onClick: () => { + this.cancel(); + this.props.setOption({key, value}); + this.props.cleanConstraints(); + } + } + ], closeAction: this.cancel, msg: "rulesmanager.constraintsmsg"}})); + + }else { + this.props.setOption({key, value}); + } + + } } module.exports = compose( connect(createSelector(activeRuleSelector, activeRule => ({activeRule})), { onExit: cleanEditing, - onSave: saveRule + onSave: saveRule, + setLoading }), enhancer)(RuleEditor); diff --git a/web/client/plugins/manager/RulesToolbar.jsx b/web/client/plugins/manager/RulesToolbar.jsx index 7e4148d7b0..721cae86e2 100644 --- a/web/client/plugins/manager/RulesToolbar.jsx +++ b/web/client/plugins/manager/RulesToolbar.jsx @@ -16,11 +16,12 @@ const Toolbar = require('../../components/misc/toolbar/Toolbar'); const Modal = require("./ModalDialog"); const Message = require("../../components/I18N/Message"); -const ToolbarWithModal = ({modalsProps, ...props}) => { +const ToolbarWithModal = ({modalsProps, loading, ...props}) => { return (
+
); }; diff --git a/web/client/product/pages/RulesManager.jsx b/web/client/product/pages/RulesManager.jsx index ca8eb44df0..0abeb9ff17 100644 --- a/web/client/product/pages/RulesManager.jsx +++ b/web/client/product/pages/RulesManager.jsx @@ -13,6 +13,7 @@ const url = require('url'); const urlQuery = url.parse(window.location.href, true).query; const ConfigUtils = require('../../utils/ConfigUtils'); +const Message = require("../../components/I18N/Message"); const {loadMapConfig} = require('../../actions/config'); const {resetControls} = require('../../actions/controls'); @@ -61,25 +62,21 @@ const HolyGrail = require('../../containers/HolyGrail'); * } * },.... * "plugins": { - * "rulesmanager": [{ - * "name": "OmniBar", - * "cfg": { - * "containerPosition": "header", - * "className": "navbar shadow navbar-home" - * } - * }, { - * "name": "Home", - * "override": { - * "OmniBar": { - * "position": 1, - * "priority": 1 - * } - * } - * },"Language", "Login", "Attribution", "RulesDataGrid", "Notifications", { - * "name": "RulesManagerFooter" , "cfg": { "containerPosition": "footer"} },{ - * "name": "RulesEditor", - * "containerPosition": "columns" - * }}] + * "rulesmanager": [ + * "Redirect" , + * { + * "name": "OmniBar", + * "cfg": { + * "containerPosition": "header", + * "className": "navbar shadow navbar-home" + * } + * }, "ManagerMenu", "Login", "Language", "NavMenu", + * "Attribution", "RulesDataGrid", "Notifications" + * ,{ + * "name": "RulesEditor", + * "cfg": {"containerPosition": "columns"} + * } + * ] * } */ @@ -118,7 +115,7 @@ class RulesManagerPage extends React.Component { pluginsConfig={pluginsConfig} plugins={this.props.plugins} params={this.props.match.params} - />) ||
; + />) ||
; } } diff --git a/web/client/reducers/rulesmanager.js b/web/client/reducers/rulesmanager.js index 1e3be061bf..ddff84cd2e 100644 --- a/web/client/reducers/rulesmanager.js +++ b/web/client/reducers/rulesmanager.js @@ -31,7 +31,8 @@ const defaultState = { "GetStyles" ] }, - triggerLoad: 0 + triggerLoad: 0, + grantDefault: "ALLOW" }; const getPosition = ({targetPosition = {}}, priority) => { @@ -130,7 +131,7 @@ function rulesmanager(state = defaultState, action) { case EDIT_RULE: { const {createNew, targetPriority} = action; if (createNew) { - return assign({}, state, {activeRule: {position: {value: getPosition(state, targetPriority), position: "offsetFromTop"}}}); + return assign({}, state, {activeRule: {grant: state.grantDefault, position: {value: getPosition(state, targetPriority), position: "offsetFromTop"}}}); } return assign({}, state, {activeRule: {...(state.selectedRules[0] || {}), position: {value: state.targetPosition.offsetFromTop, position: "offsetFromTop"}}}); } diff --git a/web/client/selectors/rulesmanager.js b/web/client/selectors/rulesmanager.js index 34f4c93be4..a0dbcb1454 100644 --- a/web/client/selectors/rulesmanager.js +++ b/web/client/selectors/rulesmanager.js @@ -61,9 +61,10 @@ const rulesEditorToolbarSelector = createSelector(selectedRules, targetPositionS showCache: sel.length === 0 }; }); +const isRulesManagerConfigured = state => state.localConfig && state.localConfig.plugins && !!state.localConfig.plugins.rulesmanager; const isEditorActive = state => state.rulesmanager && !!state.rulesmanager.activeRule; const triggerLoadSel = state => state.rulesmanager && state.rulesmanager.triggerLoad; - +const isLoading = state => state.rulesmanager && state.rulesmanager.loading; module.exports = { rulesSelector, optionsSelector, @@ -74,5 +75,7 @@ module.exports = { isEditorActive, activeRuleSelector, servicesConfigSel, - triggerLoadSel + triggerLoadSel, + isLoading, + isRulesManagerConfigured }; diff --git a/web/client/themes/default/less/rulesmanager.less b/web/client/themes/default/less/rulesmanager.less index 8ae317fd1c..5b7c370790 100644 --- a/web/client/themes/default/less/rulesmanager.less +++ b/web/client/themes/default/less/rulesmanager.less @@ -1,5 +1,189 @@ +.rm-alert-padded { + padding: 0px 15px; +} +.rules-manager-modal{ + + .ms-style-modal .mapstore-side-card.ms-sm.ms-selected { + color: #ffffff; + background-color: #078aa3; + -webkit-box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); + -moz-box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); + } + + + .ms-style-modal .ms2-border-layout-content { + padding: 0 15px; + } + .ms-style-modal .ms2-border-layout-content > span > div:last-child { + margin-bottom: 15px; + } + .ms-style-modal > div > span:first-child { + padding: 15px; + } + .mapstore-side-card.ms-sm { + width: 100% !important; + height: 52px !important; + display: flex; + } + .mapstore-side-card.ms-sm .mapstore-side-preview { + margin: 0 !important; + width: 52px !important; + height: 52px !important; + display: flex; + } + .mapstore-side-card.ms-sm .mapstore-side-card-info { + width: 100% !important; + height: 100% !important; + overflow: hidden; + } + .mapstore-side-card.ms-sm .mapstore-side-card-tool { + width: auto !important; + } + .mapstore-filter .input-group { + height: 32px; + width: 100%; + } + .mapstore-filter .input-group .input-group-addon { + position: absolute; + z-index: 2; + right: 0; + border-color: transparent; + background-color: transparent; + } + +} + + .rules-manager{ + .loading-header { + position: absolute; + top: 0px; + right: 0px; + } + .react-grid-Row + div { + border-bottom: 0px !important; + margin-top: -3px; + border-top: 4px dashed rgb(255, 238, 0); + } + .rdg-row-actions-cell { + transform: none !important; + } + position: absolute; + + textarea { + background-color: #f2f2f2; + color: #333333; + border-color: #dddddd; + outline: none !important; + } + + input, button, select, textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; + } + + + + /*codemirror */ + .react-codemirror2 { + + height: 100%; + width: 100%; + } + .react-codemirror2 { + margin-bottom: 10px; + height: @square-btn-size * 3; + } + .CodeMirror { + height: 100%; + } + + .CodeMirror-linenumber { + color: #f2f2f2; + } + + .CodeMirror-gutters { + background-color: #777; + border-color: #555; + } + + .CodeMirror-scroll { + background-color: #333; + color: #f2f2f2; + } + + .CodeMirror-cursor { + border-color: #f2f2f2; + } + + .CodeMirror-selected { + background-color: #777; + } + /* DEFAULT THEME */ + +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #d97fff;} +.cm-s-default .cm-number {color: #ffe399;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator { color: #9cffe0; } /**/ +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-comment {color: #777;} /**/ +.cm-s-default .cm-string {color: #9fffa1;} /**/ +.cm-s-default .cm-string-2 {color: #67f96a;} /**/ +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #ffe399;} /**/ +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #ffb39c;} /**/ +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-s-default .cm-error {color: #e00;} /**/ +.cm-invalidchar {color: #e00;} /**/ + +.CodeMirror-composing { border-bottom: 2px solid; } + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + .rulesmanager-editor{ + width: 500px; + } + + .rulesmanager-editor { + position: relative; + } + .rulesmanager-editor { + order: -1; + z-index: 1; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.06), 0 4px 8px rgba(0, 0, 0, 0.12); + border-right: 1px solid #dddddd; + } + + + .hide-locked-cell .react-grid-Cell--locked:focus { z-index: 9; } @@ -7,7 +191,9 @@ display: table-cell; width: 1%; } - + .ms-rule-editor .available-style-list { + padding-bottom: 5px; + } .ms-vertical-toolbar.rules-editor.re-toolbar{ order: -1; width: 52px; @@ -25,29 +211,20 @@ .autocomplete-toolbar, .autocomplete-toolbar span:focus, .rw-popup.rw-widget li:focus{ outline: none; } - - .mapstore-footer { - position: fixed; - width: 100%; - height: 52px; - display: flex; - -webkit-box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.06), 0 -4px 8px rgba(0, 0, 0, 0.12); - -moz-box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.06), 0 -4px 8px rgba(0, 0, 0, 0.12); - box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.06), 0 -4px 8px rgba(0, 0, 0, 0.12); - z-index: 10; - background-color: #ffffff; - bottom: 0; - justify-content: space-between; + .toolbar-loader { + position: absolute; + bottom: -7px; + left: -7px; } - .mapstore-footer .ms-circle-loader-md { + .ms-circle-loader-md { margin: 13px 18px; } - .mapstore-footer .ms-circle-loader-md { + .ms-circle-loader-md { text-indent: -9999em; - border-top: 3.25px solid rgba(7, 138, 163, 0.2); - border-right: 3.25px solid rgba(7, 138, 163, 0.2); - border-bottom: 3.25px solid rgba(7, 138, 163, 0.2); - border-left: 3.25px solid #078aa3; + border-top: 3.5px solid rgba(7, 138, 163, 0.2); + border-right: 3.5px solid rgba(7, 138, 163, 0.2); + border-bottom: 3.5px solid rgba(7, 138, 163, 0.2); + border-left: 3.5px solid #078aa3; -webkit-transform: translateZ(0); -ms-transform: translateZ(0); transform: translateZ(0); @@ -58,21 +235,6 @@ height: 26px; } - - .mapstore-footer .ms-logo-geosolutions { - height: 52px; - width: 156px; - overflow: hidden; - margin-right: 26px; - display: flex; - } - .ms-logo-geosolutions img{ - width: 100%; - height: auto; - margin: auto; - } - - #mapstore-navbar-container { margin-bottom: 0; z-index: 100; @@ -329,12 +491,6 @@ & > div:first-child { flex: 1; } - .glyphicon { - - &:hover { - - } - } } } \ No newline at end of file diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE index 85674b6eca..d098d15e1c 100644 --- a/web/client/translations/data.de-DE +++ b/web/client/translations/data.de-DE @@ -1362,6 +1362,12 @@ "layerlabel": "Ebene" }, "rulesmanager": { + "resetconstraints": "Setzt Einschränkungen zurück", + "constraintsmsg": "Wenn Sie die Berechtigung, den Arbeitsbereich oder die Ebene ändern, werden die Details gelöscht. Bist du sicher, dass du das machen willst?", + "defstyle": "Standardstil", + "avstyle": "Verfügbare Stile", + "clearbtn": "Alles Löschen", + "selectbtn": "Wählen Sie Alle", "placeholders": { "filter": "Suche...", "role": "Nach Rollen suchen", @@ -1392,6 +1398,7 @@ "filter": "Filter", "attribute": "Attribute Regel" }, + "rule": "Regle", "cachetitle": "Cache leeren", "cachemsg": "Möchten Sie den GeoFence-Cache wirklich löschen?", "deltitle": "Regel Löschen", @@ -1426,10 +1433,15 @@ "errorDeletingRules": "Fehler beim Löschen der Regeln.", "errorAddingRule": "Fehler beim Hinzufügen der Regel.", "errorUpdatingRule": "Fehler beim Ändern der Regel.", + "errorDuplicateRule": "Duplizierte Regel", + "errorLoading": "Ladefehler", "deleteModal": "Regeln löschen", "selectedRulesDelete": "Ausgewählte Regeln löschen ?", "deleteButton": "Löschen", - "cancelButton": "Abbrechen" + "cancelButton": "Abbrechen", + "cqlRead": "CQL Filter Lese Regeln", + "cqlWrite": "CQL Filter Write Rules", + "missingconfig": "Der Regeln-Manager vermisst die Konfiguration. Zugriff abgelehnt!" }, "tutorial": { "title": "Anleitung", diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US index fbbb8d9701..96272accf2 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -1363,6 +1363,12 @@ "layerlabel": "Layer" }, "rulesmanager": { + "resetconstraints": "Resets Constraints", + "constraintsmsg": "Changing grant, workspace or layer, the details will be deleted. Are you sure you want to do that?", + "defstyle": "Default Style", + "avstyle": "Available Styles", + "clearbtn": "Clear All", + "selectbtn": "Select All", "placeholders": { "filter": "Search...", "role": "Type to search Roles", @@ -1374,7 +1380,8 @@ "layer": "Type to search Layers", "access": "Type to search Access", "ip": "###.###.###.###/##", - "priority": "Select Priority" + "priority": "Select Priority", + "filter": "Filter styles..." }, "menutitle": "Manage GeoFence Rules", "tooltip": { @@ -1393,6 +1400,7 @@ "filter": "Filters", "attribute": "Attributes Rule" }, + "rule": "Rule", "cachetitle": "Clear Cache", "cachemsg": "Are you sure to clear the GeoFence cache?", "deltitle": "Delete Rule", @@ -1426,11 +1434,16 @@ "errorMovingRules": "Error moving rules.", "errorDeletingRules": "Error deleting rules.", "errorAddingRule": "Error adding rule.", - "errorUpdatingRule": "Error updating rule.", + "errorUpdatingRule": "Error saving rule.", + "errorDuplicateRule": "Duplicated rule.", + "errorLoading": "Error loading", "deleteModal": "Delete Rules", "selectedRulesDelete": "Delete selected rules ?", "deleteButton": "Delete", - "cancelButton": "Cancel" + "cancelButton": "Cancel", + "cqlRead": "CQL Filter Read Rules", + "cqlWrite": "CQL Filter Write Rules", + "missingconfig": "Rules manager misses the configuration. Access denied!" }, "tutorial": { "title": "Tutorial", diff --git a/web/client/translations/data.es-ES b/web/client/translations/data.es-ES index 329c1d42e0..f0d3a452bf 100644 --- a/web/client/translations/data.es-ES +++ b/web/client/translations/data.es-ES @@ -1362,6 +1362,12 @@ "layerlabel": "Capa" }, "rulesmanager": { + "resetconstraints": "Restablece las restricciones", + "constraintsmsg": "Cambiando concesión, área de trabajo o capa, los detalles serán eliminados. ¿Estás seguro de que quieres hacer eso?", + "defstyle": "Estilo por Defecto", + "avstyle": "Estilos disponibles", + "clearbtn": "Limpiar Todo", + "selectbtn": "Seleccionar Todo", "placeholders": { "filter": "Buscar...", "role": "Escriba para buscar Roles", @@ -1392,6 +1398,7 @@ "filter": "Filtros", "attribute": "Regla de atributos" }, + "rule": "Regla", "cachetitle": "Limpiar Cache", "cachemsg": "¿Estás seguro de limpiar el caché GeoFence?", "deltitle": "Eliminar Regla", @@ -1426,10 +1433,15 @@ "errorDeletingRules": "Error al borrar las reglas.", "errorAddingRule": "Error al añadir las reglas.", "errorUpdatingRule": "Error al actualizar las reglas.", + "errorDuplicateRule": "Regla duplicada.", + "errorLoading": "Error de carga", "deleteModal": "Borrar la regla", "selectedRulesDelete": "¿Está seguro de borrar las reglas seleccionadas?", "deleteButton": "Borrar", - "cancelButton": "Cancelar" + "cancelButton": "Cancelar", + "cqlRead": "Reglas de lectura del filtro CQL", + "cqlWrite": "Reglas de escritura de filtro CQL", + "missingconfig": "El administrador de reglas pierde la configuración. ¡Acceso denegado!" }, "tutorial": { "title": "Tutorial", diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR index ae915afad3..e102e6d194 100644 --- a/web/client/translations/data.fr-FR +++ b/web/client/translations/data.fr-FR @@ -1362,6 +1362,12 @@ "layerlabel": "Couche" }, "rulesmanager": { + "resetconstraints": "Réinitialise les contraintes", + "constraintsmsg": "En changeant la subvention, l'espace de travail ou la couche, les détails seront supprimés. Êtes-vous sûr de vouloir faire ça?", + "defstyle": "Style par Défaut", + "avstyle": "Styles Disponibles", + "clearbtn": "Tout Effacer", + "selectbtn": "Tout Sélectionner", "placeholders": { "filter": "Chercher...", "role": "Tapez pour rechercher des rôles", @@ -1392,6 +1398,7 @@ "filter": "Filters", "attribute": "Règle des attributs" }, + "rule": "Règle", "cachetitle": "Vider le Cache", "cachemsg": "Etes-vous sûr d'effacer le cache de GeoFence?", "deltitle": "Supprimer la règle", @@ -1425,11 +1432,16 @@ "errorMovingRules": "Erreur lors du déplacement des règles.", "errorDeletingRules": "Erreur de suppression des règles.", "errorAddingRule": "Erreur lors de l'ajout de la règle.", + "errorDuplicateRule": "Règle dupliquée", + "errorLoading": "Erreur de chargement", "errorUpdatingRule": "Erreur lors de la mise à jour règle.", "deleteModal": "Supprimer la règle", "selectedRulesDelete": "Voulez-vous supprimer les règles sélectionnées ?", "deleteButton": "Effacer", - "cancelButton": "Annuler" + "cancelButton": "Annuler", + "cqlRead": "Règles de lecture du filtre CQL", + "cqlWrite": "Règles d'écriture du filtre CQL", + "missingconfig": "Le gestionnaire de règles manque la configuration. Accès refusé!" }, "tutorial": { "title": "Tutoriel", diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index 78aafe91ca..2aaeecc4ae 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -1362,6 +1362,12 @@ "layerlabel": "Layer" }, "rulesmanager": { + "resetconstraints": "Resetta Vincoli", + "constraintsmsg": "Cambinado grant, workspace o layer, i dettagli saranno eliminati. Sei sicuro di volerlo fare?", + "defstyle": "Stile Predefinito", + "avstyle": "Stili Disponibile", + "clearbtn": "Rimuovi Tutti", + "selectbtn": "Seleziona Tutti", "placeholders": { "filter": "Ricerca...", "role": "Cerca Gruppo", @@ -1391,7 +1397,8 @@ "style": "Stile", "filter": "Filtri", "attribute": "Attributi" - }, + }, + "rule": "Regola", "cachetitle": "Pulisci Cache", "cachemsg": "Sei sicuro di voler rimuovere la cache di Geofance?", "deltitle": "Rimuovi Regola", @@ -1426,10 +1433,15 @@ "errorDeletingRules": "Errore durante la cancellazione della regola.", "errorAddingRule": "Errore durante la creazione della regola.", "errorUpdatingRule": "Errore durante l'aggiornamento della regola.", + "errorDuplicateRule": "Regola duplicata", + "errorLoading": "Errore caricamento", "deleteModal": "Elimina Regola", "selectedRulesDelete": "Vuoi eliminare le regole selezionate?", "deleteButton": "Elimina", - "cancelButton": "Annulla" + "cancelButton": "Annulla", + "cqlRead": "Filtro CQL lettura", + "cqlWrite": "Filtero CQL scrittura", + "missingconfig": "Manca la configurazione del Rules Manager. Accesso negato!" }, "tutorial": { "title": "Tutorial", diff --git a/web/client/utils/RulesEditor.js b/web/client/utils/RulesEditor.js new file mode 100644 index 0000000000..2d2eba382b --- /dev/null +++ b/web/client/utils/RulesEditor.js @@ -0,0 +1,32 @@ + /** + * 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 {isEqual, isEmpty} = require("lodash"); +const checkIp = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.)){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?(\/)(?:3[0-2]|[1-2]?[0-9]))\b/g; +const RulesEditorUtils = { + isSaveDisabled: (currentRule, initRule) => { + return RulesEditorUtils.isRulePristine(currentRule, initRule) && initRule.hasOwnProperty("id"); + }, + areDetailsActive: ({layer, grant} = {}) => { + return !!layer && grant === "ALLOW"; + }, + isRulePristine: (currentRule, initRule) => { + return isEqual(currentRule, initRule); + }, + isRuleValid: ({ipaddress = ""} = {}) => { + if (ipaddress.length > 0 ) { + return !!ipaddress.match(checkIp); + } + return true; + }, + askConfirm: ({constraints = {}} = {}, key, value) => { + return !isEmpty(constraints) && (key === "workspace" || key === "layer" || (key === "grant" && value === "DENY")); + }, + checkIp +}; + +module.exports = RulesEditorUtils; diff --git a/web/client/utils/RulesGridUtils.js b/web/client/utils/RulesGridUtils.js index 5b70d37225..b5577f7c39 100644 --- a/web/client/utils/RulesGridUtils.js +++ b/web/client/utils/RulesGridUtils.js @@ -26,7 +26,7 @@ const getIdxFarthestEl = (avgIdx, pages = [], firstIdx, lastIdx) => { return pages.map(val => firstIdx <= val && val <= lastIdx ? 0 : Math.abs(val - avgIdx)).map((distance, idx) => ({idx: pages[idx], distance})).sort((a, b) => a.distance - b.distance).reverse().map(({idx}) => idx); }; -const checkIp = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.)){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?(\/)(?:3[0-2]|[1-2]?[0-9]))\b/g; + module.exports = { getPageIdx, getRow, @@ -67,13 +67,6 @@ module.exports = { return { pages: {...tempPages, ...newPages}}; }, flattenPages: (pages = {}) => Object.keys(pages).reduce((rows, key) => rows.concat((pages[key] || [])), []), - checkIp, - isRuleValid: (rule = {}) => { - if (rule.ipaddress && rule.ipaddress.length > 0 ) { - return !!rule.ipaddress.match(checkIp); - } - return true; - }, getOffsetFromTop: (row, rows) => rows.indexOf(row), getClosestRows: (row, rows) => { const idx = rows.indexOf(row);