diff --git a/package.json b/package.json index c1cb57b80b..6cda713fbd 100644 --- a/package.json +++ b/package.json @@ -196,6 +196,7 @@ "url": "0.10.3", "uuid": "3.0.1", "w3c-schemas": "1.3.1", + "wellknown": "0.5.0", "wkt-parser": "https://github.com/geosolutions-it/wkt-parser/tarball/mapstore2_fixes", "xml2js": "0.4.17" }, diff --git a/web/client/actions/rulesmanager.js b/web/client/actions/rulesmanager.js index e0b85bc043..2ea4acf47e 100644 --- a/web/client/actions/rulesmanager.js +++ b/web/client/actions/rulesmanager.js @@ -24,8 +24,14 @@ const EDIT_RULE = "RULES_MANAGER:EDIT_RULE"; const CLEAN_EDITING = "RULES_MANAGER:CLEAN_EDITING"; const SAVE_RULE = "RULES_MANAGER:SAVE_RULE"; const RULE_SAVED = "RULES_MANAGER:RULE_SAVED"; -const DELETE_RULES = "RULES_MANAGER: DELETE_RULES"; +const DELETE_RULES = "RULES_MANAGER:DELETE_RULES"; +const CACHE_CLEAN = "RULES_MANAGER:CACHE_CLEAN"; +function onCacheClean() { + return { + type: CACHE_CLEAN + }; +} function delRules(ids) { return { type: DELETE_RULES, @@ -277,5 +283,6 @@ module.exports = { EDIT_RULE, onEditRule, CLEAN_EDITING, cleanEditing, SAVE_RULE, saveRule, RULE_SAVED, - DELETE_RULES, delRules + DELETE_RULES, delRules, + CACHE_CLEAN, onCacheClean }; diff --git a/web/client/api/WMS.js b/web/client/api/WMS.js index b454fd2d4f..c9bce21cca 100644 --- a/web/client/api/WMS.js +++ b/web/client/api/WMS.js @@ -189,10 +189,10 @@ const Api = { }, getBBox: function(record, bounds) { let layer = record; - let bbox = (layer.EX_GeographicBoundingBox || CoordinatesUtils.getWMSBoundingBox(layer.BoundingBox) || (layer.LatLonBoundingBox && layer.LatLonBoundingBox.$) || layer.latLonBoundingBox); + let bbox = (layer.EX_GeographicBoundingBox || layer.exGeographicBoundingBox || CoordinatesUtils.getWMSBoundingBox(layer.BoundingBox) || (layer.LatLonBoundingBox && layer.LatLonBoundingBox.$) || layer.latLonBoundingBox); while (!bbox && layer.Layer && layer.Layer.length) { layer = layer.Layer[0]; - bbox = (layer.EX_GeographicBoundingBox || CoordinatesUtils.getWMSBoundingBox(layer.BoundingBox) || (layer.LatLonBoundingBox && layer.LatLonBoundingBox.$) || layer.latLonBoundingBox); + bbox = (layer.EX_GeographicBoundingBox || layer.exGeographicBoundingBox || CoordinatesUtils.getWMSBoundingBox(layer.BoundingBox) || (layer.LatLonBoundingBox && layer.LatLonBoundingBox.$) || layer.latLonBoundingBox); } if (!bbox) { bbox = { diff --git a/web/client/api/geoserver/GeoFence.js b/web/client/api/geoserver/GeoFence.js index f61f9d4e26..609aa1e52d 100644 --- a/web/client/api/geoserver/GeoFence.js +++ b/web/client/api/geoserver/GeoFence.js @@ -11,9 +11,7 @@ const assign = require('object-assign'); const ConfigUtils = require('../../utils/ConfigUtils'); const EMPTY_RULE = { - constraints: { - allowedStyles: {style: []} - }, + constraints: {}, ipaddress: "", layer: "", request: "", @@ -25,14 +23,24 @@ const EMPTY_RULE = { const cleanConstraints = (rule) => { if (!rule.constraints ) { return rule; + }else if (rule.grant === "DENY") { + const {constraints: omit, ...r} = rule; + return r; } let constraints = {...rule.constraints}; constraints.allowedStyles = constraints.allowedStyles && constraints.allowedStyles.style || []; constraints.attributes = constraints.attributes && constraints.attributes.attribute || []; + constraints.restrictedAreaWkt = constraints.restrictedAreaWkt || ""; return {...rule, constraints}; }; var Api = { - + cleanCache: () => { + return axios.get('rest/geofence/ruleCache/invalidate', Api.addBaseUrlGS()) + .then(function(response) { + return response.data; + } + ); + }, loadRules: function(page, rulesFiltersValues, entries = 10) { const params = { page, @@ -92,9 +100,9 @@ var Api = { updateRule: function(rule) { // id, priority and grant aren't updatable - const {id, priority, grant, position, ...others} = rule; + const {id, priority, grant, position, ...others} = cleanConstraints(rule); const newRule = {...EMPTY_RULE, ...others}; - return axios.put(`geofence/rest/rules/id/${id}`, cleanConstraints(newRule), this.addBaseUrl({ + return axios.put(`geofence/rest/rules/id/${id}`, newRule, this.addBaseUrl({ 'headers': { 'Content': 'application/json' } diff --git a/web/client/components/manager/rulesmanager/MapModal.jsx b/web/client/components/manager/rulesmanager/MapModal.jsx new file mode 100644 index 0000000000..0e2b1900c5 --- /dev/null +++ b/web/client/components/manager/rulesmanager/MapModal.jsx @@ -0,0 +1,41 @@ +/** +* 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 enhancer = require("./enhancers/Map"); +const autoMapType = require('../../map/enhancers/autoMapType'); +const mapType = require('../../map/enhancers/mapType'); +const autoResize = require('../../map/enhancers/autoResize'); +const onMapViewChanges = require('../../map/enhancers/onMapViewChanges'); +const withDraw = require("../../map/enhancers/withDraw"); +const {compose} = require('recompose'); + +const MapWitDraw = compose( + enhancer, + onMapViewChanges, + autoResize(0), + autoMapType, + mapType, + withDraw() +)(require('../../map/BaseMap')); + +const Portal = require('react-overlays').Portal; + +module.exports = ({layer, onMapReady = () => {}}) => { + return ( + +
+ +
+
); +}; + diff --git a/web/client/components/manager/rulesmanager/RoiCql.jsx b/web/client/components/manager/rulesmanager/RoiCql.jsx new file mode 100644 index 0000000000..cf00d9271e --- /dev/null +++ b/web/client/components/manager/rulesmanager/RoiCql.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 PropTypes = require('prop-types'); +const Button = require("../../misc/toolbar/ToolbarButton"); +const ContainerDimensions = require('react-container-dimensions').default; +const {Controlled: Codemirror} = require('react-codemirror2'); +require('codemirror/lib/codemirror.css'); +require('codemirror/mode/sql/sql'); +const LocaleUtils = require('../../../utils/LocaleUtils'); + + +class RoiCql extends React.Component { + static propTypes = { + wkt: PropTypes.string, + onChangeFilter: PropTypes.func + }; + static contextTypes = { + messages: PropTypes.object + }; + constructor(props) { + super(props); + this.state = {cql: props.wkt}; + } + componentWillReceiveProps({wkt: nw}) { + if (nw !== this.props.wkt) { + this.setState({cql: nw}); + } + } + onChange = (editor, data, value) => { + this.setState(() => ({cql: value})); + } + render() { + return ( + + {({width}) => +
+ +
} +
); + } + apply = () => { + if (this.props.onChangeFilter) { + this.props.onChangeFilter(this.state.cql); + } + } +} + +module.exports = RoiCql; diff --git a/web/client/components/manager/rulesmanager/SimpleSpatialFilter.jsx b/web/client/components/manager/rulesmanager/SimpleSpatialFilter.jsx new file mode 100644 index 0000000000..0e5a992253 --- /dev/null +++ b/web/client/components/manager/rulesmanager/SimpleSpatialFilter.jsx @@ -0,0 +1,212 @@ +/** + * 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 {find} = require('lodash'); +const PropTypes = require('prop-types'); +const {Row, Col, Panel, Glyphicon} = require('react-bootstrap'); +const wk = require("wellknown"); +const ComboField = require("../../data/query/ComboField"); + +const ComboFieldListItem = require('../../data/query/ComboFieldListItem'); +const RoiCql = require("./RoiCql"); + +const LocaleUtils = require('../../../utils/LocaleUtils'); +const SwitchPanel = require('../../misc/switch/SwitchPanel'); +const I18N = require('../../I18N/I18N'); + +class SpatialFilter extends React.Component { + static propTypes = { + useMapProjection: PropTypes.bool, + spatialField: PropTypes.object, + spatialMethodOptions: PropTypes.array, + spatialPanelExpanded: PropTypes.bool, + actions: PropTypes.object, + zoom: PropTypes.number, + owner: PropTypes.string, + features: PropTypes.array, + wkt: PropTypes.string, + onError: PropTypes.func + }; + + static contextTypes = { + messages: PropTypes.object + }; + + static defaultProps = { + useMapProjection: true, + spatialField: {}, + spatialPanelExpanded: true, + spatialOperations: [], + owner: "queryform", + features: [], + actions: { + onExpandSpatialFilterPanel: () => {}, + onSelectSpatialMethod: () => {}, + onChangeSpatialFilterValue: () => {}, + onChangeDrawingStatus: () => {}, + onRemoveSpatialSelection: () => {} + } + }; + getMethodFromId = (id) => { + return find(this.props.spatialMethodOptions, method => method && method.id === id) || null; + }; + renderHeader = () => { + const spatialFilterHeader = LocaleUtils.getMessageById(this.context.messages, "queryform.spatialfilter.spatial_filter_header"); + return ( + + {spatialFilterHeader} + + + ); + }; + + renderSpatialHeader = () => { + const selectedMethod = this.getMethodFromId(this.props.spatialField.method); + return ( +
+ + +
+ + + + ()} + fieldOptions={ + this.props.spatialMethodOptions.map((opt) => { + return LocaleUtils.getMessageById(this.context.messages, opt.name) || opt.name; + }) + } + placeholder={LocaleUtils.getMessageById(this.context.messages, "queryform.spatialfilter.combo_placeholder")} + fieldName="method" + fieldRowId={new Date().getTime()} + fieldValue={ + LocaleUtils.getMessageById(this.context.messages, selectedMethod ? selectedMethod.name : "") || selectedMethod && selectedMethod.name || "" + } + onUpdateField={this.updateSpatialMethod}/> + +
+
+ ); + }; + renderSpatialPanel = () => { + const showLabel = this.getMethodFromId(this.props.spatialField.method) && this.props.spatialField.method !== "CQL" && !this.props.spatialField.geometry; + return ( + + {this.props.spatialMethodOptions.length > 1 ? this.renderSpatialHeader() : } + { this.props.spatialField.method === "CQL" && ( + + )} + + + {showLabel && ( + +
+ +
+
) + } + +
+
+ ); + }; + renderButtons = () => { + const buttons = []; + const showReset = this.props.spatialField.geometry && this.props.spatialField.geometry.coordinates; + if (showReset) { + buttons.push({ + glyph: 'clear-filter', + tooltipId: "rulesmanager.remove", + onClick: () => this.resetSpatialFilter() + }); + } + return buttons; + }; + render() { + return ( +
+ this.props.actions.onExpandSpatialFilterPanel(expanded)} + > + {this.renderSpatialPanel()} + +
+ ); + } + + updateSpatialMethod = (id, name, value) => { + const method = this.props.spatialMethodOptions.filter((opt) => { + if (value === (LocaleUtils.getMessageById(this.context.messages, opt.name) || opt.name)) { + return opt; + } + })[0].id; + + this.props.actions.onSelectSpatialMethod(method, name); + switch (method) { + case "CQL": + return; + default: { + this.changeDrawingStatus('start', method, {stopAfterDrawing: true}); + } + } + + }; + + resetSpatialFilter = () => { + this.changeDrawingStatus('clean', null); + }; + changeDrawingStatus = (status, method, options) => { + this.props.actions.onChangeDrawingStatus( + status, + method !== undefined ? method : this.props.spatialField.method, + this.props.owner, + this.props.features, + options); + }; + cqlChanged = (cql) => { + if (cql.length === 0) { + this.props.actions.onChangeDrawingStatus( + "clean", + "", + this.props.owner, + [], + {}); + return; + } + try { + const geometry = wk.parse(cql); + if (geometry) { + this.props.actions.onChangeDrawingStatus( + "replace", + "", + this.props.owner, + [geometry], + {}); + }else { + throw new Error(); + } + + }catch (err) { + this.props.actions.onError({title: "rulesmanager.errorTitle", message: "rulesmanager.errorCQL"}); + } + } +} + +module.exports = SpatialFilter; diff --git a/web/client/components/manager/rulesmanager/enhancers/Map.js b/web/client/components/manager/rulesmanager/enhancers/Map.js new file mode 100644 index 0000000000..14ce9218e9 --- /dev/null +++ b/web/client/components/manager/rulesmanager/enhancers/Map.js @@ -0,0 +1,74 @@ +/** +* 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, defaultProps, withPropsOnChange, withProps} = require("recompose"); +const MapUtils = require("../../../../utils/MapUtils"); +const CoordinatesUtils = require("../../../../utils/CoordinatesUtils"); +const {isEmpty} = require("lodash"); +const defaultBaseLayer = { + group: "background", + id: "mapnik__0", + loading: false, + loadingError: false, + name: "mapnik", + source: "osm", + title: "Open Street Map", + type: "osm", + visibility: true +}; + + +module.exports = compose( + defaultProps({ + onMapReady: () => {}, + baseLayer: defaultBaseLayer + + }), + withPropsOnChange("baseLayer", props => { + return {layers: [props.baseLayer]}; + }), + withStateHandlers(() => ({ + map: { projection: "EPSG:4326" }, + initialized: false + }), + { + onMapViewChanges: () => (map) => ( {map}), + onLayerLoad: ({map, initialized}, {onMapReady, baseLayer}) => (layerId) => { + // Map is ready when background is loaded just first load + if (!initialized && layerId === baseLayer.id) { + onMapReady(map); + return {initialized: true}; + } + }, + centerLayer: (state, {layer: l}) => (map) => { + if (isEmpty(l)) { + return {}; + } + const center = MapUtils.getCenterForExtent(l.bbox.extent, l.bbox.crs); + const extent = l.bbox.crs !== (map.projection || "EPSG:3857") ? CoordinatesUtils.reprojectBbox(l.bbox.extent, l.bbox.crs, map.projection || "EPSG:3857") : l.bbox.extent; + let zoom = 1; + if (map.size) { + zoom = MapUtils.getZoomForExtent(extent, map.size, 0, 21); + } + const {bbox: omit, ...om} = map; + return {map: {...om, zoom, center, extent, mapStateSource: "mapModal"}}; + } + }), + withPropsOnChange("layer", props => { + return {layers: props.layers.concat(props.layer || [])}; + }), + withPropsOnChange(({map = {}}, {map: nM}) => { + return (!map.size && nM.size); + }, + ({ centerLayer, map}) => { + if (map.size) { + centerLayer(map); + } + return {}; + }), + withProps(({onLayerLoad}) => ({eventHandlers: {onLayerLoad}})) +); diff --git a/web/client/components/manager/rulesmanager/enhancers/__tests__/autoComplete-test.js b/web/client/components/manager/rulesmanager/enhancers/__tests__/autoComplete-test.js index e9ec8551f1..96849758e7 100644 --- a/web/client/components/manager/rulesmanager/enhancers/__tests__/autoComplete-test.js +++ b/web/client/components/manager/rulesmanager/enhancers/__tests__/autoComplete-test.js @@ -27,12 +27,20 @@ describe('autoComplete enhancer', () => { setTimeout(done); }); it('it calls load function', (done) => { + let counter = 0; const Sink = autoComplete(createSink( props => { - expect(props).toExist(); - expect(props.onChange).toExist(); - expect(props.onSelect).toExist(); - expect(props.onToggle).toExist(); - props.onChange("%"); + switch (counter) { + case 0: { + expect(props).toExist(); + expect(props.onChange).toExist(); + expect(props.onSelect).toExist(); + expect(props.onToggle).toExist(); + props.onChange("%"); + counter++; + break; + } + default: return; + } })); const loadData = (search, page, size, parentsFilter, count) => { expect(search).toBe("%"); @@ -46,11 +54,19 @@ describe('autoComplete enhancer', () => { loadData={loadData}/>, document.getElementById("container")); }); it('it emits selected val', (done) => { + let counter = 0; const Sink = autoComplete(createSink( props => { - expect(props).toExist(); - expect(props.onValueSelected).toExist(); - expect(props.onSelect).toExist(); - props.onSelect("%"); + switch (counter) { + case 0: { + expect(props).toExist(); + expect(props.onValueSelected).toExist(); + expect(props.onSelect).toExist(); + props.onSelect("%"); + counter++; + break; + } + default: return; + } })); const onValueSelected = (val) => { expect(val).toBe("%"); @@ -59,13 +75,118 @@ describe('autoComplete enhancer', () => { ReactDOM.render(, document.getElementById("container")); }); it('on toggle with empty val it cleans selected', (done) => { + let counter = 0; const Sink = autoComplete(createSink( props => { - expect(props).toExist(); - expect(props.selectedValue).toBe("%"); - expect(props.onValueSelected).toExist(); - expect(props.onSelect).toExist(); - props.onChange(""); - props.onToggle(false); + switch (counter) { + case 0: { + expect(props).toExist(); + expect(props.selectedValue).toBe("%"); + expect(props.onValueSelected).toExist(); + expect(props.onSelect).toExist(); + props.onChange(""); + counter++; + break; + } + case 1: { + expect(props.onToggle).toExist(); + props.onToggle(false); + counter++; + break; + } + default: { + return; + } + } + })); + const onValueSelected = (val) => { + expect(val).toNotExist(); + done(); + }; + ReactDOM.render(, document.getElementById("container")); + }); + it('on toggle', (done) => { + let counter = 0; + const Sink = autoComplete(createSink( props => { + switch (counter) { + case 0: { + expect(props).toExist(); + expect(props.selectedValue).toBe("%"); + props.onChange("TEST"); + counter++; + break; + } + case 1: { + expect(props).toExist(); + expect(props.selectedValue).toBe("TEST"); + expect(props.typing).toBeTruthy(); + props.onToggle(false); + counter++; + break; + } + case 2: { + expect(props).toExist(); + expect(props.selectedValue).toBe("%"); + expect(props.typing).toBeFalsy(); + props.onToggle(false); + counter++; + done(); + break; + } + default: + return counter++; + } + })); + ReactDOM.render(, document.getElementById("container")); + }); + it('on toggle call onLoad function', (done) => { + let counter = 0; + const Sink = autoComplete(createSink( props => { + switch (counter) { + case 0: { + expect(props).toExist(); + props.onToggle(true); + counter++; + break; + } + case 1: { + expect(props).toExist(); + expect(props.emptyReq).toBe(1); + break; + } + default: + return counter++; + } + })); + const loadData = (search, page, size, parentsFilter, count) => { + expect(search).toBe("%"); + expect(page).toBe(0); + expect(size).toBe(5); + expect(count).toBe(true); + done(); + }; + ReactDOM.render(, document.getElementById("container")); + }); + it('on rest clean the selected value', (done) => { + let counter = 0; + const Sink = autoComplete(createSink( props => { + switch (counter) { + case 0: { + expect(props).toExist(); + expect(props.selectedValue).toBe("%"); + props.onChange("TEST"); + counter++; + break; + } + case 1: { + expect(props).toExist(); + expect(props.selectedValue).toBe("TEST"); + props.onReset(); + counter++; + break; + } + default: + return {}; + } })); const onValueSelected = (val) => { expect(val).toNotExist(); diff --git a/web/client/components/manager/rulesmanager/enhancers/__tests__/fixedOptions-test.js b/web/client/components/manager/rulesmanager/enhancers/__tests__/fixedOptions-test.js new file mode 100644 index 0000000000..45f5d07c90 --- /dev/null +++ b/web/client/components/manager/rulesmanager/enhancers/__tests__/fixedOptions-test.js @@ -0,0 +1,142 @@ +/* + * 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 expect = require('expect'); +const fixedOptions = require('../fixedOptions'); + +const rxjsConfig = require('recompose/rxjsObservableConfig').default; +setObservableConfig(rxjsConfig); + + +describe('fixedOption enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('it emits selected val', (done) => { + let counter = 0; + const Sink = fixedOptions(createSink( props => { + switch (counter) { + case 0: { + expect(props).toExist(); + expect(props.onValueSelected).toExist(); + expect(props.onSelect).toExist(); + props.onSelect("%"); + counter++; + break; + } + default: return; + } + })); + const onValueSelected = (val) => { + expect(val).toBe("%"); + done(); + }; + ReactDOM.render(, document.getElementById("container")); + }); + it('on toggle with empty val it cleans selected', (done) => { + let counter = 0; + const Sink = fixedOptions(createSink( props => { + switch (counter) { + case 0: { + expect(props).toExist(); + expect(props.selectedValue).toBe("%"); + expect(props.onValueSelected).toExist(); + expect(props.onSelect).toExist(); + props.onChange(""); + counter++; + break; + } + case 1: { + expect(props.onToggle).toExist(); + props.onToggle(false); + counter++; + break; + } + default: { + return; + } + } + })); + const onValueSelected = (val) => { + expect(val).toNotExist(); + done(); + }; + ReactDOM.render(, document.getElementById("container")); + }); + it('on toggle', (done) => { + let counter = 0; + const Sink = fixedOptions(createSink( props => { + switch (counter) { + case 0: { + expect(props).toExist(); + expect(props.selectedValue).toBe("%"); + props.onChange("TEST"); + counter++; + break; + } + case 1: { + expect(props).toExist(); + expect(props.selectedValue).toBe("TEST"); + expect(props.typing).toBeTruthy(); + props.onToggle(false); + counter++; + break; + } + case 2: { + expect(props).toExist(); + expect(props.selectedValue).toBe("%"); + expect(props.typing).toBeFalsy(); + props.onToggle(false); + counter++; + done(); + break; + } + default: + return counter++; + } + })); + ReactDOM.render(, document.getElementById("container")); + }); + it('on rest clean the selected value', (done) => { + let counter = 0; + const Sink = fixedOptions(createSink( props => { + switch (counter) { + case 0: { + expect(props).toExist(); + expect(props.selectedValue).toBe("%"); + props.onChange("TEST"); + counter++; + break; + } + case 1: { + expect(props).toExist(); + expect(props.selectedValue).toBe("TEST"); + props.onReset(); + counter++; + break; + } + default: + return {}; + } + })); + const onValueSelected = (val) => { + expect(val).toNotExist(); + done(); + }; + ReactDOM.render(, document.getElementById("container")); + }); +}); diff --git a/web/client/components/manager/rulesmanager/enhancers/autoComplete.js b/web/client/components/manager/rulesmanager/enhancers/autoComplete.js index d4510e0e3d..1040f6be34 100644 --- a/web/client/components/manager/rulesmanager/enhancers/autoComplete.js +++ b/web/client/components/manager/rulesmanager/enhancers/autoComplete.js @@ -94,6 +94,7 @@ const dataStreamFactory = prop$ => { module.exports = compose( defaultProps({ + clearable: true, emptySearch: "%", stopPropagation: true, paginated: true, @@ -135,6 +136,14 @@ module.exports = compose( page, count }), + onReset: ({typing, val}, {onValueSelected, selected}) => () => { + if (selected) { + onValueSelected(); + } + if (typing) { + return {typing: false}; + } + }, onChange: ({stopChange, val: oldVal, emptyReq}, {initialData = [], valueField}) => (val = "") => { if (stopChange) { return {stopChange: false}; @@ -143,8 +152,8 @@ module.exports = compose( const newReq = oldVal && oldVal.length > 0 && currentVal.length === 0 ? emptyReq + 1 : emptyReq; return currentVal.length === 0 && {typing: true, val: currentVal, data: initialData, emptyReq: newReq} || {typing: true, val: currentVal}; }, - onToggle: ({ val = "", data, emptyReq}, {selected, onValueSelected, initialData = []}) => (open) => { - if (!open && val === "" && selected) { + onToggle: ({ val = "", data, emptyReq}, {clearable, selected, onValueSelected, initialData = []}) => (open) => { + if (!clearable && !open && val === "" && selected) { onValueSelected(); return {typing: false}; }else if (!open && val !== selected) { diff --git a/web/client/components/manager/rulesmanager/enhancers/fixedOptions.js b/web/client/components/manager/rulesmanager/enhancers/fixedOptions.js index 9eed793480..9be2ba4f46 100644 --- a/web/client/components/manager/rulesmanager/enhancers/fixedOptions.js +++ b/web/client/components/manager/rulesmanager/enhancers/fixedOptions.js @@ -15,6 +15,7 @@ const dataStreamFactory = prop$ => { module.exports = compose( defaultProps({ + clearable: true, stopPropagation: true, emitOnReset: false, paginated: false, @@ -43,6 +44,14 @@ module.exports = compose( return { val: undefined }; }, + onReset: ({typing, val}, {onValueSelected, selected}) => () => { + if (selected) { + onValueSelected(); + } + if (typing) { + return {typing: false}; + } + }, onChange: ({stopChange}, {valueField}) => (val = "") => { if (stopChange) { @@ -51,8 +60,8 @@ module.exports = compose( const currentVal = isObject(val) && val[valueField] || val; return {val: currentVal, typing: true}; }, - onToggle: ({ val = ""}, {selected, onValueSelected}) => (open) => { - if (!open && val === "" && selected) { + onToggle: ({ val = ""}, {clearable, selected, onValueSelected}) => (open) => { + if (!clearable && !open && val === "" && selected) { onValueSelected(); return {typing: false}; }else if (!open && val !== selected) { diff --git a/web/client/components/manager/rulesmanager/ruleseditor/AttributesEditor.jsx b/web/client/components/manager/rulesmanager/ruleseditor/AttributesEditor.jsx index dac0da2e7e..7c40515bfe 100644 --- a/web/client/components/manager/rulesmanager/ruleseditor/AttributesEditor.jsx +++ b/web/client/components/manager/rulesmanager/ruleseditor/AttributesEditor.jsx @@ -9,8 +9,10 @@ const React = require('react'); const {Grid, Row, Col} = require('react-bootstrap'); const Message = require("../../../I18N/Message"); const Select = require("../AttributeAccessSelect"); +const {castArray} = require("lodash"); + const getAttribute = (name, {attributes = {}}) => { - return (attributes.attribute || []).filter(a => a.name === name)[0]; + return castArray((attributes.attribute || [])).filter(a => a.name === name)[0]; }; const getAttributeValue = (name, constraints) => { return (getAttribute(name, constraints) || {}).access || "READWRITE"; diff --git a/web/client/components/manager/rulesmanager/ruleseditor/FiltersEditor.jsx b/web/client/components/manager/rulesmanager/ruleseditor/FiltersEditor.jsx index 4b9873e430..f1101db242 100644 --- a/web/client/components/manager/rulesmanager/ruleseditor/FiltersEditor.jsx +++ b/web/client/components/manager/rulesmanager/ruleseditor/FiltersEditor.jsx @@ -12,14 +12,28 @@ 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 filtersEnhancer = require("./enhancers/filters"); +const MapModal = require("../MapModal"); +const MapSwitch = require("../../../misc/switch/SwitchPanel"); +const SwitchPanel = switchEnhancer(MapSwitch); const Message = require("../../../I18N/Message"); +const SpatialFilter = require("../SimpleSpatialFilter"); -module.exports = ({constraints = {}, active = false, setOption= () => {}}) => { - return ( +const {isEmpty} = require("lodash"); + +const spatialMethodOptions = [ + {"id": "BBOX", "name": "queryform.spatialfilter.methods.box"}, + {"id": "Circle", "name": "queryform.spatialfilter.methods.circle"}, + {"id": "Polygon", "name": "queryform.spatialfilter.methods.poly"}, + {"id": "CQL", "name": "queryform.spatialfilter.methods.cql"} +]; + + +module.exports = filtersEnhancer(({onMapReady, geometryState = {}, spatialField = {}, layer = {}, constraints = {}, active = false, setOption= () => {}, mapActive = false, actions = {}}) => { + const enabled = !isEmpty(layer); + return enabled && ( - }> + setOption({key: "cqlFilterRead", value: ""})} title={} initExpanded={!!constraints.cqlFilterRead}>
{({width}) =>
@@ -35,7 +49,7 @@ module.exports = ({constraints = {}, active = false, setOption= () => {}}) => {
- }> + setOption({key: "cqlFilterWrite", value: ""})} title={} initExpanded={!!constraints.cqlFilterWrite}>
{({width}) =>
@@ -51,5 +65,15 @@ module.exports = ({constraints = {}, active = false, setOption= () => {}}) => {
+ + + {mapActive && } ); -}; +}); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx b/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx index 8ce88face9..d932420ca0 100644 --- a/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx +++ b/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx @@ -9,8 +9,8 @@ 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", loading = false, detailsActive = false, type = ""}) => { +const {areDetailsActive} = require("../../../../utils/RulesEditor"); +module.exports = ({rule = {}, onNavChange = () => {}, onExit = () => {}, disableSave = true, onSave = () => {}, activeTab = "1", loading = false, type = ""}) => { const buttons = [{ glyph: '1-close', tooltipId: 'rulesmanager.tooltip.close', @@ -23,6 +23,7 @@ module.exports = ({onNavChange = () => {}, onExit = () => {}, disableSave = true onClick: onSave, disabled: disableSave || loading }]; + const detailsActive = areDetailsActive(rule); return (
@@ -30,9 +31,9 @@ module.exports = ({onNavChange = () => {}, onExit = () => {}, disableSave = true
); }; diff --git a/web/client/components/manager/rulesmanager/ruleseditor/__tests__/AttributesEditor-test.jsx b/web/client/components/manager/rulesmanager/ruleseditor/__tests__/AttributesEditor-test.jsx new file mode 100644 index 0000000000..df0ad5e035 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/__tests__/AttributesEditor-test.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 ReactDOM = require('react-dom'); +const expect = require('expect'); +const AttributesEditor = require('../AttributesEditor.jsx'); +const constraints = { + attributes: { + attribute: {access: "READONLY", name: "cat"} + } +}; +const attributes = [ + { + "name": "the_geom", + "type": "gml:Point", + "localType": "Point"}, + { + "name": "cat", + "maxOccurs": 1, + "type": "xsd:int", + "localType": "int"}]; + +describe('Attributes Editor component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('render nothing if not active', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const el = container.querySelector('.ms-rule-editor'); + expect(el).toExist(); + expect(el.style.display).toBe("none"); + }); + it('render attributes', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const rows = container.querySelectorAll('.row'); + expect(rows).toExist(); + expect(rows.length).toBe(3); + }); + +}); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/__tests__/Header-test.jsx b/web/client/components/manager/rulesmanager/ruleseditor/__tests__/Header-test.jsx index d0e3969a49..028ec7dbd6 100644 --- a/web/client/components/manager/rulesmanager/ruleseditor/__tests__/Header-test.jsx +++ b/web/client/components/manager/rulesmanager/ruleseditor/__tests__/Header-test.jsx @@ -37,7 +37,7 @@ describe('Rules Editor Header component', () => { }); it('render navigation items with details active', () => { - ReactDOM.render(
, document.getElementById("container")); + ReactDOM.render(
, document.getElementById("container")); const container = document.getElementById('container'); const el = container.querySelector('.ms-panel-header-container'); expect(el).toExist(); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/__tests__/StylesEditor-test.jsx b/web/client/components/manager/rulesmanager/ruleseditor/__tests__/StylesEditor-test.jsx new file mode 100644 index 0000000000..af7d4bb70a --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/__tests__/StylesEditor-test.jsx @@ -0,0 +1,78 @@ +/* + * 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 expect = require('expect'); +const StylesEditor = require('../StylesEditor.jsx'); +const constraints = { + allowedStyles: {style: ["poly_landmarks"]}, + defaultStyle: "poly_landmarks" +}; +const ReactTestUtils = require('react-dom/test-utils'); +const styles = [{name: "poly_landmarks", title: "poly_landmarks"}]; +describe('Styles Editor component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('render nothing if not active', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const el = container.querySelector('.ms-rule-editor'); + expect(el).toExist(); + expect(el.style.display).toBe("none"); + }); + it('render defaults when active', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const rows = container.querySelectorAll('.ms-add-style'); + expect(rows).toExist(); + expect(rows.length).toBe(2); + const btns = container.querySelectorAll('button'); + expect(btns).toExist(); + expect(btns.length).toBe(2); + ReactTestUtils.Simulate.click(btns[0]); + ReactTestUtils.Simulate.click(btns[1]); + + }); + it('render defaults style and styles list', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const cards = container.querySelectorAll('.mapstore-side-card'); + expect(cards).toExist(); + expect(cards.length).toBe(2); + }); + it('render default style modal', () => { + ReactDOM.render(, document.getElementById("container")); + const modal = document.querySelector('.ms-style-modal'); + expect(modal).toExist(); + const sideCard = modal.querySelector(".mapstore-side-card"); + expect(sideCard).toExist(); + ReactTestUtils.Simulate.click(sideCard); + }); + it('render availables styles modal', () => { + ReactDOM.render(, document.getElementById("container")); + const modal = document.querySelector('.ms-style-modal'); + expect(modal).toExist(); + const sideCard = modal.querySelector(".mapstore-side-card"); + expect(sideCard).toExist(); + ReactTestUtils.Simulate.click(sideCard); + const btns = modal.querySelectorAll('button'); + expect(btns).toExist(); + expect(btns.length).toBe(2); + ReactTestUtils.Simulate.click(btns[0]); + ReactTestUtils.Simulate.click(btns[1]); + }); + +}); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/enhancers/filters.js b/web/client/components/manager/rulesmanager/ruleseditor/enhancers/filters.js index aa70765243..b2404147fe 100644 --- a/web/client/components/manager/rulesmanager/ruleseditor/enhancers/filters.js +++ b/web/client/components/manager/rulesmanager/ruleseditor/enhancers/filters.js @@ -5,17 +5,62 @@ * 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"); - - +const {compose, withStateHandlers, withPropsOnChange} = require("recompose"); +const {connect} = require("react-redux"); +const {changeDrawingStatus} = require("../../../../../actions/draw"); +const {geometryStateSel} = require("../../../../../selectors/rulesmanager"); +const {error} = require("../../../../../actions/notifications"); module.exports = compose( - withStateHandlers(() => ({ - panels: {} - }), + connect(state => ({geometryState: geometryStateSel(state)}), {onChangeDrawingStatus: changeDrawingStatus, onError: error}), + withStateHandlers(({constraints = {}}) => { + const {restrictedAreaWkt: wkt} = constraints; + return { + mapActive: wkt && wkt.length > 0, + spatialField: {} + }; + }, { - onSwitch: ({panels}) => (panel, expanded) => ({ - panels: {...panels, [panel]: expanded} - }) - + toggleMap: (state, {onChangeDrawingStatus}) => (isActive) => { + if (!isActive) { + onChangeDrawingStatus( "clean", + "", + "rulesmanager", + [], + {}); + return { + mapActive: isActive, + spatialField: {} + }; + } + return { + mapActive: isActive + }; + }, + onSelectSpatialMethod: ({spatialField}) => (method, name) => ( + {spatialField: {...spatialField, [name]: method}} + ), + onMapReady: (state, {geometryState = {}, spatialField = {}, onChangeDrawingStatus}) => () => { + if (geometryState.geometry && geometryState.geometry.coordinates) { + onChangeDrawingStatus( "create", + "MultiPolygons", + "rulesmanager", + [geometryState.geometry], + {}); + return {spatialField: {...spatialField, method: "Polygon"}}; + } + return {}; + } + }), + withPropsOnChange("toggleMap", ({toggleMap, onSelectSpatialMethod, onError, onChangeDrawingStatus}) => ({ + actions: { + onChangeDrawingStatus, + onExpandSpatialFilterPanel: toggleMap, + onSelectSpatialMethod, + onError + } + })), + withPropsOnChange(["spatialField", "geometryState", "constraints"], ({spatialField = {}, geometryState = {}, constraints = {}}) => { + const {restrictedAreaWkt: wkt} = constraints; + return {spatialField: {...geometryState, ...spatialField, wkt}}; }) ); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/enhancers/switch.js b/web/client/components/manager/rulesmanager/ruleseditor/enhancers/switch.js index aa565357ea..b0cdfaa14b 100644 --- a/web/client/components/manager/rulesmanager/ruleseditor/enhancers/switch.js +++ b/web/client/components/manager/rulesmanager/ruleseditor/enhancers/switch.js @@ -9,13 +9,18 @@ const {compose, withStateHandlers} = require("recompose"); module.exports = compose( - withStateHandlers(() => ({ - expanded: false - }), + withStateHandlers(({initExpanded}) => { + return { + expanded: !!initExpanded + }; }, { - onSwitch: () => (expanded) => ({ - expanded - }) - + onSwitch: (state, {reset}) => (expanded) => { + if (!expanded) { + reset(); + } + return { + expanded + }; + } }) ); diff --git a/web/client/components/map/enhancers/withDraw.js b/web/client/components/map/enhancers/withDraw.js new file mode 100644 index 0000000000..0e9279e17a --- /dev/null +++ b/web/client/components/map/enhancers/withDraw.js @@ -0,0 +1,37 @@ +/* + * 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 { withPropsOnChange } = require("recompose"); + +const {connect} = require("react-redux"); +const {changeDrawingStatus, endDrawing, setCurrentStyle, geometryChanged, drawStopped} = require('../../../actions/draw'); + +const defaultDrawConnect = connect((state) => +state.draw || {}, { + onChangeDrawingStatus: changeDrawingStatus, + onEndDrawing: endDrawing, + onGeometryChanged: geometryChanged, + onDrawStopped: drawStopped, + setCurrentStyle: setCurrentStyle +}); +/** + * Add the draw tool to base-map. The draw support is already present in plugins but It needs + * to be connected and added to tools to work. + * It's possible to pass a connect function to override default connection to state and action + * @param {function} connectFunction connect function to override default connection of the draw tool. + */ +module.exports = (connectFunction = defaultDrawConnect) => withPropsOnChange( + ['plugins'], + ({plugins}= {}) => { + const {DrawSupport, tools = {}, ...rest} = plugins; + if (!DrawSupport) { + return {}; + } + const Draw = connectFunction(DrawSupport); + return {plugins: {...rest, tools: {...tools, draw: Draw}}}; + } +); diff --git a/web/client/components/misc/combobox/PagedCombobox.jsx b/web/client/components/misc/combobox/PagedCombobox.jsx index 72bd9f8c3a..7e6eceb356 100644 --- a/web/client/components/misc/combobox/PagedCombobox.jsx +++ b/web/client/components/misc/combobox/PagedCombobox.jsx @@ -50,7 +50,9 @@ class PagedCombobox extends React.Component { tooltip: PropTypes.object, valueField: PropTypes.string, placeholder: PropTypes.string, - stopPropagation: PropTypes.bool + stopPropagation: PropTypes.bool, + clearable: PropTypes.bool, + onReset: PropTypes.func }; static contextTypes = { @@ -77,6 +79,7 @@ class PagedCombobox extends React.Component { onToggle: () => {}, onChange: () => {}, onSelect: () => {}, + onReset: () => {}, textField: "label", tooltip: { customizedTooltip: undefined, @@ -86,7 +89,8 @@ class PagedCombobox extends React.Component { overlayTriggerKey: "", placement: "top" }, - valueField: "value" + valueField: "value", + clearable: false }; renderWithTooltip = (field) => { @@ -159,17 +163,25 @@ class PagedCombobox extends React.Component { textField={this.props.textField} valueField={this.props.valueField} value={this.props.selectedValue} - />); + /> + ); return this.props.tooltip && this.props.tooltip.enabled ? this.renderWithTooltip(field) : field; } render() { - let label = this.props.label ? () : (); // TODO change "the else case" value with null ? + const {selectedValue: v, disabled, onReset, label: l, clearable} = this.props; + let label = l ? () : (); // TODO change "the else case" value with null ? return (
{label} - {this.renderField()} + {clearable ? ( +
+ {this.renderField()} + x +
) : this.renderField() + }
); } } + module.exports = PagedCombobox; diff --git a/web/client/epics/rulesmanager.js b/web/client/epics/rulesmanager.js index 708ed7e881..60a2d48aa2 100644 --- a/web/client/epics/rulesmanager.js +++ b/web/client/epics/rulesmanager.js @@ -1,14 +1,16 @@ const Rx = require("rxjs"); -const {SAVE_RULE, setLoading, RULE_SAVED, DELETE_RULES} = require("../actions/rulesmanager"); -const {error} = require("../actions/notifications"); -const {updateRule, createRule, deleteRule} = require("../observables/rulesmanager"); +const {SAVE_RULE, setLoading, RULE_SAVED, DELETE_RULES, CACHE_CLEAN} = require("../actions/rulesmanager"); +const {error, success} = require("../actions/notifications"); +const {drawSupportReset} = require("../actions/draw"); +const {updateRule, createRule, deleteRule, cleanCache } = require("../observables/rulesmanager"); // To do add Error management const {get} = require("lodash"); const saveRule = stream$ => stream$ .mapTo({type: RULE_SAVED}) + .concat(Rx.Observable.of(drawSupportReset())) .catch(({data}) => { - const isDuplicate = data.indexOf("Duplicate Rule") === 0; + const isDuplicate = data.indexOf("Duplicat") === 0; return Rx.Observable.of(error({title: "rulesmanager.errorTitle", message: isDuplicate ? "rulesmanager.errorDuplicateRule" : "rulesmanager.errorUpdatingRule"})); }) .startWith(setLoading(true)) @@ -21,6 +23,15 @@ module.exports = { onDelete: (action$, {getState}) => action$.ofType(DELETE_RULES) .switchMap(({ids = get(getState(), "rulesmanager.selectedRules", []).map(row => row.id)}) => { return Rx.Observable.combineLatest(ids.map(id => deleteRule(id))).let(saveRule); - }) + }), + onCacheClean: action$ => action$.ofType(CACHE_CLEAN) + .exhaustMap( () => + cleanCache() + .mapTo(success({title: "rulesmanager.errorTitle", message: "rulesmanager.cacheCleaned"})) + .startWith(setLoading(true)) + .catch(() => { + return Rx.Observable.of(error({title: "rulesmanager.errorTitle", message: "rulesmanager.errorCleaningCache"})); + }) + .concat(Rx.Observable.of(setLoading(false)))) }; diff --git a/web/client/observables/rulesmanager.js b/web/client/observables/rulesmanager.js index 92296929cc..b1a28b10e7 100644 --- a/web/client/observables/rulesmanager.js +++ b/web/client/observables/rulesmanager.js @@ -1,16 +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 WMS = require('../api/WMS'); const {getLayerCapabilities, describeLayer} = require("./wms"); const {describeFeatureType} = require("./wfs"); +const {RULE_SAVED} = require("../actions/rulesmanager"); const xmlToJson$ = response => Rx.Observable.bindNodeCallback( (data, callback) => parseString(data, { tagNameProcessors: [stripPrefix], @@ -66,7 +66,7 @@ const fullUpdate = (update$) => update$.filter(({rule: r, origRule: oR}) =>getUp const {priority: p, id: omit, ...oldRule} = origRule; oldRule.position = {value: p, position: "fixedPriority"}; // We have to restore original rule and to throw the exception!! - return Rx.Observable.defer(() => GeoFence.addRule(oldRule)).do(() => { throw (e); }); + return Rx.Observable.defer(() => GeoFence.addRule(oldRule)).concat(Rx.Observable.of({type: RULE_SAVED}).do(() => { throw (e); })); }); }) .switchMap(({data: id}) => { @@ -82,8 +82,8 @@ const grantUpdate = (update$) => update$.filter(({rule: r, origRule: oR}) => get .catch((e) => { const {priority: p, id: omit, ...oldRule} = origRule; oldRule.position = {value: p, position: "fixedPriority"}; - // We have to restore original rule and to throw the exception!! - return Rx.Observable.defer(() => GeoFence.addRule(oldRule)).do(() => { throw (e); }); + // We have to restore original rule and to throw the exception and reload the rules!! + return Rx.Observable.defer(() => GeoFence.addRule(oldRule)).concat(Rx.Observable.of({type: RULE_SAVED}).do(() => { throw (e); })); }); }) ); @@ -129,15 +129,17 @@ module.exports = { deleteRule, getStylesAndAttributes: (layer, workspace) => { const {url} = ConfigUtils.getDefaults().geoFenceGeoServerInstance || {}; - const l = {url: `${fixUrl(url)}ows`, name: `${workspace}:${layer}`}; + const name = `${workspace}:${layer}`; + const l = {url: `${fixUrl(url)}ows`, name}; return Rx.Observable.combineLatest(getLayerCapabilities(l) - .map(({style}) => ({style})), + .map((cp) => ({style: cp.style, ly: {bbox: WMS.getBBox(cp), name, url: `${fixUrl(url)}wms`, type: "wms", visibility: true, format: "image/png", title: cp.title}})), 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})); + }), ({style, ly}, {properties, type}) => ({styles: style || [], properties, type, layer: ly})); - } + }, + cleanCache: () => Rx.Observable.defer(() => GeoFence.cleanCache()) }; diff --git a/web/client/plugins/manager/EditorEnhancer.js b/web/client/plugins/manager/EditorEnhancer.js index 4ad52b7376..4bc1183d8b 100644 --- a/web/client/plugins/manager/EditorEnhancer.js +++ b/web/client/plugins/manager/EditorEnhancer.js @@ -1,7 +1,10 @@ -const {compose, withStateHandlers, defaultProps} = require('recompose'); +const {compose, withStateHandlers, defaultProps, withPropsOnChange} = require('recompose'); const propsStreamFactory = require('../../components/misc/enhancers/propsStreamFactory'); const Rx = require("rxjs"); +const {isEmpty} = require("lodash"); +const {connect} = require("react-redux"); +const {changeDrawingStatus} = require("../..//actions/draw"); const sameLayer = ({activeRule: f1}, {activeRule: f2}) => f1.layer === f2.layer; const emitStop = stream$ => stream$.filter(() => false).startWith({}); const {getStylesAndAttributes} = require("../../observables/rulesmanager"); @@ -21,7 +24,9 @@ const dataStreamFactory = prop$ => { }).do(() => setLoading(false)); }).let(emitStop); }; -module.exports = compose( +module.exports = compose(connect(() => ({}), { + onChangeDrawingStatus: changeDrawingStatus + }), defaultProps({dataStreamFactory}), withStateHandlers(({activeRule: initRule}) => ({ activeRule: initRule, @@ -49,6 +54,7 @@ module.exports = compose( activeRule: {...activeRule, [key]: value} }; }, + setRule: () => (activeRule) => ({activeRule}), setConstraintsOption: ({activeRule, type}) => ({key, value}) => { const constraints = {...(activeRule.constraints || {}), type, [key]: value}; return {activeRule: {...activeRule, constraints}}; @@ -56,13 +62,25 @@ module.exports = compose( onNavChange: () => activeEditor => ({ activeEditor }), - cleanConstraints: ({activeRule}) => () => { + cleanConstraints: ({activeRule}, {onChangeDrawingStatus}) => (keepLayer) => { const {constraints, ...rule} = activeRule; - return {activeRule: rule}; + onChangeDrawingStatus( "clean", + "", + "rulesmanager", + [], + {}); + return keepLayer && {activeRule: rule} || {activeRule: rule, styles: undefined, properties: undefined, type: undefined, layer: undefined}; }, - optionsLoaded: () => ({styles, properties, type}) => { - return {styles, properties, type}; + optionsLoaded: () => ({styles, properties, type, layer}) => { + return {styles, properties, type, layer}; + } + }),// Merge geometry state from draw into activeRule + withPropsOnChange(["geometryState"], ({activeRule = {}, geometryState, setRule}) => { + if (!isEmpty(geometryState)) { + const {constraints = {}} = activeRule; + setRule({...activeRule, constraints: {...constraints, restrictedAreaWkt: geometryState.wkt}}); } + return {}; }), propsStreamFactory ); diff --git a/web/client/plugins/manager/RulesEditor.jsx b/web/client/plugins/manager/RulesEditor.jsx index 90b787a053..b7d8b263ef 100644 --- a/web/client/plugins/manager/RulesEditor.jsx +++ b/web/client/plugins/manager/RulesEditor.jsx @@ -13,9 +13,9 @@ const {createSelector} = require("reselect"); const {compose} = require('recompose'); const enhancer = require("./EditorEnhancer"); const {cleanEditing, saveRule, setLoading} = require("../../actions/rulesmanager"); -const {activeRuleSelector} = require("../../selectors/rulesmanager"); +const {activeRuleSelector, geometryStateSel} = require("../../selectors/rulesmanager"); -const {isSaveDisabled, areDetailsActive, isRulePristine, isRuleValid, askConfirm} = require("../../utils/RulesEditor"); +const {isSaveDisabled, 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"); @@ -41,7 +41,8 @@ class RuleEditor extends React.Component { type: PropTypes.string, properties: PropTypes.array, loading: PropTypes.bool, - cleanConstraints: PropTypes.func + cleanConstraints: PropTypes.func, + layer: PropTypes.object } static defaultProps = { activeEditor: "1", @@ -53,7 +54,7 @@ class RuleEditor extends React.Component { type: "" } render() { - const {loading, activeRule, activeEditor, onNavChange, initRule, styles = [], setConstraintsOption, type, properties} = this.props; + const {loading, activeRule, layer, activeEditor, onNavChange, initRule, styles = [], setConstraintsOption, type, properties} = this.props; const {modalProps} = this.state || {}; return ( } > - + ); @@ -125,7 +126,7 @@ class RuleEditor extends React.Component { onClick: () => { this.cancel(); this.props.setOption({key, value}); - this.props.cleanConstraints(); + this.props.cleanConstraints(key === 'grant'); } } ], closeAction: this.cancel, msg: "rulesmanager.constraintsmsg"}})); @@ -138,7 +139,7 @@ class RuleEditor extends React.Component { } module.exports = compose( - connect(createSelector(activeRuleSelector, activeRule => ({activeRule})), { + connect(createSelector([activeRuleSelector, geometryStateSel], (activeRule, geometryState) => ({activeRule, geometryState})), { onExit: cleanEditing, onSave: saveRule, setLoading diff --git a/web/client/plugins/manager/RulesToolbar.jsx b/web/client/plugins/manager/RulesToolbar.jsx index 721cae86e2..2b145303c2 100644 --- a/web/client/plugins/manager/RulesToolbar.jsx +++ b/web/client/plugins/manager/RulesToolbar.jsx @@ -10,7 +10,7 @@ import { withPropsOnChange } from "recompose"; const React = require("react"); const {compose, withProps, withStateHandlers} = require("recompose"); const {connect} = require("react-redux"); -const { onEditRule, delRules} = require('../../actions/rulesmanager'); +const { onEditRule, delRules, onCacheClean} = require('../../actions/rulesmanager'); const {rulesEditorToolbarSelector} = require('../../selectors/rulesmanager'); const Toolbar = require('../../components/misc/toolbar/Toolbar'); const Modal = require("./ModalDialog"); @@ -31,7 +31,8 @@ const EditorToolbar = compose( rulesEditorToolbarSelector, { deleteRules: delRules, - editOrCreate: onEditRule + editOrCreate: onEditRule, + cleanCache: onCacheClean } ), withStateHandlers(() => ({ @@ -84,7 +85,7 @@ const EditorToolbar = compose( } }] })), - withPropsOnChange(["modal"], ({modal, cancelModal, deleteRules}) => { + withPropsOnChange(["modal"], ({modal, cancelModal, deleteRules, cleanCache}) => { switch (modal) { case "delete": return { @@ -119,7 +120,7 @@ const EditorToolbar = compose( { text: , bsStyle: 'primary', - onClick: () => { cancelModal(); } + onClick: () => { cancelModal(); cleanCache(); } } ], closeAction: cancelModal, diff --git a/web/client/reducers/rulesmanager.js b/web/client/reducers/rulesmanager.js index ddff84cd2e..b686b47b2e 100644 --- a/web/client/reducers/rulesmanager.js +++ b/web/client/reducers/rulesmanager.js @@ -7,10 +7,16 @@ */ const assign = require('object-assign'); +const wk = require('wellknown'); +const {isEmpty} = require("lodash"); const { RULES_SELECTED, RULES_LOADED, UPDATE_ACTIVE_RULE, ACTION_ERROR, OPTIONS_LOADED, UPDATE_FILTERS_VALUES, LOADING, EDIT_RULE, SET_FILTER, CLEAN_EDITING, RULE_SAVED} = require('../actions/rulesmanager'); +const { + CHANGE_DRAWING_STATUS +} = require('../actions/draw'); + const _ = require('lodash'); const defaultState = { services: { @@ -45,6 +51,17 @@ const getPosition = ({targetPosition = {}}, priority) => { return 0; } }; +// GEOFENCE ACCEPTS ONLY MULTYPOLYGON +const fixGeometry = (g, method = "") => { + if (method === "" || isEmpty(g) || !g.coordinates || g.coordinates.length === 0) { + return g; + } + const c = g.coordinates[0]; + if (method === "Polygon") { + return {...g, type: "MultiPolygon", coordinates: [[[...c, c[0]]]]}; + } + return {...g, type: "MultiPolygon", coordinates: [[c]]}; +}; function rulesmanager(state = defaultState, action) { switch (action.type) { @@ -133,14 +150,35 @@ function rulesmanager(state = defaultState, action) { if (createNew) { 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"}}}); + const activeRule = state.selectedRules[0] || {}; + const geometryState = activeRule.constraints && activeRule.constraints.restrictedAreaWkt && { + wkt: activeRule.constraints.restrictedAreaWkt, + geometry: wk.parse(activeRule.constraints.restrictedAreaWkt)} || {}; + return assign({}, state, {activeRule, + position: {value: state.targetPosition.offsetFromTop, position: "offsetFromTop"}, + geometryState}); } case RULE_SAVED: { - return assign({}, state, {triggerLoad: (state.triggerLoad || 0) + 1, activeRule: undefined, selectedRules: [], targetPosition: undefined }); + return assign({}, state, {triggerLoad: (state.triggerLoad || 0) + 1, geometryState: undefined, activeRule: undefined, selectedRules: [], targetPosition: undefined }); } case CLEAN_EDITING: { - return assign({}, state, {activeRule: undefined}); + return assign({}, state, {activeRule: undefined, geometryState: undefined}); } + case CHANGE_DRAWING_STATUS: { + let newState; + if (action.owner === "rulesmanager" && (action.status === "stop" || action.status === "start" || action.status === "clean")) { + const geometry = fixGeometry(((action.features || [])[0] || {}), action.method); + newState = assign({}, state, {geometryState: assign({}, { + geometry, + wkt: !isEmpty(geometry) && wk.stringify(geometry) || undefined + })}); + } else { + newState = state; + } + + return newState; + } + default: return state; } diff --git a/web/client/selectors/rulesmanager.js b/web/client/selectors/rulesmanager.js index a0dbcb1454..524fc279e8 100644 --- a/web/client/selectors/rulesmanager.js +++ b/web/client/selectors/rulesmanager.js @@ -65,6 +65,7 @@ const isRulesManagerConfigured = state => state.localConfig && state.localConfig const isEditorActive = state => state.rulesmanager && !!state.rulesmanager.activeRule; const triggerLoadSel = state => state.rulesmanager && state.rulesmanager.triggerLoad; const isLoading = state => state.rulesmanager && state.rulesmanager.loading; +const geometryStateSel = state => state.rulesmanager && state.rulesmanager.geometryState; module.exports = { rulesSelector, optionsSelector, @@ -77,5 +78,6 @@ module.exports = { servicesConfigSel, triggerLoadSel, isLoading, - isRulesManagerConfigured + isRulesManagerConfigured, + geometryStateSel }; diff --git a/web/client/themes/default/less/select.less b/web/client/themes/default/less/select.less index e49d2c985f..2061d80adb 100644 --- a/web/client/themes/default/less/select.less +++ b/web/client/themes/default/less/select.less @@ -32,7 +32,33 @@ .rw-select { border-left: 1px solid @ms2-color-shade-lighter !important; } - +.rw-combo-clearable { + background-color: @ms2-color-background !important; +} +.rw-combo-clearable input.rw-input{ + -webkit-box-shadow: inset 0 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px rgba(0, 0, 0, 0.075); +} +.rw-combo-clearable button{ + border: none !important; + -webkit-box-shadow: inset 0 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px rgba(0, 0, 0, 0.075); +} +.rw-combo-clear .hidden{ + display: none; +} +.rw-combo-clear { + position: relative; + float: right; + bottom: 2em; + right: 2.3em; + color: @ms2-color-primary !important; + cursor: pointer; +} +.rw-combo-clearable.disabled .rw-combo-clear{ + cursor: not-allowed; + color: fadeout(@ms2-color-primary, 30) !important; +} .Select-placeholder { background-color: @ms2-color-background; border: 1px solid @ms2-color-shade-lighter; diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE index 73435aedff..5a4c711efc 100644 --- a/web/client/translations/data.de-DE +++ b/web/client/translations/data.de-DE @@ -708,7 +708,8 @@ "box": "BoundingBox", "buffer": "Buffer", "circle": "Kreis", - "poly": "Polygon" + "poly": "Polygon", + "cql": "CQL" }, "operations": { "intersects": "Schneidet", @@ -1362,6 +1363,8 @@ "layerlabel": "Ebene" }, "rulesmanager": { + "apply": "Anwenden", + "remove": "Entfernen Sie die Geometrie", "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", @@ -1423,7 +1426,10 @@ "close": "Schließen", "previous": "vorheriger", "next": "nächster", + "cacheCleaned": "Der Cache wurde erfolgreich bereinigt", "errorTitle": "Geofence", + "errorCQL": "Geometrie nicht gültig!", + "errorCleaningCache": "Fehler beim Reinigen des Geofence-Cache.", "errorLoadingRoles": "Fehler beim Laden der Rollen.", "errorLoadingUsers": "Fehler beim Laden der Benutzer.", "errorLoadingWorkspaces": "Fehler beim Laden der Workspaces.", diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US index 58a6abe91c..040ddf7b85 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -709,7 +709,8 @@ "box": "Rectangle", "buffer": "Buffer", "circle": "Circle", - "poly": "Polygon" + "poly": "Polygon", + "cql": "CQL" }, "operations": { "intersects": "Intersects", @@ -1363,6 +1364,8 @@ "layerlabel": "Layer" }, "rulesmanager": { + "apply": "Apply", + "remove": "Remove geometry", "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", @@ -1425,7 +1428,10 @@ "close": "Close", "previous": "previous", "next": "next", + "cacheCleaned": "Cache successfully cleaned", "errorTitle": "Geofence", + "errorCQL": "Geometry not valid!", + "errorCleaningCache": "Error cleaning geofence cache.", "errorLoadingRoles": "Error loading roles.", "errorLoadingUsers": "Error loading users.", "errorLoadingWorkspaces": "Error loading workspaces.", diff --git a/web/client/translations/data.es-ES b/web/client/translations/data.es-ES index 3406ffe542..c9814be4e0 100644 --- a/web/client/translations/data.es-ES +++ b/web/client/translations/data.es-ES @@ -708,7 +708,8 @@ "box": "Rectángulo", "buffer": "Buffer", "circle": "Círculo", - "poly": "Polígono" + "poly": "Polígono", + "cql": "CQL" }, "operations": { "intersects": "intersecciona", @@ -1362,6 +1363,8 @@ "layerlabel": "Capa" }, "rulesmanager": { + "apply": "Aplicar", + "remove": "Eliminar geometría", "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", @@ -1423,7 +1426,10 @@ "close": "Cerrar", "previous": "previo", "next": "próximo", + "cacheCleaned": "Caché limpiada con éxito", "errorTitle": "Geofence", + "errorCQL": "Geometría no válida!", + "errorCleaningCache": "Error al limpiar la memoria caché geofence.", "errorLoadingRoles": "Error al cargar los roles.", "errorLoadingUsers": "Error al cargar los usuarios.", "errorLoadingWorkspaces": "Error al cargar los espacios de trabajo.", diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR index 24eec6ccf8..f68b40ab9d 100644 --- a/web/client/translations/data.fr-FR +++ b/web/client/translations/data.fr-FR @@ -709,7 +709,8 @@ "box": "Rectangle", "buffer": "Zone tampon", "circle": "Cercle", - "poly": "Polygone" + "poly": "Polygone", + "cql": "CQL" }, "operations": { "intersects": "intersecte", @@ -1362,6 +1363,8 @@ "layerlabel": "Couche" }, "rulesmanager": { + "apply": "Appliquer", + "remove": "Supprimer la géométrie", "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", @@ -1423,7 +1426,10 @@ "close": "Fermer", "previous": "précédent", "next": "prochain", + "cacheCleaned": "Cache nettoyé avec succès", "errorTitle": "Geofence", + "errorCQL": "La géométrie n'est pas valide!", + "errorCleaningCache": "Erreur lors du nettoyage du cache de geofence.", "errorLoadingRoles": "Erreur lors du chargement des rôles.", "errorLoadingUsers": "Erreur lors du chargement des utilisateurs.", "errorLoadingWorkspaces": "Erreur lors du chargement des workspaces.", diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index 11eff97ac3..ab02ff6d5d 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -708,7 +708,8 @@ "box": "Rettangolo", "buffer": "Buffer", "circle": "Cerchio", - "poly": "Poligono" + "poly": "Poligono", + "cql": "CQL" }, "operations": { "intersects": "Intersezione", @@ -1362,6 +1363,8 @@ "layerlabel": "Layer" }, "rulesmanager": { + "apply": "Applica", + "remove": "Elimina la geometia", "resetconstraints": "Resetta Vincoli", "constraintsmsg": "Cambinado grant, workspace o layer, i dettagli saranno eliminati. Sei sicuro di volerlo fare?", "defstyle": "Stile Predefinito", @@ -1423,7 +1426,10 @@ "close": "Chiudi", "previous": "precedente", "next": "prossima", + "cacheCleaned": "Cache eliminata", "errorTitle": "Geofence", + "errorCQL": "Geometria non valida!", + "errorCleaningCache": "Errore durante la pulizia della cache.", "errorLoadingRoles": "Errore durante il caricamento dei ruoli", "errorLoadingUsers": "Errore durante il caricamento degli utenti.", "errorLoadingWorkspaces": "Errore durante il caricamento dei workspaces.", diff --git a/web/client/utils/RulesEditor.js b/web/client/utils/RulesEditor.js index 2d2eba382b..738ac68c9b 100644 --- a/web/client/utils/RulesEditor.js +++ b/web/client/utils/RulesEditor.js @@ -11,8 +11,8 @@ const RulesEditorUtils = { isSaveDisabled: (currentRule, initRule) => { return RulesEditorUtils.isRulePristine(currentRule, initRule) && initRule.hasOwnProperty("id"); }, - areDetailsActive: ({layer, grant} = {}) => { - return !!layer && grant === "ALLOW"; + areDetailsActive: ({layer} = {}) => { + return !!layer; }, isRulePristine: (currentRule, initRule) => { return isEqual(currentRule, initRule); @@ -24,7 +24,7 @@ const RulesEditorUtils = { return true; }, askConfirm: ({constraints = {}} = {}, key, value) => { - return !isEmpty(constraints) && (key === "workspace" || key === "layer" || (key === "grant" && value === "DENY")); + return !isEmpty(constraints) && (key === "workspace" || key === "layer" || (key === "grant" && value !== "ALLOW")); }, checkIp };