diff --git a/docma-config.json b/docma-config.json index 9085a54d7e..8b000814ba 100644 --- a/docma-config.json +++ b/docma-config.json @@ -218,7 +218,7 @@ "web/client/plugins/WFSDownload.jsx", "web/client/plugins/ZoomIn.jsx", "web/client/plugins/ZoomOut.jsx" - ] + ] }, "./docs/**/*md", { diff --git a/package.json b/package.json index 7fedde7b54..c1cb57b80b 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,7 @@ "react-container-dimensions": "1.3.2", "react-copy-to-clipboard": "5.0.0", "react-data-grid": "2.0.59", + "react-data-grid-addons": "3.0.11", "react-dnd": "2.4.0", "react-dnd-html5-backend": "2.4.1", "react-dock": "0.2.4", diff --git a/web/client/actions/__tests__/rulesmanager-test.js b/web/client/actions/__tests__/rulesmanager-test.js index c3e3b7c12c..3fba228be8 100644 --- a/web/client/actions/__tests__/rulesmanager-test.js +++ b/web/client/actions/__tests__/rulesmanager-test.js @@ -10,11 +10,44 @@ const expect = require('expect'); const { RULES_SELECTED, RULES_LOADED, UPDATE_ACTIVE_RULE, ACTION_ERROR, OPTIONS_LOADED, UPDATE_FILTERS_VALUES, rulesSelected, rulesLoaded, updateActiveRule, - actionError, optionsLoaded, updateFiltersValues} = require('../rulesmanager'); + actionError, optionsLoaded, updateFiltersValues, + SET_FILTER, setFilter, + SAVE_RULE, saveRule, cleanEditing, CLEAN_EDITING, + onEditRule, EDIT_RULE, delRules, DELETE_RULES} = require('../rulesmanager'); describe('test rules manager actions', () => { - - it('rules slected', () => { + it('save rule', () => { + const rule = {}; + const action = saveRule(rule); + expect(action).toExist(); + expect(action.type).toBe(SAVE_RULE); + expect(action.rule).toBe(rule); + }); + it('clean editing', () => { + const action = cleanEditing(); + expect(action).toExist(); + expect(action.type).toBe(CLEAN_EDITING); + }); + it('on edit rule', () => { + const action = onEditRule(); + expect(action).toExist(); + expect(action.type).toBe(EDIT_RULE); + expect(action.createNew).toBe(false); + expect(action.targetPriority).toBe(0); + }); + it('delete rules', () => { + const action = delRules(); + expect(action).toExist(); + expect(action.type).toBe(DELETE_RULES); + }); + it('set Filter', () => { + const action = setFilter("key", "value"); + expect(action).toExist(); + expect(action.type).toBe(SET_FILTER); + expect(action.key).toBe("key"); + expect(action.value).toBe("value"); + }); + it('rules selected', () => { const rules = [ { id: "rules1" }, { id: "rules2" } diff --git a/web/client/actions/rulesmanager.js b/web/client/actions/rulesmanager.js index 2c2e975675..e0b85bc043 100644 --- a/web/client/actions/rulesmanager.js +++ b/web/client/actions/rulesmanager.js @@ -18,13 +18,57 @@ const UPDATE_ACTIVE_RULE = 'UPDATE_ACTIVE_RULE'; const UPDATE_FILTERS_VALUES = 'UPDATE_FILTERS_VALUES'; const ACTION_ERROR = 'ACTION_ERROR'; const OPTIONS_LOADED = 'OPTIONS_LOADED'; +const LOADING = 'RULES_MANAGER:LOADING'; +const SET_FILTER = "RULES_MANAGER:SET_FILTER"; +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"; -function rulesSelected(rules, merge, unselect) { +function delRules(ids) { + return { + type: DELETE_RULES, + ids + }; +} + +function setFilter(key, value) { + return { + type: SET_FILTER, + key, + value + }; +} + +function onEditRule(targetPriority = 0, createNew = false) { + return { + type: EDIT_RULE, + createNew, + targetPriority + }; +} + +function cleanEditing() { + return { + type: CLEAN_EDITING + }; +} + +function setLoading(loading) { + return { + type: LOADING, + loading + }; +} + +function rulesSelected(rules, merge, unselect, targetPosition) { return { type: RULES_SELECTED, - rules: rules, - merge: merge, - unselect: unselect + rules, + merge, + unselect, + targetPosition }; } @@ -203,6 +247,8 @@ function updateRule() { }; } +const saveRule = (rule) => ({type: SAVE_RULE, rule}); + module.exports = { RULES_SELECTED, RULES_LOADED, @@ -225,5 +271,11 @@ module.exports = { loadWorkspaces, loadLayers, actionError, - optionsLoaded + optionsLoaded, + LOADING, setLoading, + SET_FILTER, setFilter, + EDIT_RULE, onEditRule, + CLEAN_EDITING, cleanEditing, + SAVE_RULE, saveRule, RULE_SAVED, + DELETE_RULES, delRules }; diff --git a/web/client/api/geoserver/GeoFence.js b/web/client/api/geoserver/GeoFence.js index b6e985ef83..6ea166969e 100644 --- a/web/client/api/geoserver/GeoFence.js +++ b/web/client/api/geoserver/GeoFence.js @@ -10,17 +10,27 @@ const axios = require('../../libs/ajax'); const assign = require('object-assign'); const ConfigUtils = require('../../utils/ConfigUtils'); - +const EMPTY_RULE = { + constraints: {}, + ipaddress: "", + layer: "", + request: "", + rolename: "", + service: "", + username: "", + workspace: "" + }; var Api = { - loadRules: function(rulesPage, rulesFiltersValues) { - const options = { - 'params': { - 'page': rulesPage - 1, - 'entries': 10 - } + loadRules: function(page, rulesFiltersValues, entries = 10) { + const params = { + page, + entries, + ...this.assignFiltersValue(rulesFiltersValues) }; - this.assignFiltersValue(rulesFiltersValues, options); + const options = {params, 'headers': { + 'Content': 'application/json' + }}; return axios.get('geofence/rest/rules', this.addBaseUrl(options)) .then(function(response) { return response.data; @@ -30,9 +40,8 @@ var Api = { getRulesCount: function(rulesFiltersValues) { const options = { - 'params': {} + 'params': this.assignFiltersValue(rulesFiltersValues) }; - this.assignFiltersValue(rulesFiltersValues, options); return axios.get('geofence/rest/rules/count', this.addBaseUrl(options)).then(function(response) { return response.data; }); @@ -55,10 +64,15 @@ var Api = { }, addRule: function(rule) { - if (!rule.access) { - rule.access = "ALLOW"; + const newRule = {...rule}; + if (!newRule.instance) { + const {id: instanceId} = ConfigUtils.getDefaults().geoFenceGeoServerInstance; + newRule.instance = {id: instanceId}; + } + if (!newRule.grant) { + newRule.grant = "ALLOW"; } - return axios.post('geofence/rest/rules', rule, this.addBaseUrl({ + return axios.post('geofence/rest/rules', newRule, this.addBaseUrl({ 'headers': { 'Content': 'application/json' } @@ -66,29 +80,34 @@ var Api = { }, updateRule: function(rule) { - return axios.post('geofence/rest/rules/id/' + rule.id, rule, this.addBaseUrl({ + // 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({ 'headers': { 'Content': 'application/json' } })); }, - assignFiltersValue: function(rulesFiltersValues, options) { - if (rulesFiltersValues) { - assign(options.params, {"userName": this.normalizeFilterValue(rulesFiltersValues.userName)}); - assign(options.params, {"roleName": this.normalizeFilterValue(rulesFiltersValues.roleName)}); - assign(options.params, {"service": this.normalizeFilterValue(rulesFiltersValues.service)}); - assign(options.params, {"request": this.normalizeFilterValue(rulesFiltersValues.request)}); - assign(options.params, {"workspace": this.normalizeFilterValue(rulesFiltersValues.workspace)}); - assign(options.params, {"layer": this.normalizeFilterValue(rulesFiltersValues.layer)}); - } - return options; + assignFiltersValue: function(rulesFiltersValues = {}) { + return Object.keys(rulesFiltersValues).map(key => ({key, normKey: this.normalizeKey(key)})) + .reduce((params, {key, normKey}) => ({...params, [normKey]: this.normalizeFilterValue(rulesFiltersValues[key])}), {}); }, normalizeFilterValue(value) { return value === "*" ? undefined : value; }, - + normalizeKey(key) { + switch (key) { + case 'username': + return 'userName'; + case 'rolename': + return 'groupName'; + default: + return key; + } + }, assignFilterValue: function(queryParameters, filterName, filterAny, filterValue) { if (!filterValue) { return; @@ -99,29 +118,52 @@ var Api = { assign(queryParameters, {[filterName]: filterValue}); } }, - - getGroups: function() { - return axios.get('security/rest/roles', this.addBaseUrl({ + getGroupsCount: function(filter = " ") { + const encodedFilter = encodeURIComponent(`%${filter}%`); + return axios.get(`geofence/rest/groups/count/${encodedFilter}`, this.addBaseUrl({ 'headers': { - 'Accept': 'application/json' + 'Accept': 'text/plain' } })).then(function(response) { return response.data; }); }, - - getUsers: function() { - return axios.get('security/rest/usergroup/users', this.addBaseUrl({ + getGroups: function(filter, page, entries = 10) { + const params = { + page, + entries, + nameLike: `%${filter}%` + }; + const options = {params}; + return axios.get(`geofence/rest/groups`, this.addBaseUrl(options)).then(function(response) { + return response.data; + }); + }, + getUsersCount: function(filter = " ") { + const encodedFilter = encodeURIComponent(`%${filter}%`); + return axios.get(`geofence/rest/users/count/${encodedFilter}`, this.addBaseUrl({ 'headers': { - 'Accept': 'application/json' + 'Accept': 'text/plain' } })).then(function(response) { return response.data; }); }, + getUsers: function(filter, page, entries = 10) { + const params = { + page, + entries, + nameLike: `%${filter}%` + }; + const options = {params}; + return axios.get(`geofence/rest/users`, this.addBaseUrl(options)).then(function(response) { + return response.data; + }); + }, + getWorkspaces: function() { - return axios.get('rest/workspaces', this.addBaseUrl({ + return axios.get('rest/workspaces', this.addBaseUrlGS({ 'headers': { 'Accept': 'application/json' } @@ -134,8 +176,12 @@ var Api = { return !value ? '*' : value; }, - addBaseUrl: function(options) { - return assign(options, {baseURL: ConfigUtils.getDefaults().geoServerUrl}); + addBaseUrl: function(options = {}) { + return assign(options, {baseURL: ConfigUtils.getDefaults().geoFenceUrl}); + }, + addBaseUrlGS: function(options = {}) { + const {url: baseURL} = ConfigUtils.getDefaults().geoFenceGeoServerInstance || {}; + return assign(options, {baseURL}); } }; diff --git a/web/client/components/data/grid/DataGrid.jsx b/web/client/components/data/grid/DataGrid.jsx index 0467709b5d..913923bd59 100644 --- a/web/client/components/data/grid/DataGrid.jsx +++ b/web/client/components/data/grid/DataGrid.jsx @@ -27,6 +27,7 @@ class DataGrid extends Grid { componentWillUnmount() { if (this.canvas) { this.canvas.removeEventListener('scroll', this.scrollListener); + this.canvas = null; } if (this.props.displayFilters) { this.onToggleFilter(); diff --git a/web/client/components/manager/rulesmanager/enhancers/__tests__/autoComplete-test.js b/web/client/components/manager/rulesmanager/enhancers/__tests__/autoComplete-test.js new file mode 100644 index 0000000000..e9ec8551f1 --- /dev/null +++ b/web/client/components/manager/rulesmanager/enhancers/__tests__/autoComplete-test.js @@ -0,0 +1,76 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +const ReactDOM = require('react-dom'); +const { createSink, setObservableConfig} = require('recompose'); +const expect = require('expect'); +const autoComplete = require('../autoComplete'); + +const rxjsConfig = require('recompose/rxjsObservableConfig').default; +setObservableConfig(rxjsConfig); + + +describe('autoComplete enhancer', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('it calls load function', (done) => { + const Sink = autoComplete(createSink( props => { + expect(props).toExist(); + expect(props.onChange).toExist(); + expect(props.onSelect).toExist(); + expect(props.onToggle).toExist(); + props.onChange("%"); + })); + const loadData = (search, page, size, parentsFilter, count) => { + expect(search).toBe("%"); + expect(page).toBe(0); + expect(size).toBe(5); + expect(parentsFilter.workspaces).toBe("cite"); + expect(count).toBe(true); + done(); + }; + ReactDOM.render(, document.getElementById("container")); + }); + it('it emits selected val', (done) => { + const Sink = autoComplete(createSink( props => { + expect(props).toExist(); + expect(props.onValueSelected).toExist(); + expect(props.onSelect).toExist(); + props.onSelect("%"); + })); + const onValueSelected = (val) => { + expect(val).toBe("%"); + done(); + }; + ReactDOM.render(, document.getElementById("container")); + }); + it('on toggle with empty val it cleans selected', (done) => { + 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); + })); + 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 new file mode 100644 index 0000000000..845e23dc9a --- /dev/null +++ b/web/client/components/manager/rulesmanager/enhancers/autoComplete.js @@ -0,0 +1,159 @@ +const Rx = require("rxjs"); +const { compose, withStateHandlers, defaultProps, createEventHandler, renameProp} = require('recompose'); +const propsStreamFactory = require('../../../misc/enhancers/propsStreamFactory'); +const {isObject} = require("lodash"); +const stop = stream$ => stream$.filter(() => false); + + +const sameParentFilter = ({parentsFilter: f1}, {parentsFilter: f2}) => f1 === f2; +const sameFilter = ({val: f1}, {val: f2}) => f1 === f2; +const sameEmptyRequest = ({emptyReq: f1}, {emptyReq: f2}) => f1 === f2; + +// ParentFilter change so resets the data +const resetStream = prop$ => prop$.distinctUntilChanged((oP, nP) => sameParentFilter(oP, nP)).skip(1).do(({resetCombo}) => resetCombo()).let(stop); + +// Trigger first loading when value change +const triggerEmptyLoadDataStream = prop$ => prop$.distinctUntilChanged((oP, nP) => sameEmptyRequest(oP, nP)) + .skip(1) + .debounceTime(300) + .switchMap(({emptySearch, loadingErrorMsg, nextPage, prevPage, onError, parentsFilter = {}, size = 10, pagination, loadData, setData = () => {}}) => { + return loadData(emptySearch, 0, size, parentsFilter, true) + .do(({count, data}) => { + setData({ + pagination: {...pagination, loadNextPage: nextPage, loadPrevPage: prevPage, firstPage: true, lastPage: Math.ceil(count / size) <= 1}, + data, + page: 0, + count + }); + }).let(stop).startWith({busy: true}).catch((e) => Rx.Observable.of(e).do(() => { + onError(loadingErrorMsg); + }).mapTo({busy: false})).concat(Rx.Observable.of({busy: false})); + }); + +// Trigger first loading when value change +const triggerLoadDataStream = prop$ => prop$.distinctUntilChanged((oP, nP) => sameFilter(oP, nP)) + .debounceTime(300) + .filter(({val}) => val && val.length > 0) + .switchMap(({loadingErrorMsg, nextPage, prevPage, onError, parentsFilter = {}, val = "", size = 10, pagination, loadData, setData = () => {}}) => { + return loadData(val, 0, size, parentsFilter, true) + .do(({count, data}) => { + setData({ + pagination: {...pagination, loadNextPage: nextPage, loadPrevPage: prevPage, firstPage: true, lastPage: Math.ceil(count / size) <= 1}, + data, + page: 0, + count + }); + }).let(stop).startWith({busy: true}).catch((e) => Rx.Observable.of(e).do(() => { + onError(loadingErrorMsg); + }).mapTo({busy: false})).concat(Rx.Observable.of({busy: false})); + }); + +const loadPageStream = page$ => page$ + .switchMap(({pageStep, page, parentsFilter, count, val, size, + pagination, setData, loadData, onError, loadingErrorMsg}) => { + const newPage = page + pageStep; + return loadData(val, newPage, size, parentsFilter) + .do(({data}) => { + setData({ + pagination: {...pagination, firstPage: newPage === 0, lastPage: Math.ceil(count / size) <= newPage + 1}, + data, + page: newPage + }); + }).let(stop).startWith({busy: true}).catch((e) => Rx.Observable.of(e).do(() => { + onError(loadingErrorMsg); + }).mapTo({busy: false})).concat(Rx.Observable.of({busy: false})); + }); +const dataStreamFactory = prop$ => { + const {handler: nextPage, stream: nextPage$ } = createEventHandler(); + const {handler: prevPage, stream: prevPage$ } = createEventHandler(); + const page$ = Rx.Observable + .merge(nextPage$.mapTo(1), prevPage$.mapTo(-1)) + .withLatestFrom(prop$.map(({onError, loadData, page, parentsFilter, count, + val, size, pagination, setData, loadingErrorMsg}) => ({ + loadData, onError, page, parentsFilter, count, val, size, pagination, + setData, loadingErrorMsg})), (pageStep, other) => ({ pageStep, ...other})); + + const $p = prop$.map(o => ({ ...o, nextPage, prevPage, nextPage$, prevPage$})); + return triggerLoadDataStream($p).merge(triggerEmptyLoadDataStream($p), loadPageStream(page$), resetStream($p)).startWith({busy: false}); +}; + + +/** + * Add remote loading to PaginatedCombo components. The data are managed in the enhanced state. + * Pass as a prop an onLoad function that returns a stream$. onLoad will be called with: + * search text, page (0 first page), size, count (if true should return the number of elements) + * The stream has to return and object with data (array of loaded elements) and [count] the number of + * total elements if required. + * Also pass onValueSelected and selected props. onValueSelected is called whe the users select a value + * from the downloaded data. + * @name autoComplete + * @memberof manager.rulesmanager.enhancers + * @param {PaginatedCombo} Component The PaginatedCombo to enhance with remote loading + * @return {HOC} An HOC that replaces the prop string with localized string. + */ + +module.exports = compose( + defaultProps({ + emptySearch: "%", + stopPropagation: true, + paginated: true, + size: 5, + loadData: () => Rx.Observable.of({data: [], count: 0}), + parentsFilter: {}, + filter: false, + dataStreamFactory, + onValueSelected: () => {}, + onError: () => {}, + loadingErrorMsg: { + title: "", + message: "" + } + }), + withStateHandlers(({paginated, selected, initialData = []}) => ({ + val: selected, + page: 0, + data: initialData, + emptyReq: 0, + pagination: { + paginated: paginated, + firstPage: false, + lastPage: false, + loadPrevPage: () => {}, + loadNextPage: () => {} + }}), { + resetCombo: ({onValueSelected}, { initialData = []}) => () => { + return { + data: initialData, + page: 0, + val: undefined, + select: undefined + }; }, + setData: ({count: oldCount}) => ({pagination, data, page = 0, count = oldCount} = {}) => ({ + pagination, + data, + page, + count + }), + onChange: (state, {initialData = [], valueField}) => (val = "") => { + const currentVal = isObject(val) && val[valueField] || val; + return currentVal.length === 0 && {val: currentVal, data: initialData} || {val: currentVal}; + }, + onToggle: ({ val = "", data, emptyReq}, {selected, onValueSelected, initialData = []}) => (open) => { + if (!open && val === "" && selected) { + onValueSelected(); + }else if (!open && val !== selected) { + return {val: selected, data: initialData}; + }else if (open && val.length === 0 && data.length === 0) { + return {emptyReq: emptyReq + 1}; + } + }, + onSelect: (state, {onValueSelected, selected, valueField}) => (select) => { + const selectedVal = isObject(select) && select[valueField] || select; + if (selectedVal !== selected) { + onValueSelected(selectedVal); + } + } + }), + propsStreamFactory, + renameProp("val", "selectedValue") +); diff --git a/web/client/components/manager/rulesmanager/enhancers/fixedOptions.js b/web/client/components/manager/rulesmanager/enhancers/fixedOptions.js new file mode 100644 index 0000000000..cf2f21492e --- /dev/null +++ b/web/client/components/manager/rulesmanager/enhancers/fixedOptions.js @@ -0,0 +1,65 @@ +const { compose, withStateHandlers, defaultProps, renameProp} = require('recompose'); +const propsStreamFactory = require('../../../misc/enhancers/propsStreamFactory'); +const {isObject} = require("lodash"); +const stop = stream$ => stream$.filter(() => false); + +const sameParentFilter = ({parentsFilter: f1}, {parentsFilter: f2}) => f1 === f2; + +// ParentsFilter change so resets the data +const resetStream = prop$ => prop$.distinctUntilChanged((oP, nP) => sameParentFilter(oP, nP)).skip(1).do(({resetCombo}) => resetCombo()).let(stop); + + +const dataStreamFactory = prop$ => { + return resetStream(prop$).startWith({busy: false}); +}; + +module.exports = compose( + defaultProps({ + stopPropagation: true, + emitOnReset: false, + paginated: false, + parentsFilter: {}, + filter: "startsWith", + dataStreamFactory, + onValueSelected: () => {}, + onError: () => {}, + loadingErroMsg: "", + data: [] + }), + withStateHandlers(({paginated, selected}) => ({ + val: selected, + pagination: { + paginated: paginated, + firstPage: false, + lastPage: false, + loadPrevPage: () => {}, + loadNextPage: () => {} + }}), { + resetCombo: (state, {emitOnReset, onValueSelected}) => () => { + if (emitOnReset) { + onValueSelected(); + } + return { + val: undefined, + select: undefined + }; }, + onChange: () => (val = "") => { + return {val}; + }, + onToggle: ({ val = ""}, {selected, onValueSelected}) => (open) => { + if (!open && val === "" && selected) { + onValueSelected(); + }else if (!open && val !== selected) { + return {val: selected}; + } + }, + onSelect: (state, {onValueSelected, selected, valueField}) => (select) => { + const selectedVal = isObject(select) && select[valueField] || select; + if (selectedVal !== selected) { + onValueSelected(selectedVal); + } + } + }), + propsStreamFactory, + renameProp("val", "selectedValue") +); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/EditMain.jsx b/web/client/components/manager/rulesmanager/ruleseditor/EditMain.jsx new file mode 100644 index 0000000000..48529728db --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/EditMain.jsx @@ -0,0 +1,21 @@ +const React = require('react'); +const {Grid} = require('react-bootstrap'); +const Selectors = require("./attributeselectors"); + +module.exports = ({rule = {}, setOption= () => {}, active = true}) => { + return ( + + { !!rule.id && () } + + + + + + + + + + ); +}; + + diff --git a/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx b/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx new file mode 100644 index 0000000000..7fc8fe0bb0 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/Header.jsx @@ -0,0 +1,31 @@ +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}) => { + const buttons = [{ + glyph: '1-close', + tooltipId: 'rulesmanager.tooltip.close', + onClick: onExit + }, + { + glyph: 'floppy-disk', + tooltipId: 'rulesmanager.tooltip.save', + onClick: onSave, + disabled: disableSave + }]; + return (
+
+ +
+ +
); +}; + + diff --git a/web/client/components/manager/rulesmanager/ruleseditor/__tests__/EditMain-test.jsx b/web/client/components/manager/rulesmanager/ruleseditor/__tests__/EditMain-test.jsx new file mode 100644 index 0000000000..650363a738 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/__tests__/EditMain-test.jsx @@ -0,0 +1,82 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const rxjsConfig = require('recompose/rxjsObservableConfig').default; + +const React = require('react'); +const ReactDOM = require('react-dom'); +const expect = require('expect'); +const Provider = require('react-redux').Provider; +const Editor = require('../EditMain.jsx'); + +const configureMockStore = require('redux-mock-store').default; +const { setObservableConfig } = require('recompose'); +setObservableConfig(rxjsConfig); +const mockStore = configureMockStore(); + +const renderComp = (props, store) => { + return ReactDOM.render( + + + , document.getElementById("container")); +}; + + +describe('Rules Editor Main Editor component', () => { + let store; + beforeEach((done) => { + store = mockStore(); + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('render nothing if not active', () => { + store = mockStore({ + rulesmanager: {} + }); + renderComp({active: false}, store); + const container = document.getElementById('container'); + const el = container.querySelector('.ms-rule-editor'); + expect(el).toExist(); + expect(el.style.display).toBe("none"); + }); + it('render default when active', () => { + store = mockStore({ + rulesmanager: {} + }); + renderComp({active: true}, store); + const container = document.getElementById('container'); + const el = container.querySelector('.ms-rule-editor'); + expect(el).toExist(); + const rows = el.querySelectorAll('.row'); + expect(rows).toExist(); + expect(rows.length).toBe(8); + const disabledRows = el.querySelectorAll('.ms-disabled.row'); + expect(disabledRows).toExist(); + expect(disabledRows.length).toBe(2); + }); + it('render priority selector', () => { + store = mockStore({ + rulesmanager: {} + }); + renderComp({active: true, rule: {id: 10}}, store); + const container = document.getElementById('container'); + const el = container.querySelector('.ms-rule-editor'); + expect(el).toExist(); + const rows = el.querySelectorAll('.row'); + expect(rows).toExist(); + expect(rows.length).toBe(9); + const disabledRows = el.querySelectorAll('.ms-disabled.row'); + expect(disabledRows).toExist(); + expect(disabledRows.length).toBe(2); + }); +}); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/__tests__/Header-test.jsx b/web/client/components/manager/rulesmanager/ruleseditor/__tests__/Header-test.jsx new file mode 100644 index 0000000000..d0e3969a49 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/__tests__/Header-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 Header = require('../Header.jsx'); +describe('Rules Editor Header component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('render default buttons and navigation items', () => { + ReactDOM.render(
, document.getElementById("container")); + const container = document.getElementById('container'); + const el = container.querySelector('.ms-panel-header-container'); + expect(el).toExist(); + const btns = el.querySelectorAll('button'); + expect(btns).toExist(); + expect(btns.length).toBe(2); + const navItems = el.querySelectorAll('.nav.nav-tabs li'); + expect(navItems).toExist(); + expect(navItems.length).toBe(4); + const navItemsDisabled = el.querySelectorAll('.nav.nav-tabs li.disabled'); + expect(navItemsDisabled).toExist(); + expect(navItemsDisabled.length).toBe(3); + + }); + it('render navigation items with details active', () => { + ReactDOM.render(
, document.getElementById("container")); + const container = document.getElementById('container'); + const el = container.querySelector('.ms-panel-header-container'); + expect(el).toExist(); + const btns = el.querySelectorAll('button'); + expect(btns).toExist(); + expect(btns.length).toBe(2); + const navItems = el.querySelectorAll('.nav.nav-tabs li'); + expect(navItems).toExist(); + expect(navItems.length).toBe(4); + const navItemsDisabled = el.querySelectorAll('.nav.nav-tabs li.disabled'); + expect(navItemsDisabled).toExist(); + expect(navItemsDisabled.length).toBe(0); + }); +}); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Access.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Access.jsx new file mode 100644 index 0000000000..e0f0875ab5 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Access.jsx @@ -0,0 +1,47 @@ +/** +* 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 PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const {Row, Col} = require('react-bootstrap'); +const fixedOptions = require("../../enhancers/fixedOptions"); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const { compose, defaultProps, withHandlers, withPropsOnChange} = require('recompose'); +const Message = require('../../../../I18N/Message'); + +const AccessSelector = (props) => ( + + + + + + + + ); + +module.exports = compose( + defaultProps({ + size: 5, + textField: "label", + valueField: "value", + parentsFilter: {}, + filter: "startsWith", + placeholder: "rulesmanager.placeholders.access", + data: [ + {value: "ALLOW", label: "ALLOW"}, + {value: "DENY", label: "DENY"} + ] + }), + withPropsOnChange(["services"], ({services, data}) => ({data: services || data})), + withHandlers({ + onValueSelected: ({setOption = () => {}}) => filterTerm => { + setOption({key: "grant", value: filterTerm}); + } + }), + localizedProps(["placeholder"]), + fixedOptions +)(AccessSelector); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/IpAddress.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/IpAddress.jsx new file mode 100644 index 0000000000..a54b806faa --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/IpAddress.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 {FormControl, FormGroup, Row, Col} = require("react-bootstrap"); +const Message = require('../../../../I18N/Message'); +const {checkIp} = require('../../../../../utils/RulesGridUtils'); +const withLocalized = require("../../../../misc/enhancers/localizedProps"); +const {compose, defaultProps} = require("recompose"); + +class IpAddress extends React.Component { + static propTypes = { + selected: PropTypes.string, + setOption: PropTypes.func, + placeholder: PropTypes.string, + disabled: PropTypes.bool + } + static defaultProps = { + setOption: () => {}, + disabled: false + } + getValidationState() { + if (this.props.selected && this.props.selected.length > 0) { + return this.props.selected.match(checkIp) && "success" || "error"; + } + return null; + } + render() { + const {disabled, selected, placeholder} = this.props; + return ( + + + + + + + + + + + ); + } + handleChange = (e) => { + this.props.setOption({key: "ipaddress", value: e.target.value}); + } +} + +module.exports = compose( + defaultProps({ + placeholder: "rulesmanager.placeholders.ip" + }), + withLocalized(["placeholder"]))(IpAddress); + diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Layer.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Layer.jsx new file mode 100644 index 0000000000..45a6459ba0 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Layer.jsx @@ -0,0 +1,58 @@ +/** +* 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 {Row, Col} = require('react-bootstrap'); +const PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const autoComplete = require("../../enhancers/autoComplete"); +const { compose, defaultProps, withHandlers, withPropsOnChange} = require('recompose'); +const {error} = require('../../../../../actions/notifications'); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const {loadLayers} = require('../../../../../observables/rulesmanager'); +const {connect} = require("react-redux"); +const Message = require('../../../../I18N/Message'); + +const LayerSelector = (props) => ( + + + + + + + + ); + +module.exports = compose( + connect(() => ({}), {onError: error}), + defaultProps({ + paginated: false, + emitOnReset: true, + size: 5, + textField: "name", + valueField: "name", + loadData: loadLayers, + parentsFilter: {}, + filter: false, + placeholder: "rulesmanager.placeholders.layer", + loadingErrorMsg: { + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoadingLayers" + } + }), + withPropsOnChange(["workspace"], ({workspace}) => { + return { + parentsFilter: {workspace}, + disabled: !workspace + }; }), + withHandlers({ + onValueSelected: ({setOption = () => {}}) => filterTerm => { + setOption({key: "layer", value: filterTerm}); + } + }), + localizedProps(["placeholder"]), + autoComplete +)(LayerSelector); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Priority.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Priority.jsx new file mode 100644 index 0000000000..ac2a6052f3 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Priority.jsx @@ -0,0 +1,64 @@ +/** +* 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 {FormControl, FormGroup, Row, Col} = require("react-bootstrap"); +const Message = require('../../../../I18N/Message'); +const {toNumber, isNumber} = require("lodash"); +const withLocalized = require("../../../../misc/enhancers/localizedProps"); +const {compose, defaultProps} = require("recompose"); + +class Priority extends React.Component { + static propTypes = { + selected: PropTypes.string, + setOption: PropTypes.func, + placeholder: PropTypes.string, + disabled: PropTypes.bool + } + static defaultProps = { + setOption: () => {}, + disabled: false + } + getValidationState() { + if (this.props.selected && this.props.selected.length > 0) { + return isNumber(toNumber(this.props.selected)) && "success" || "error"; + } + return null; + } + render() { + const {disabled, selected, placeholder} = this.props; + return ( + + + + + + + + + + + ); + } + handleChange = (e) => { + this.props.setOption({key: "priority", value: e.target.value}); + } +} + +module.exports = compose( + defaultProps({ + placeholder: "rulesmanager.placeholders.priority" + }), + withLocalized(["placeholder"]))(Priority); + diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Request.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Request.jsx new file mode 100644 index 0000000000..bd3fed49f8 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Request.jsx @@ -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 React = require("react"); +const PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const {Row, Col} = require('react-bootstrap'); +const fixedOptions = require("../../enhancers/fixedOptions"); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const { compose, defaultProps, withHandlers, withPropsOnChange} = require('recompose'); +const Message = require('../../../../I18N/Message'); +const {connect} = require("react-redux"); +const {createSelector} = require("reselect"); +const {servicesConfigSel} = require("../../../../../selectors/rulesmanager"); +const selector = createSelector(servicesConfigSel, services => ({ + services +})); + +const RequestSelector = (props) => ( + + + + + + + + ); + +module.exports = compose( + connect(selector), + defaultProps({ + size: 5, + emitOnReset: true, + textField: "label", + valueField: "value", + parentsFilter: {}, + filter: "startsWith", + placeholder: "rulesmanager.placeholders.request", + services: { + "WFS": [ + "DescribeFeatureType", + "GetCapabilities", + "GetFeature", + "GetFeatureWithLock", + "LockFeature", + "Transaction" + ], + "WMS": [ + "DescribeLayer", + "GetCapabilities", + "GetFeatureInfo", + "GetLegendGraphic", + "GetMap", + "GetStyles" + ] + } + }), + withPropsOnChange(["service", "services"], ({services = {}, service}) => { + return { + data: service && (services[service] || []).map(req => ({label: req, value: req.toUpperCase()})), + parentsFilter: {service}, + disabled: !service + }; }), + withHandlers({ + onValueSelected: ({setOption = () => {}}) => filterTerm => { + setOption({key: "request", value: filterTerm}); + } + }), + localizedProps(["placeholder"]), + fixedOptions +)(RequestSelector); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Role.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Role.jsx new file mode 100644 index 0000000000..1d8ce3deb3 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Role.jsx @@ -0,0 +1,52 @@ +/** +* 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 {Row, Col} = require('react-bootstrap'); +const PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const autoComplete = require("../../enhancers/autoComplete"); +const { compose, defaultProps, withHandlers} = require('recompose'); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const {getRoles} = require('../../../../../observables/rulesmanager'); +const {connect} = require("react-redux"); +const {error} = require('../../../../../actions/notifications'); +const Message = require('../../../../I18N/Message'); + +const RoleSelector = (props) => ( + + + + + + + + ); + +module.exports = compose( + connect(() => ({}), {onError: error}), + defaultProps({ + size: 5, + textField: "name", + valueField: "name", + loadData: getRoles, + parentsFilter: {}, + filter: false, + placeholder: "rulesmanager.placeholders.role", + loadingErrorMsg: { + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoadingRoles" + } + }), + withHandlers({ + onValueSelected: ({setOption = () => {}}) => filterTerm => { + setOption({key: "rolename", value: filterTerm}); + } + }), + localizedProps(["placeholder", "loadingErroMsg"]), + autoComplete +)(RoleSelector); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Service.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Service.jsx new file mode 100644 index 0000000000..7b3dc851db --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Service.jsx @@ -0,0 +1,56 @@ +/** +* Copyright 2018, GeoSolutions Sas. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. +*/ +const React = require("react"); +const PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const {Row, Col} = require('react-bootstrap'); +const fixedOptions = require("../../enhancers/fixedOptions"); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const { compose, defaultProps, withHandlers, withPropsOnChange} = require('recompose'); +const Message = require('../../../../I18N/Message'); +const {connect} = require("react-redux"); +const {createSelector} = require("reselect"); +const {servicesSelector} = require("../../../../../selectors/rulesmanager"); + +const selector = createSelector(servicesSelector, ( services) => ({ + services +})); + +const WorkspaceSelector = (props) => ( + + + + + + + + ); + +module.exports = compose( + connect(selector), + defaultProps({ + size: 5, + textField: "label", + valueField: "value", + parentsFilter: {}, + filter: "startsWith", + placeholder: "rulesmanager.placeholders.service", + data: [ + {value: "WMS", label: "WMS"}, + {value: "WFS", label: "WFS"}, + {value: "WCS", label: "WCS"} + ] + }), + withPropsOnChange(["services"], ({services, data}) => ({data: services || data})), + withHandlers({ + onValueSelected: ({setOption = () => {}}) => filterTerm => { + setOption({key: "service", value: filterTerm}); + } + }), + localizedProps(["placeholder"]), + fixedOptions +)(WorkspaceSelector); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/User.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/User.jsx new file mode 100644 index 0000000000..47e0cfa532 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/User.jsx @@ -0,0 +1,52 @@ +/** +* 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 {Row, Col} = require('react-bootstrap'); +const PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const autoComplete = require("../../enhancers/autoComplete"); +const { compose, defaultProps, withHandlers} = require('recompose'); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const {getUsers} = require('../../../../../observables/rulesmanager'); +const {connect} = require("react-redux"); +const {error} = require('../../../../../actions/notifications'); +const Message = require('../../../../I18N/Message'); + +const UserSelector = (props) => ( + + + + + + + + ); + +module.exports = compose( + connect(() => ({}), {onError: error}), + defaultProps({ + size: 5, + textField: "userName", + valueField: "userName", + loadData: getUsers, + parentsFilter: {}, + filter: false, + placeholder: "rulesmanager.placeholders.user", + loadingErrorMsg: { + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoadingRoles" + } + }), + withHandlers({ + onValueSelected: ({setOption = () => {}}) => filterTerm => { + setOption({key: "username", value: filterTerm}); + } + }), + localizedProps(["placeholder", "loadingErroMsg"]), + autoComplete +)(UserSelector); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Workspace.jsx b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Workspace.jsx new file mode 100644 index 0000000000..e1f6aae423 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/Workspace.jsx @@ -0,0 +1,52 @@ +/** +* 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 {Row, Col} = require('react-bootstrap'); +const PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const autoComplete = require("../../enhancers/autoComplete"); +const { compose, defaultProps, withHandlers} = require('recompose'); +const {error} = require('../../../../../actions/notifications'); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const {getWorkspaces} = require('../../../../../observables/rulesmanager'); +const {connect} = require("react-redux"); +const Message = require('../../../../I18N/Message'); + +const WorkspaceSelector = (props) => ( + + + + + + + + ); + +module.exports = compose( + connect(() => ({}), {onError: error}), + defaultProps({ + paginated: false, + size: 5, + textField: "name", + valueField: "name", + loadData: getWorkspaces, + parentsFilter: {}, + filter: "startsWith", + placeholder: "rulesmanager.placeholders.workspace", + loadingErrorMsg: { + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoadingWorkspaces" + } + }), + withHandlers({ + onValueSelected: ({setOption = () => {}}) => filterTerm => { + setOption({key: "workspace", value: filterTerm}); + } + }), + localizedProps(["placeholder"]), + autoComplete +)(WorkspaceSelector); diff --git a/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/index.js b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/index.js new file mode 100644 index 0000000000..4972b2f764 --- /dev/null +++ b/web/client/components/manager/rulesmanager/ruleseditor/attributeselectors/index.js @@ -0,0 +1,12 @@ +module.exports = { + Role: require("./Role"), + User: require("./User"), + Service: require("./Service"), + Request: require("./Request"), + Workspace: require("./Workspace"), + Layer: require("./Layer"), + Access: require("./Access"), + Ip: require("./IpAddress"), + Priority: require("./Priority") +}; + diff --git a/web/client/components/manager/rulesmanager/rulesgrid/RulesGrid.jsx b/web/client/components/manager/rulesmanager/rulesgrid/RulesGrid.jsx new file mode 100644 index 0000000000..04ccd9fd64 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/RulesGrid.jsx @@ -0,0 +1,123 @@ +/** +* 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 RuleRenderer = require('./renderers/RuleRenderer'); + +const { Draggable} = require('react-data-grid-addons'); + +const DataGrid = require('../../../data/grid/DataGrid'); +const { Container: DraggableContainer, DropTargetRowContainer: dropTargetRowContainer } = Draggable; +const PriorityActionCell = require("./renderers/PriorityActionCell"); +const RowRenderer = dropTargetRowContainer(RuleRenderer); + +class RulesGrid extends React.Component { + static propTypes = { + rowKey: PropTypes.string.isRequired, + width: PropTypes.number, + height: PropTypes.number, + rows: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + rowsCount: PropTypes.number, + columns: PropTypes.array, + onSelect: PropTypes.func, + selectedIds: PropTypes.array, + rowGetter: PropTypes.func, + onGridScroll: PropTypes.func, + onAddFilter: PropTypes.func, + onReorderRows: PropTypes.func, + isEditing: PropTypes.bool + }; + static contextTypes = { + messages: PropTypes.object + }; + + static defaultProps = { + rowKey: 'id', + rows: [], + onSort: () => {}, + onSelect: () => {}, + selectedIds: [], + columns: [], + isEditing: false + }; + + componentDidMount() { + if (this.props.rowsCount > 0) { + this.grid.scrollListener(); + } + } + componentDidUpdate = ({rowsCount, isEditing}) => { + if (this.props.rowsCount > 0 && rowsCount !== this.props.rowsCount || (isEditing && !this.props.isEditing)) { + this.grid.scrollListener(); + } + } + componentWillUnmount() { + this.grid = null; + } + onRowsSelected = (rows) => { + const selectedRules = this._getSelectedRow(this.props.rowKey, this.props.selectedIds, this.props.rows).concat(rows.map(({row}) => row).filter(({id}) => id !== "empty_row")); + if (selectedRules.length > 0) { + this.props.onSelect(selectedRules); + } + } + onRowsDeselected = (rows) => { + let rowIds = rows.map(r => r.row[this.props.rowKey]); + const selectedIds = this.props.selectedIds.filter(i => rowIds.indexOf(i) === -1); + this.props.onSelect(this._getSelectedRow(this.props.rowKey, selectedIds, this.props.rows)); + } + render() { + return ( + + { this.grid = grid; }} + enableCellSelection={false} + rowActionsCell={PriorityActionCell} + columns={this.props.columns} + rowGetter={this.props.rowGetter} + rowsCount={this.props.rowsCount} + rows={this.props.rows} + minHeight={this.props.height} + minWidth={this.props.width} + rowRenderer={} + virtualScroll + onGridScroll={this.props.onGridScroll} + onAddFilter={this.props.onAddFilter} + rowSelection={{ + showCheckbox: true, + enableShiftSelect: true, + onRowsSelected: this.onRowsSelected, + onRowsDeselected: this.onRowsDeselected, + selectBy: { + keys: {rowKey: this.props.rowKey, values: this.props.selectedIds} + }}}/> + ); + } + + isDraggedRowSelected = (selectedRows, rowDragSource) => { + if (selectedRows && selectedRows.length > 0) { + let key = this.props.rowKey; + return selectedRows.filter(r => r[key] === rowDragSource.data[key]).length > 0; + } + return false; + }; + _getSelectedRow = ( rowKey = [], selectedKeys = [], rows = {}) => Object.keys(rows).reduce((sel, key) => sel.concat(rows[key].filter(row => selectedKeys.indexOf(row[rowKey]) !== -1)), []) + + reorderRows = (e) => { + if (e.rowSource.data[this.props.rowKey] === "empty_row" || e.rowTarget.data[this.props.rowKey] === "empty_row") { + return; + }else if (e.rowSource.idx === e.rowTarget.idx) { + return; + } + let selectedRows = this._getSelectedRow(this.props.rowKey, this.props.selectedIds, this.props.rows); + let draggedRows = this.isDraggedRowSelected(selectedRows, e.rowSource) ? selectedRows : [e.rowSource.data]; + this.props.onReorderRows({rules: draggedRows, targetPriority: e.rowTarget.data.priority}); + }; +} + +module.exports = RulesGrid; diff --git a/web/client/components/manager/rulesmanager/rulesgrid/__tests__/RuleGrid-test.jsx b/web/client/components/manager/rulesmanager/rulesgrid/__tests__/RuleGrid-test.jsx new file mode 100644 index 0000000000..a64585b403 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/__tests__/RuleGrid-test.jsx @@ -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 React = require('react'); +const ReactDOM = require('react-dom'); + +const expect = require('expect'); +const RulesGrid = require('../RulesGrid'); + + +describe('Test RulesGrid component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('render with defaults', () => { + ReactDOM.render( {}} rowsCount={0} width={100} height={100}/>, document.getElementById("container")); + const container = document.getElementById('container'); + expect(container).toExist(); + const grid = container.querySelector(".react-grid-Container"); + expect(grid).toExist(); + }); +}); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/filtersStream-test.js b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/filtersStream-test.js new file mode 100644 index 0000000000..a31db5b333 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/filtersStream-test.js @@ -0,0 +1,23 @@ +/* + * 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 expect = require('expect'); +const filtersStream = require('../filtersStream'); +const Rx = require("rxjs"); +describe('rulegrid filterStream', () => { + it('debounce addFilter$', (done) => { + const setFilters = (filter) => { + expect(filter).toExist(); + expect(filter).toBe("WFS"); + done(); + }; + const addFilter$ = Rx.Observable.from(["WF", "WFS"]); + const prop$ = Rx.Observable.of({setFilters, addFilter$}); + filtersStream(prop$).subscribe(() => {}); + }); +}); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/scrollStream-test.js b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/scrollStream-test.js new file mode 100644 index 0000000000..5973f29a54 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/scrollStream-test.js @@ -0,0 +1,27 @@ +/* + * 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 expect = require('expect'); +const scrollStream = require('../scrollStream'); +const Rx = require("rxjs"); +describe('rulegrid scrollStream', () => { + it('generate pages request', (done) => { + const moreRules = (pagesRequest) => { + expect(pagesRequest).toExist(); + expect(pagesRequest.pagesToLoad).toExist(); + expect(pagesRequest.pagesToLoad).toEqual([0, 1]); + expect(pagesRequest.startPage).toBe(0); + expect(pagesRequest.endPage).toBe(1); + expect(pagesRequest.pages).toEqual({}); + done(); + }; + const onGridScroll$ = Rx.Observable.of({ firstRowIdx: 0, lastRowIdx: 10}); + const prop$ = Rx.Observable.of({ size: 10, moreRules, pages: {}, rowsCount: 50, vsOverScan: 5, scrollDebounce: 50, onGridScroll$}); + scrollStream(prop$).subscribe(() => {}); + }); +}); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/triggerFetch-test.js b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/triggerFetch-test.js new file mode 100644 index 0000000000..3d86502456 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/triggerFetch-test.js @@ -0,0 +1,38 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const expect = require('expect'); +const triggerFetch = require('../triggerFetch'); +const Rx = require("rxjs"); +const axios = require('../../../../../../libs/ajax'); +const triggerInterceptors = (config) => { + if (config.url.indexOf("geofence/rest/rules/count") !== -1) { + config.url = "base/web/client/test-resources/geofence/rest/rules/count"; + } + return config; +}; + +describe('rulegrid triggerFetch', () => { + it('get count', (done) => { + const inter = axios.interceptors.request.use(triggerInterceptors); + const onLoad = ({pages, rowsCount}) => { + expect(pages).toEqual({}); + expect(rowsCount).toBe(10); + done(); + }; + const onLoadError = () => {}; + const prop$ = Rx.Observable.of({version: 0, filters: {}, setLoading: () => {}, onLoad, onLoadError}); + triggerFetch(prop$).subscribe({ + next: () => {}, + error: () => {}, + complete: () => { + axios.interceptors.request.eject(inter); + } + }); + }); +}); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/virtualScrollFetch-test.js b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/virtualScrollFetch-test.js new file mode 100644 index 0000000000..dfb07d348a --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/__tests__/virtualScrollFetch-test.js @@ -0,0 +1,48 @@ +/* + * 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 expect = require('expect'); +const virtualScrollFetch = require('../virtualScrollFetch'); +const Rx = require("rxjs"); + +const axios = require('../../../../../../libs/ajax'); +const rulesInterceptor = (config) => { + if (config.url.indexOf("geofence/rest/rules") !== -1) { + config.url = "base/web/client/test-resources/geofence/rest/rules/rules.xml"; + } + return config; +}; + +describe('rulegrid virtulaScrollFetch', () => { + it('generate pages request', (done) => { + const inter = axios.interceptors.request.use(rulesInterceptor); + const onLoad = ({pages}) => { + expect(pages).toExist(); + expect(pages[0]).toExist(); + expect(pages[0].length).toBe(5); + done(); + }; + const onLoadError = () => {}; + const pages$ = Rx.Observable.of({ pagesToLoad: [0], startPage: 0, endPage: 0, pages: {}}); + const prop$ = Rx.Observable.of({size: 5, + maxStoredPages: 5, + filters: {}, + onLoad, + moreRules: () => {}, + setLoading: () => {}, + onLoadError + }); + virtualScrollFetch(pages$)(prop$).subscribe({ + next: () => {}, + error: () => {}, + complete: () => { + axios.interceptors.request.eject(inter); + } + }); + }); +}); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/enhancers/filtersStream.js b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/filtersStream.js new file mode 100644 index 0000000000..5ea7af423e --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/filtersStream.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. + */ + +/** + * Function that debaounce filters change + * @param {Observable} Stream of props. + * @return {Observable} Stream of props to trigger the data fetch + */ +module.exports = (props$) => props$ + .distinctUntilChanged(({setFilters}, newProps) => setFilters === newProps.setFilters) + .switchMap(({setFilters, addFilter$}) => addFilter$ + .debounceTime(500) + .do((f) => setFilters(f)) + ); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/enhancers/reorderRules.js b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/reorderRules.js new file mode 100644 index 0000000000..757febeb40 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/reorderRules.js @@ -0,0 +1,33 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const Rx = require("rxjs"); +const samePages = ({pages: oP}, {pages: nP}) => oP === nP; +const { moveRules } = require('../../../../../observables/rulesmanager'); + +/** + * Function that converts stream of a reorder rules into action that updates geofence + * @param {Observable} Stream of props. + * @return {Observable} Stream of props to trigger the data fetch + */ +module.exports = () => (prop$) => + prop$.distinctUntilChanged((oP, nP) => samePages(oP, nP)) + .switchMap(({ orderRule$, setLoading, setData, onLoadError, pages}) => + orderRule$ + .switchMap(({rules, targetPriority}) => { + setData({pages, editing: true}); + setLoading(true); + return moveRules(targetPriority, rules) + .catch((e) => Rx.Observable.of({ + error: e + }).do(() => onLoadError({ + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorMovingRules" + })).do(() => setLoading(false))) + .do(() => setData({pages: []})); + } +)); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/enhancers/rulesgrid.js b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/rulesgrid.js new file mode 100644 index 0000000000..129ca9dcc7 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/rulesgrid.js @@ -0,0 +1,110 @@ +const React = require("react"); +const { compose, withPropsOnChange, withHandlers, withStateHandlers, defaultProps, createEventHandler } = require('recompose'); +const propsStreamFactory = require('../../../../misc/enhancers/propsStreamFactory'); +const triggerFetch = require("./triggerFetch"); +const virtualScrollFetch = require("./virtualScrollFetch"); +const reorderRules = require("./reorderRules"); +const scrollStream = require("./scrollStream"); +const filtersStream = require("./filtersStream"); +const FilterRenderers = require("../filterRenderers"); +const Message = require('../../../../I18N/Message'); +const AccessFormatter = require('../formatters/AccessFormatter'); +const {getRow, flattenPages, getOffsetFromTop} = require('../../../../../utils/RulesGridUtils'); + +const emitStop = stream$ => stream$.filter(() => false).startWith({}); +const triggerLoadStream = prop$ => prop$.distinctUntilChanged(({triggerLoad}, nP) => triggerLoad === nP.triggerLoad) + .skip(1) + .do(({incrementVersion}) => incrementVersion()); + +const dataStreamFactory = $props => { + const {handler: onGridScroll, stream: onGridScroll$ } = createEventHandler(); + const {handler: moreRules, stream: page$ } = createEventHandler(); + const {handler: onAddFilter, stream: addFilter$ } = createEventHandler(); + const {handler: onReorderRows, stream: orderRule$ } = createEventHandler(); + const $p = $props.map(o => ({ ...o, onGridScroll$, addFilter$, orderRule$, moreRules})); + return triggerFetch($p).let(emitStop) + .combineLatest([ + virtualScrollFetch(page$)($p).let(emitStop), + reorderRules(page$)($p).let(emitStop), + filtersStream($p).let(emitStop), + scrollStream($p).let(emitStop), + triggerLoadStream($props).let(emitStop) + ]) + .mapTo({onGridScroll, + onAddFilter, + onReorderRows}); + +}; + +module.exports = compose( + defaultProps({ + sortable: false, + size: 20, + onSelect: () => {}, + onLoadError: () => {}, + setLoading: () => {}, + dataStreamFactory, + virtualScroll: true, + setFilters: () => {}, + columns: [ + { key: 'rolename', name: , filterable: true, filterRenderer: FilterRenderers.RolesFilter}, + { key: 'username', name: , filterable: true, filterRenderer: FilterRenderers.UsersFilter}, + { key: 'ipaddress', name: , filterable: false}, + { key: 'service', name: , filterable: true, filterRenderer: FilterRenderers.ServicesFilter}, + { key: 'request', name: , filterable: true, filterRenderer: FilterRenderers.RequestsFilter }, + { key: 'workspace', name: , filterable: true, filterRenderer: FilterRenderers.WorkspacesFilter}, + { key: 'layer', name: , filterable: true, filterRenderer: FilterRenderers.LayersFilter}, + { key: 'grant', name: , formatter: AccessFormatter, filterable: false } + ] + }), + withStateHandlers({ + pages: {}, + rowsCount: 0, + version: 0, + isEditing: false + }, { + setData: ({rowsCount: oldRowsCount}) => ({pages, rowsCount = oldRowsCount, editing = false} = {}) => ({ + pages, + rowsCount, + error: undefined, + isEditing: editing + }), + setFilters: (state, {filters = {}, setFilters}) => ({column, filterTerm}) => { + // Can add some logic here to clean related filters + if (column.key === "workspace" && filters.layer) { + setFilters("layer"); + }else if (column.key === "service" && filters.request) { + setFilters("request"); + } + setFilters(column.key, filterTerm); + return {rowsCount: 0, pages: {}}; + }, + incrementVersion: ({ version }) => () => ({ + version: version + 1, + isEditing: true + }) + }), + withHandlers({ + onLoad: ({ setData = () => {}, onLoad = () => {}} = {}) => (...args) => { + setData(...args); + onLoad(...args); + }, + onSelect: ({onSelect: select, pages, rowsCount}) => (selected) => { + if ( selected.length === 1) { + const offsetFromTop = getOffsetFromTop(selected[0], flattenPages(pages)); + select(selected, false, false, {offsetFromTop, rowsCount}); + }else { + select(selected); + } + } + }), + withPropsOnChange( + ["enableColumnFilters", "pages"], + props => ({ + displayFilters: props.enableColumnFilters, + rows: props.pages + }) + ), + withHandlers({ rowGetter: props => i => getRow(i, props.rows, props.size) }), + propsStreamFactory +); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/enhancers/scrollStream.js b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/scrollStream.js new file mode 100644 index 0000000000..975f69ef60 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/scrollStream.js @@ -0,0 +1,35 @@ +/* + * 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. + */ + +/** + * Function that converts a stream of a scrollEvent into page requestes + * @param {Observable} Stream of props. + * @return {Observable} Stream of props to trigger the data fetch + */ + +const {getPagesToLoad} = require('../../../../../utils/RulesGridUtils'); + +const sameRowsCount = ({rowsCount: oR}, {rowsCount: nR}) => oR === nR; +const samePages = ({pages: oP}, {pages: nP}) => oP === nP; + +module.exports = ($props) => { + return $props.distinctUntilChanged((oP, nP) => sameRowsCount(oP, nP) && samePages(oP, nP)) + .switchMap(({ size, moreRules, pages, rowsCount, vsOverScan = 5, scrollDebounce = 50, onGridScroll$ }) => { + return onGridScroll$.filter(() => rowsCount !== 0) + .debounceTime(scrollDebounce) + .map(({ firstRowIdx, lastRowIdx }) => { + const fr = firstRowIdx - vsOverScan < 0 ? 0 : firstRowIdx - vsOverScan; + const lr = lastRowIdx + vsOverScan > rowsCount - 1 ? rowsCount - 1 : lastRowIdx + vsOverScan; + const startPage = Math.floor(fr / size); + const endPage = Math.floor(lr / size); + return {pagesToLoad: getPagesToLoad(startPage, endPage, pages), startPage, endPage, pages}; + }) + .filter(({pagesToLoad}) => pagesToLoad.length > 0) + .do((p) => moreRules(p)); + }); +}; diff --git a/web/client/components/manager/rulesmanager/rulesgrid/enhancers/triggerFetch.js b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/triggerFetch.js new file mode 100644 index 0000000000..0c5e1df1c9 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/triggerFetch.js @@ -0,0 +1,35 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const Rx = require('rxjs'); + +const sameFilter = (f1, {filters: f2}) => f1 === f2; +const sameVersion = (f1, {version: f2}) => f1 === f2; +const { getCount } = require('../../../../../observables/rulesmanager'); +/** + * Function that converts stream of a RulesGrid props to trigger data fetch events, It gets the rules count + * @param {Observable} Stream of props. + * @return {Observable} Stream of props to trigger the data fetch + */ +module.exports = ($props) => { + return $props.distinctUntilChanged( + ({filters, version}, newProps) => sameVersion(version, newProps) && sameFilter(filters, newProps)) + .switchMap(({filters, setLoading, onLoad, onLoadError = () => { }}) => { + setLoading(true); + return getCount(filters) + .do(() => setLoading(false)) + .do((rowsCount) => onLoad({pages: {}, rowsCount})) + .catch((e) => Rx.Observable.of({ + error: e + }).do(() => onLoadError({ + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoadingRules" + })).do(() => setLoading(false))); + } + ); +}; diff --git a/web/client/components/manager/rulesmanager/rulesgrid/enhancers/virtualScrollFetch.js b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/virtualScrollFetch.js new file mode 100644 index 0000000000..bad7940295 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/enhancers/virtualScrollFetch.js @@ -0,0 +1,53 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const Rx = require('rxjs'); +const { loadRules } = require('../../../../../observables/rulesmanager'); +const sameFilter = ({filters: f1}, {filters: f2}) => f1 === f2; +const {updatePages, getPagesToLoad} = require('../../../../../utils/RulesGridUtils'); +/** + * Create an operator that responds to page$ request stream. + * While loading the pages request are stored and the last request is emitted on load end + * @param {Observable} page$ the stream of virtual scroll pages requests + * @returns a function that can be merged with stream of + * props to retrieve data using virtual scroll. This stearm doesn't emit + */ +module.exports = page$ => props$ => props$.distinctUntilChanged((oProps, nProps) => sameFilter(oProps, nProps)) + .switchMap(({size = 5, maxStoredPages = 5, filters = {}, + onLoad = () => { }, moreRules, setLoading, onLoadError = () => { } + }) => page$.delay(1).exhaustMap((pagesRequest) => { + // First request + setLoading(true); + return loadRules(pagesRequest.pagesToLoad, filters, size) + .do(newPages => onLoad({ + ...updatePages(newPages.pages, pagesRequest, maxStoredPages) + })) + .do(() => setLoading(false)) + .catch((e) => Rx.Observable.of({ + error: e + }).do(() => onLoadError({ + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoadingRules" + })).do(() => setLoading(false))) // Store pages requests and emit on first request end + .withLatestFrom(page$, ({pages: nPages}, lastRequest) => ({ + lastRequest, + nPages + })) + .filter(({error}) => !error) + .map(({lastRequest, nPages}) => { + // Prepare the new request merging last loaded pages and last pages request + const {pages: tPages} = updatePages(nPages, pagesRequest, maxStoredPages); + const pagesToLoad = getPagesToLoad(lastRequest.startPage, lastRequest.endPage, tPages); + return {...lastRequest, + pages: tPages, + pagesToLoad}; + }) + .filter(({pagesToLoad}) => pagesToLoad.length > 0) + .do((newRequest) => moreRules(newRequest)); + }) +); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/LayersFilter.jsx b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/LayersFilter.jsx new file mode 100644 index 0000000000..a5beabcfb2 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/LayersFilter.jsx @@ -0,0 +1,49 @@ +/** +* 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 PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const autoComplete = require("../../enhancers/autoComplete"); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const { compose, defaultProps, withHandlers} = require('recompose'); +const {loadLayers} = require('../../../../../observables/rulesmanager'); +const {error} = require('../../../../../actions/notifications'); +const {connect} = require("react-redux"); +const {createSelector} = require("reselect"); +const {filterSelector} = require("../../../../../selectors/rulesmanager"); +const workspaceSelector = createSelector(filterSelector, (filter) => filter.workspace); +const parentFiltersSel = createSelector(workspaceSelector, (workspace) => ({ + workspace +})); +const selector = createSelector([filterSelector, parentFiltersSel], (filter, parentsFilter) => ({ + selected: filter.layer, + disabled: !filter.workspace, + parentsFilter +})); + +module.exports = compose( + connect(selector, {onError: error}), + defaultProps({ + size: 5, + textField: "name", + valueField: "name", + loadData: loadLayers, + parentsFilter: {}, + filter: false, + placeholder: "rulesmanager.placeholders.filter", + loadingErrorMsg: { + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoadingLayers" + } + }), + withHandlers({ + onValueSelected: ({column = {}, onFilterChange = () => {}}) => filterTerm => { + onFilterChange({column, filterTerm}); + } + }), + localizedProps(["placeholder"]), + autoComplete +)(PagedCombo); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/RequestsFilter.jsx b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/RequestsFilter.jsx new file mode 100644 index 0000000000..e8bc4fbe4d --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/RequestsFilter.jsx @@ -0,0 +1,68 @@ +/** +* 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 PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const fixedOptions = require("../../enhancers/fixedOptions"); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const { compose, defaultProps, withHandlers, withPropsOnChange} = require('recompose'); + +const {connect} = require("react-redux"); +const {createSelector} = require("reselect"); +const {filterSelector, servicesConfigSel} = require("../../../../../selectors/rulesmanager"); +const serviceSelector = createSelector(filterSelector, (filter) => filter.service); +const parentFiltersSel = createSelector(serviceSelector, (service) => ({ + parentFilters: {service} +})); +const selector = createSelector([filterSelector, parentFiltersSel, servicesConfigSel], (filter, parentsFilter, services) => ({ + selected: filter.request, + disabled: !filter.service, + service: filter.service, + parentsFilter, + services +})); + + +module.exports = compose( + connect(selector), + defaultProps({ + size: 5, + textField: "label", + valueField: "value", + parentsFilter: {}, + filter: "startsWith", + placeholder: "rulesmanager.placeholders.filter", + services: { + "WFS": [ + "DescribeFeatureType", + "GetCapabilities", + "GetFeature", + "GetFeatureWithLock", + "LockFeature", + "Transaction" + ], + "WMS": [ + "DescribeLayer", + "GetCapabilities", + "GetFeatureInfo", + "GetLegendGraphic", + "GetMap", + "GetStyles" + ] + } + }), + withPropsOnChange(["service", "services"], ({services = {}, service}) => { + return { + data: service && (services[service] || []).map(req => ({label: req, value: req.toUpperCase()})) + }; }), + withHandlers({ + onValueSelected: ({column = {}, onFilterChange = () => {}}) => filterTerm => { + onFilterChange({column, filterTerm}); + } + }), + localizedProps(["placeholder"]), + fixedOptions +)(PagedCombo); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/RolesFilter.jsx b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/RolesFilter.jsx new file mode 100644 index 0000000000..f727879e52 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/RolesFilter.jsx @@ -0,0 +1,43 @@ +/** +* 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 PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const autoComplete = require("../../enhancers/autoComplete"); +const { compose, defaultProps, withHandlers} = require('recompose'); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const {getRoles} = require('../../../../../observables/rulesmanager'); +const {connect} = require("react-redux"); +const {createSelector} = require("reselect"); +const {error} = require('../../../../../actions/notifications'); +const {filterSelector} = require("../../../../../selectors/rulesmanager"); +const selector = createSelector(filterSelector, (filter) => ({ + selected: filter.rolename +})); + +module.exports = compose( + connect(selector, {onError: error}), + defaultProps({ + size: 5, + textField: "name", + valueField: "name", + loadData: getRoles, + parentsFilter: {}, + filter: false, + placeholder: "rulesmanager.placeholders.filter", + loadingErrorMsg: { + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoadingRoles" + } + }), + withHandlers({ + onValueSelected: ({column = {}, onFilterChange = () => {}}) => filterTerm => { + onFilterChange({column, filterTerm}); + } + }), + localizedProps(["placeholder", "loadingErroMsg"]), + autoComplete +)(PagedCombo); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/ServicesFilter.jsx b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/ServicesFilter.jsx new file mode 100644 index 0000000000..2dd2e6bc17 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/ServicesFilter.jsx @@ -0,0 +1,45 @@ +/** +* 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 PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const fixedOptions = require("../../enhancers/fixedOptions"); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const { compose, defaultProps, withHandlers, withPropsOnChange} = require('recompose'); + +const {connect} = require("react-redux"); +const {createSelector} = require("reselect"); +const {filterSelector, servicesSelector} = require("../../../../../selectors/rulesmanager"); + +const selector = createSelector(filterSelector, servicesSelector, (filter, services) => ({ + selected: filter.service, + services +})); + +module.exports = compose( + connect(selector), + defaultProps({ + size: 5, + textField: "label", + valueField: "value", + parentsFilter: {}, + filter: "startsWith", + placeholder: "rulesmanager.placeholders.filter", + data: [ + {value: "WMS", label: "WMS"}, + {value: "WFS", label: "WFS"}, + {value: "WCS", label: "WCS"} + ] + }), + withPropsOnChange(["services"], ({services, data}) => ({data: services || data})), + withHandlers({ + onValueSelected: ({column = {}, onFilterChange = () => {}}) => filterTerm => { + onFilterChange({column, filterTerm}); + } + }), + localizedProps(["placeholder"]), + fixedOptions +)(PagedCombo); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/UsersFilter.jsx b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/UsersFilter.jsx new file mode 100644 index 0000000000..96b5f02782 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/UsersFilter.jsx @@ -0,0 +1,43 @@ +/** +* 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 PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const autoComplete = require("../../enhancers/autoComplete"); +const { compose, defaultProps, withHandlers} = require('recompose'); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const {getUsers} = require('../../../../../observables/rulesmanager'); +const {connect} = require("react-redux"); +const {createSelector} = require("reselect"); +const {filterSelector} = require("../../../../../selectors/rulesmanager"); +const {error} = require('../../../../../actions/notifications'); +const selector = createSelector(filterSelector, (filter) => ({ + selected: filter.username +})); + +module.exports = compose( + connect(selector, {onError: error}), + defaultProps({ + size: 5, + textField: "userName", + valueField: "userName", + loadData: getUsers, + parentsFilter: {}, + filter: false, + placeholder: "rulesmanager.placeholders.filter", + loadingErrorMsg: { + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoadingUsers" + } + }), + withHandlers({ + onValueSelected: ({column = {}, onFilterChange = () => {}}) => filterTerm => { + onFilterChange({column, filterTerm}); + } + }), + localizedProps(["placeholder"]), + autoComplete +)(PagedCombo); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/WorkspacesFilter.jsx b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/WorkspacesFilter.jsx new file mode 100644 index 0000000000..0b9f0571f2 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/WorkspacesFilter.jsx @@ -0,0 +1,46 @@ +/** +* 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 PagedCombo = require('../../../../misc/combobox/PagedCombobox'); +const autoComplete = require("../../enhancers/autoComplete"); +const { compose, defaultProps, withHandlers} = require('recompose'); +const {error} = require('../../../../../actions/notifications'); +const localizedProps = require("../../../../misc/enhancers/localizedProps"); +const {getWorkspaces} = require('../../../../../observables/rulesmanager'); +const {connect} = require("react-redux"); +const {createSelector} = require("reselect"); +const {filterSelector} = require("../../../../../selectors/rulesmanager"); + +const selector = createSelector(filterSelector, (filter) => ({ + selected: filter.workspace +})); + + +module.exports = compose( + connect(selector, {onError: error}), + defaultProps({ + paginated: false, + size: 5, + textField: "name", + valueField: "name", + loadData: getWorkspaces, + parentsFilter: {}, + filter: "startsWith", + placeholder: "rulesmanager.placeholders.filter", + loadingErrorMsg: { + title: "rulesmanager.errorTitle", + message: "rulesmanager.errorLoadingWorkspaces" + } + }), + withHandlers({ + onValueSelected: ({column = {}, onFilterChange = () => {}}) => filterTerm => { + onFilterChange({column, filterTerm}); + } + }), + localizedProps(["placeholder"]), + autoComplete +)(PagedCombo); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/__tests__/WorkspacesFilter-test.jsx b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/__tests__/WorkspacesFilter-test.jsx new file mode 100644 index 0000000000..4454b8212d --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/__tests__/WorkspacesFilter-test.jsx @@ -0,0 +1,44 @@ +/* + * 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 PropTypes = require('prop-types'); + +const { withContext } = require('recompose'); +const mockStore = withContext({ + store: PropTypes.any +}, ({store = {}} = {}) => ({ + store: { + dispatch: () => { }, + subscribe: () => { }, + getState: () => ({}), + ...store + } +})); + +const WorkspacesFilter = mockStore(require('../WorkspacesFilter')); + + +describe('it render components', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('Load Workspaces', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const input = container.querySelector('input'); + expect(input).toExist(); + }); +}); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/index.js b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/index.js new file mode 100644 index 0000000000..cc24299dcb --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/filterRenderers/index.js @@ -0,0 +1,8 @@ +module.exports = { + RolesFilter: require('./RolesFilter'), + UsersFilter: require('./UsersFilter'), + WorkspacesFilter: require('./WorkspacesFilter'), + LayersFilter: require('./LayersFilter'), + ServicesFilter: require('./ServicesFilter'), + RequestsFilter: require('./RequestsFilter') +}; diff --git a/web/client/components/manager/rulesmanager/rulesgrid/formatters/AccessFormatter.jsx b/web/client/components/manager/rulesmanager/rulesgrid/formatters/AccessFormatter.jsx new file mode 100644 index 0000000000..e918ab195f --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/formatters/AccessFormatter.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 accessField = { + ALLOW: { + className: 'ms-allow-cell' + }, + DENY: { + className: 'ms-deny-cell' + } +}; + +module.exports = ({value = 'DENY', msClasses = accessField}) => ( +
+
+ {value.toUpperCase()} +
+
+ ); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/formatters/__tests__/AccessFormatter-test.jsx b/web/client/components/manager/rulesmanager/rulesgrid/formatters/__tests__/AccessFormatter-test.jsx new file mode 100644 index 0000000000..bfb027a58a --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/formatters/__tests__/AccessFormatter-test.jsx @@ -0,0 +1,35 @@ +/** + * 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 AccessFormatter = require('../AccessFormatter'); +const expect = require('expect'); + +describe('Test AccessFormatter component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + it('render with defaults', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const el = container.querySelector('.ms-deny-cell'); + expect(el).toExist(); + }); + it('render with ALLOW', () => { + ReactDOM.render(, document.getElementById("container")); + const container = document.getElementById('container'); + const el = container.querySelector('.ms-allow-cell'); + expect(el).toExist(); + }); +}); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/renderers/PriorityActionCell.jsx b/web/client/components/manager/rulesmanager/rulesgrid/renderers/PriorityActionCell.jsx new file mode 100644 index 0000000000..e3aa99e6e7 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/renderers/PriorityActionCell.jsx @@ -0,0 +1,65 @@ +const React = require('react'); +const PropTypes = require('prop-types'); +const { DragSource: dragSource } = require('react-dnd'); +const { editors } = require('react-data-grid'); +const { CheckboxEditor } = editors; + +class PriorityActionsCell extends React.Component { + + renderRowIndex() { + return (
+ { this.props.dependentValues && this.props.dependentValues.priority || "" } +
); + } + + render() { + const {connectDragSource, rowSelection} = this.props; + let rowHandleStyle = rowSelection !== null ? {position: 'absolute', marginTop: '5px'} : {}; + let isSelected = this.props.value; + let editorClass = isSelected ? 'rdg-actions-checkbox selected' : 'rdg-actions-checkbox'; + + return connectDragSource( +
+
+ {!isSelected ? this.renderRowIndex() : null} + {rowSelection !== null &&
+ +
} +
); + } +} + +PriorityActionsCell.propTypes = { + rowIdx: PropTypes.number.isRequired, + connectDragSource: PropTypes.func.isRequired, + connectDragPreview: PropTypes.func.isRequired, + isDragging: PropTypes.bool.isRequired, + isRowHovered: PropTypes.bool, + column: PropTypes.object, + dependentValues: PropTypes.object, + value: PropTypes.bool, + rowSelection: PropTypes.object.isRequired +}; + +PriorityActionsCell.defaultProps = { + rowIdx: 0 +}; + +function collect(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + connectDragPreview: connect.dragPreview() + }; +} + +const rowIndexSource = { + beginDrag(props) { + return { idx: props.rowIdx, data: props.dependentValues }; + }, + endDrag(props) { + return { idx: props.rowIdx, data: props.dependentValues }; + } +}; + +module.exports = dragSource('Row', rowIndexSource, collect)(PriorityActionsCell); diff --git a/web/client/components/manager/rulesmanager/rulesgrid/renderers/RuleRenderer.jsx b/web/client/components/manager/rulesmanager/rulesgrid/renderers/RuleRenderer.jsx new file mode 100644 index 0000000000..7d2678d9b1 --- /dev/null +++ b/web/client/components/manager/rulesmanager/rulesgrid/renderers/RuleRenderer.jsx @@ -0,0 +1,47 @@ +/** +* 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 { Row: Rule } = require('react-data-grid'); + +const accessField = { + ALLOW: { + classNameRow: 'ms-allow-row' + }, + DENY: { + classNameRow: 'ms-deny-row' + } +}; + +class RuleRenderer extends React.Component { + static propTypes = { + idx: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number]), + row: PropTypes.object, + isSelected: PropTypes.bool + }; + static defaultProps = { + selected: [] + } + constructor(props) { + super(props); + this.setScrollLeft = (scrollBy) => this.row.setScrollLeft(scrollBy); + } + componentWillUnmount() { + this.setScrollLeft = null; + } + render() { + const {row = {}, isSelected} = this.props; + const extraClasses = (isSelected && ' ms-row-select ' || '') + ((accessField[row.grant] || {}).classNameRow || ' '); + return ( this.row = node } extraClasses={extraClasses} {...this.props} />); + } + +} + +module.exports = RuleRenderer; diff --git a/web/client/components/misc/combobox/PagedCombobox.jsx b/web/client/components/misc/combobox/PagedCombobox.jsx index 610b65e9ad..80ed8533fa 100644 --- a/web/client/components/misc/combobox/PagedCombobox.jsx +++ b/web/client/components/misc/combobox/PagedCombobox.jsx @@ -48,7 +48,9 @@ class PagedCombobox extends React.Component { selectedValue: PropTypes.string, textField: PropTypes.string, tooltip: PropTypes.object, - valueField: PropTypes.string + valueField: PropTypes.string, + placeholder: PropTypes.string, + stopPropagation: PropTypes.bool }; static contextTypes = { @@ -56,6 +58,7 @@ class PagedCombobox extends React.Component { }; static defaultProps = { + stopPropagation: false, dropUp: false, itemComponent: AutocompleteListItem, loading: false, @@ -106,10 +109,20 @@ class PagedCombobox extends React.Component { return (
{ !firstPage && - this.props.pagination.loadPrevPage() }/> + { + if (this.props.stopPropagation) { + e.stopPropagation(); + } + this.props.pagination.loadPrevPage(); + }}/> } { !lastPage && - this.props.pagination.loadNextPage()}/> + { + if (this.props.stopPropagation) { + e.stopPropagation(); + } + this.props.pagination.loadNextPage(); + }}/> }
); @@ -130,6 +143,7 @@ class PagedCombobox extends React.Component { } const data = this.props.loading ? [] : options; const field = ( this.props.onChange(val)} onFocus={() => this.props.onFocus(this.props.data)} onSelect={(v) => this.props.onSelect(v)} - onToggle={() => this.props.onToggle()} + onToggle={(stato) => this.props.onToggle(stato)} textField={this.props.textField} valueField={this.props.valueField} value={this.props.selectedValue} diff --git a/web/client/epics/rulesmanager.js b/web/client/epics/rulesmanager.js new file mode 100644 index 0000000000..e2276634c5 --- /dev/null +++ b/web/client/epics/rulesmanager.js @@ -0,0 +1,25 @@ +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"); +// To do add Error management +const {get} = require("lodash"); +const saveRule = stream$ => stream$ + .mapTo({type: RULE_SAVED}) + .catch(() => { + return Rx.Observable.of(error({title: "rulesmanager.errorTitle", message: "rulesmanager.errorUpdatingRule"})); + }) + .startWith(setLoading(true)) + .concat(Rx.Observable.of(setLoading(false))); +module.exports = { + onSave: (action$, {getState}) => action$.ofType(SAVE_RULE) + .exhaustMap(({rule}) => + rule.id ? updateRule(rule, get(getState(), "rulesmanager.activeRule", {})).let(saveRule) : createRule(rule).let(saveRule) + ), + 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); + }) +}; + diff --git a/web/client/observables/rulesmanager.js b/web/client/observables/rulesmanager.js new file mode 100644 index 0000000000..5ef6f567b2 --- /dev/null +++ b/web/client/observables/rulesmanager.js @@ -0,0 +1,105 @@ +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 xmlToJson$ = response => Rx.Observable.bindNodeCallback( (data, callback) => parseString(data, { + tagNameProcessors: [stripPrefix], + explicitArray: false, + mergeAttrs: true +}, callback))(response); + + +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 || [])})) + ); +const countUsers = (filter = "") => Rx.Observable.defer(() => GeoFence.getUsersCount(filter)); +const loadUsers = (filter = "", page = 0, size = 10) => Rx.Observable.defer(() => GeoFence.getUsers(filter, page, size)) +.switchMap( response => xmlToJson$(response). + map(({UserList = {}}) => ({users: [].concat(UserList.User || [])})) +); + +const countRoles = (filter = "") => Rx.Observable.defer(() => GeoFence.getGroupsCount(filter)); + +const loadRoles = (filter = "", page = 0, size = 10) => Rx.Observable.defer(() => GeoFence.getGroups(filter, page, size)) +.switchMap( response => xmlToJson$(response). + map(({UserGroupList = {}}) => ({users: [].concat(UserGroupList.UserGroup || [])})) +); +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) + .switchMap(({rule, origRule}) => deleteRule(rule.id) + .switchMap(() => { + const {priority, id, ...newRule} = rule; + return Rx.Observable.defer(() => GeoFence.addRule(newRule)) + .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); }); + }); + }) + .switchMap(({data: id}) => { + 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)) + .switchMap(({rule, origRule}) => deleteRule(rule.id) + .switchMap(() => { + const {priority, id, ...newRule} = rule; + newRule.position = {value: priority, position: "fixedPriority"}; + return Rx.Observable.defer(() => GeoFence.addRule(newRule)) + .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); }); + }); + }) + ); +// 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) + .switchMap(({rule}) => Rx.Observable.defer(() => GeoFence.updateRule(rule))); +module.exports = { + loadRules: (pages = [], filters = {}, size) => + Rx.Observable.combineLatest(pages.map(p => loadSinglePage(p, filters, size))) + .map(results => results.reduce( (acc, {page, rules}) => ({...acc, [page]: rules}), {})) + .map(p => ({pages: p})), + getCount: (filters = {}) => Rx.Observable.defer(() => GeoFence.getRulesCount(filters)), + moveRules: (targetPriority, rulesIds) => Rx.Observable.defer(() => GeoFence.moveRules(targetPriority, rulesIds)), + getUsers: (userFilter = "", page = 0, size = 10, parentsFilter = {}, countEl = false) => { + return countEl && Rx.Observable.combineLatest([countUsers(userFilter), loadUsers(userFilter, page, size)], (count, {users}) => ({ + count, + data: users + })) || loadUsers(userFilter, page, size).map(({users}) => ({data: users})); + }, + getRoles: (roleFilter = "", page = 0, size = 10, parentsFilter = {}, countEl = false) => { + return countEl && Rx.Observable.combineLatest([countRoles(roleFilter), loadRoles(roleFilter, page, size)], (count, {users}) => ({ + count, + data: users + })) || loadRoles(roleFilter, page, size).map(({users}) => ({data: users})); + }, + getWorkspaces: ({size}) => Rx.Observable.defer(() => GeoFence.getWorkspaces()) + .map(({workspaces = {}}) => ({count: size, data: [].concat(workspaces.workspace)})), + loadLayers: (layerFilter = "", page = 0, size = 10, parentsFilter = {}) => { + const {url: baseURL} = ConfigUtils.getDefaults().geoFenceGeoServerInstance || {}; + const catalogUrl = baseURL + 'csw'; + const {workspace = ""} = parentsFilter; + return Rx.Observable.defer(() => + CatalogAPI.workspaceSearch(catalogUrl, (page) * size + 1, size, layerFilter, workspace)) + .map((layers) => ({data: layers.records.map(layer => ({name: layer.dc.identifier.replace(/^.*?:/g, '')})), count: layers.numberOfRecordsMatched})); + }, + updateRule: (rule, origRule) => { + const fullUp = Rx.Observable.of({rule, origRule}).let(fullUpdate); + const simpleUpdate = Rx.Observable.of({rule, origRule}).let(justUpdate); + const grant = Rx.Observable.of({rule, origRule}).let(grantUpdate); + return fullUp.merge(simpleUpdate, grant); + }, + createRule: (rule) => Rx.Observable.defer(() => GeoFence.addRule(rule)), + deleteRule + +}; diff --git a/web/client/plugins/RulesDataGrid.jsx b/web/client/plugins/RulesDataGrid.jsx new file mode 100644 index 0000000000..507eb080fe --- /dev/null +++ b/web/client/plugins/RulesDataGrid.jsx @@ -0,0 +1,77 @@ +/* + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const React = require('react'); +const {connect} = require('react-redux'); +const {compose} = require("recompose"); + +const {createSelector} = require('reselect'); +const {selectedRules, filterSelector, isEditorActive, triggerLoadSel} = require('../selectors/rulesmanager'); + +const ContainerDimensions = require('react-container-dimensions').default; +const PropTypes = require('prop-types'); +const {rulesSelected, setLoading, setFilter} = require("../actions/rulesmanager"); +const {error} = require('../actions/notifications'); + +const ruelsSelector = createSelector([selectedRules, filterSelector, triggerLoadSel], (rules, filters, triggerLoad) => { + return { + selectedIds: rules.map(r => r.id), + filters, + triggerLoad +}; }); +const rulesGridEnhancer = compose( + connect( ruelsSelector, {onSelect: rulesSelected, onLoadError: error, setLoading, setFilters: setFilter}), + require('../components/manager/rulesmanager/rulesgrid/enhancers/rulesgrid')); + +const RulesGrid = rulesGridEnhancer(require('../components/manager/rulesmanager/rulesgrid/RulesGrid')); + +/** + * @name RulesGrid + * @memberof plugins + * @class + * @prop {boolean} cfg.virtualScroll default true. Activates virtualScroll. When false the grid uses normal pagination + * @prop {number} cfg.maxStoredPages default 5. In virtual Scroll mode determines the size of the loaded pages cache + * @prop {number} cfg.vsOverScan default 20. Number of rows to load above/below the visible slice of the grid + * @prop {number} cfg.scrollDebounce default 50. milliseconds of debounce interval between two scroll event + * @classdesc + * Rules-grid it's part of rules-manager page. It loads goefence's rules from configured geofence instance. + * It uses virtualScroll to manage rules loading. It allows to order geofence's rules by drag and drop. + * Rules can be filtered selecting values form columns' header. +*/ + +class RulesDataGrid extends React.Component { + static propTypes = { + enabled: PropTypes.bool + }; + static defaultProps = { + enabled: true + }; + render() { + return ({({width, height}) => + (
+ {!this.props.enabled && (
)} + +
) + } + ); + } +} +const RulesDataGridPlugin = connect( + createSelector( + isEditorActive, + editing => ({enabled: !editing}) + ), { + // setEditing, + // onMount: () => setEditorAvailable(true), + // onUnmount: () => setEditorAvailable(false) + } +)(RulesDataGrid); +module.exports = { + RulesDataGridPlugin, + reducers: {rulesmanager: require('../reducers/rulesmanager')} +}; diff --git a/web/client/plugins/RulesEditor.jsx b/web/client/plugins/RulesEditor.jsx new file mode 100644 index 0000000000..60cd6b371e --- /dev/null +++ b/web/client/plugins/RulesEditor.jsx @@ -0,0 +1,90 @@ +/* + * 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 {createSelector} = require('reselect'); +const {connect} = require('react-redux'); +const PropTypes = require('prop-types'); + +const { isEditorActive} = require('../selectors/rulesmanager'); + +const Editor = require('./manager/RulesEditor'); +const Toolbar = require('./manager/RulesToolbar'); + +/** + * @name RulesEditor + * @memberof plugins + * @class + * @classdesc + * Rules-editor it's part of rules-manager page. It allow a admin user to add, modify and delete geofence rules +*/ +class RulesEditorComponent extends React.Component { + static propTypes = { + id: PropTypes.string, + editing: PropTypes.bool, + limitDockHeight: PropTypes.bool, + fluid: PropTypes.bool, + zIndex: PropTypes.number, + dockSize: PropTypes.number, + position: PropTypes.string, + onMount: PropTypes.func, + onUnmount: PropTypes.func, + setEditing: PropTypes.func, + dimMode: PropTypes.string, + src: PropTypes.string, + style: PropTypes.object + }; + static defaultProps = { + id: "rules-editor", + editing: false, + dockSize: 500, + limitDockHeight: true, + zIndex: 10000, + 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}/>
+ : (
+ +
); + } +} + +const Plugin = connect( + createSelector( + isEditorActive, + editing => ({editing}) + ), { + // setEditing, + // onMount: () => setEditorAvailable(true), + // onUnmount: () => setEditorAvailable(false) + } +)(RulesEditorComponent); +module.exports = { + RulesEditorPlugin: Plugin, + reducers: { + rulesmanager: require('../reducers/rulesmanager') + }, + epics: require("../epics/rulesmanager") +}; diff --git a/web/client/plugins/RulesManagerFooter.jsx b/web/client/plugins/RulesManagerFooter.jsx new file mode 100644 index 0000000000..da43e9a6c1 --- /dev/null +++ b/web/client/plugins/RulesManagerFooter.jsx @@ -0,0 +1,47 @@ +/* + * 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 src = require("../product/plugins/attribution/geosolutions-brand.png"); +const {connect} = require('react-redux'); + +class RulesManagerFooter extends React.Component { + + static propTypes = { + loading: PropTypes.bool, + containerPosition: PropTypes.string + }; + + static defaultProps = { + loading: false, + containerPosition: "footer" + }; + + render() { + return ( +
+
+
+
+
+
+
+
+ +
+
+
+ ); + } +} + +module.exports = { + RulesManagerFooterPlugin: connect(({rulesmanager}) => ({loading: rulesmanager.loading}))(RulesManagerFooter), + reducers: {} +}; diff --git a/web/client/plugins/manager/EditorEnhancer.js b/web/client/plugins/manager/EditorEnhancer.js new file mode 100644 index 0000000000..6ad9924f4e --- /dev/null +++ b/web/client/plugins/manager/EditorEnhancer.js @@ -0,0 +1,31 @@ +const {compose, withStateHandlers} = require('recompose'); + + +module.exports = compose( + // defaultProps({dataStreamFactory}), + withStateHandlers(({activeRule: initRule}) => ({ + activeRule: initRule, + initRule, + activeEditor: "1" + }), { + 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) { + const {request, service, ...newActive} = activeRule; + return {activeRule: newActive}; + }else if (!value) { + const {[key]: omit, ...newActive} = activeRule; + return {activeRule: newActive}; + } + return { + activeRule: {...activeRule, [key]: value} + }; + }, + onNavChange: () => activeEditor => ({ + activeEditor + }) + }) +); diff --git a/web/client/plugins/manager/ManagerMenu.jsx b/web/client/plugins/manager/ManagerMenu.jsx index 8812c58b5e..142afd3426 100644 --- a/web/client/plugins/manager/ManagerMenu.jsx +++ b/web/client/plugins/manager/ManagerMenu.jsx @@ -52,6 +52,11 @@ class ManagerMenu extends React.Component { "msgId": "users.title", "glyph": "1-group-mod", "path": "/manager/usermanager" + }, + { + "msgId": "rulesmanager.menutitle", + "glyph": "lock", + "path": "/rules-manager" }], role: "", onItemClick: () => {}, diff --git a/web/client/plugins/manager/ModalDialog.jsx b/web/client/plugins/manager/ModalDialog.jsx new file mode 100644 index 0000000000..abc3c25b02 --- /dev/null +++ b/web/client/plugins/manager/ModalDialog.jsx @@ -0,0 +1,31 @@ +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); + +const Message = require('../../components/I18N/Message'); +const Portal = require('../../components/misc/Portal'); +const ResizableModal = require('../../components/misc/ResizableModal'); + +module.exports = ({title = "", showDialog = false, buttons = [], closeAction = () => {}, msg = "Missing message"}) => { + return ( + + } + size="xs" + show={showDialog} + onClose={closeAction} + buttons={buttons}> +
+
+ +
+
+
+
+ ); +}; diff --git a/web/client/plugins/manager/RulesEditor.jsx b/web/client/plugins/manager/RulesEditor.jsx new file mode 100644 index 0000000000..b351c2c182 --- /dev/null +++ b/web/client/plugins/manager/RulesEditor.jsx @@ -0,0 +1,101 @@ +/* + * 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 {connect} = require('react-redux'); +const {createSelector} = require("reselect"); +const {compose} = require('recompose'); +const enhancer = require("./EditorEnhancer"); +const {cleanEditing, saveRule} = require("../../actions/rulesmanager"); +const {activeRuleSelector} = require("../../selectors/rulesmanager"); + +const {isEqual} = require("lodash"); + +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 ModalDialog = require("./ModalDialog"); +const {isRuleValid} = require("../../utils/RulesGridUtils"); + +class RuleEditor extends React.Component { + static propTypes = { + initRule: PropTypes.object, + activeRule: PropTypes.object, + activeEditor: PropTypes.string, + onNavChange: PropTypes.func, + setOption: PropTypes.func, + onExit: PropTypes.func, + onSave: PropTypes.func, + onDelete: PropTypes.func + } + static defaultProps = { + activeEditor: "1", + onNavChange: () => {}, + setOption: () => {}, + onExit: () => {}, + onSave: () => {}, + onDelete: () => {} + } + render() { + const {activeRule, activeEditor, onNavChange, setOption, initRule} = this.props; + const {modalProps} = this.state || {}; + return ( + }> + + + ); + } + cancelEditing = () => { + const {activeRule, initRule, onExit} = this.props; + if (!isEqual(activeRule, initRule)) { + this.setState( () => ({modalProps: {title: "featuregrid.toolbar.saveChanges", + showDialog: true, buttons: [{ + text: , + bsStyle: 'primary', + onClick: this.cancel + }, + { + text: , + bsStyle: 'primary', + onClick: onExit + } + ], closeAction: this.cancel, msg: "map.details.sureToClose"}})); + } else { + onExit(); + } + } + cancel = () => { + this.setState( () => ({modalProps: {showDialog: false}})); + } + save = () => { + const {activeRule, onSave} = this.props; + if (isRuleValid(activeRule)) { + onSave(activeRule); + }else { + this.setState( () => ({modalProps: {title: "featuregrid.toolbar.saveChanges", + showDialog: true, buttons: [ + { + text: 'Ok', + bsStyle: 'primary', + onClick: this.cancel + } + ], closeAction: this.cancel, msg: "rulesmanager.invalidForm"}})); + } + } +} + +module.exports = compose( + connect(createSelector(activeRuleSelector, activeRule => ({activeRule})), { + onExit: cleanEditing, + onSave: saveRule + }), + enhancer)(RuleEditor); diff --git a/web/client/plugins/manager/RulesToolbar.jsx b/web/client/plugins/manager/RulesToolbar.jsx new file mode 100644 index 0000000000..7e4148d7b0 --- /dev/null +++ b/web/client/plugins/manager/RulesToolbar.jsx @@ -0,0 +1,142 @@ +import { withPropsOnChange } from "recompose"; + +/* + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require("react"); +const {compose, withProps, withStateHandlers} = require("recompose"); +const {connect} = require("react-redux"); +const { onEditRule, delRules} = require('../../actions/rulesmanager'); +const {rulesEditorToolbarSelector} = require('../../selectors/rulesmanager'); +const Toolbar = require('../../components/misc/toolbar/Toolbar'); +const Modal = require("./ModalDialog"); +const Message = require("../../components/I18N/Message"); + +const ToolbarWithModal = ({modalsProps, ...props}) => { + return ( +
+ + +
+ ); +}; + +const EditorToolbar = compose( + connect( + rulesEditorToolbarSelector, + { + deleteRules: delRules, + editOrCreate: onEditRule + } + ), + withStateHandlers(() => ({ + modal: "none" + }), { + cancelModal: () => () => ({ + modal: "none" + }), + showModal: () => (modal) => ({ + modal + }) + }), + withProps(({ + showAdd, showEdit, showModal, showInsertBefore, showInsertAfter, showDel, showCache, + editOrCreate = () => {} + }) => ({ + buttons: [{ + glyph: 'plus', + tooltipId: 'rulesmanager.tooltip.addT', + visible: showAdd, + onClick: editOrCreate.bind(null, 0, true) + }, { + glyph: 'pencil', + tooltipId: 'rulesmanager.tooltip.editT', + visible: showEdit, + onClick: editOrCreate.bind(null, 0, false) + }, { + glyph: 'add-row-before', + tooltipId: 'rulesmanager.tooltip.addBeT', + visible: showInsertBefore, + onClick: editOrCreate.bind(null, -1, true) + }, { + glyph: 'add-row-after', + tooltipId: 'rulesmanager.tooltip.addAfT', + visible: showInsertAfter, + onClick: editOrCreate.bind(null, 1, true) + }, { + glyph: 'trash', + tooltipId: 'rulesmanager.tooltip.deleteT', + visible: showDel, + onClick: () => { + showModal("delete"); + } + }, { + glyph: 'clear-brush', + tooltipId: 'rulesmanager.tooltip.cacheT', + visible: showCache, + onClick: () => { + showModal("cache"); + } + }] + })), + withPropsOnChange(["modal"], ({modal, cancelModal, deleteRules}) => { + switch (modal) { + case "delete": + return { + modalsProps: { + showDialog: true, + title: "rulesmanager.deltitle", + buttons: [{ + text: , + bsStyle: 'primary', + onClick: cancelModal + }, + { + text: , + bsStyle: 'primary', + onClick: () => { cancelModal(); deleteRules(); } + } + ], + closeAction: cancelModal, + msg: "rulesmanager.delmsg" + } + }; + case "cache": + return { + modalsProps: { + showDialog: true, + title: "rulesmanager.cachetitle", + buttons: [{ + text: , + bsStyle: 'primary', + onClick: cancelModal + }, + { + text: , + bsStyle: 'primary', + onClick: () => { cancelModal(); } + } + ], + closeAction: cancelModal, + msg: "rulesmanager.cachemsg" + } + }; + default: + return { + modalsProps: {showDialog: false, + title: "", + buttons: [], + closeAction: cancelModal, + msg: "" + } + }; + } + + }) +)( ToolbarWithModal); + +module.exports = EditorToolbar; diff --git a/web/client/product/appConfig.js b/web/client/product/appConfig.js index 892159d9f9..43098b35bf 100644 --- a/web/client/product/appConfig.js +++ b/web/client/product/appConfig.js @@ -35,11 +35,15 @@ module.exports = { name: "dashboard", path: "/dashboard", component: require('./pages/Dashboard') - }, { - name: "dashboard", - path: "/dashboard/:did", - component: require('./pages/Dashboard') - }], + }, { + name: "dashboard", + path: "/dashboard/:did", + component: require('./pages/Dashboard') + }, { + name: "rulesmanager", + path: "/rules-manager", + component: require('./pages/RulesManager') + }], initialState: { defaultState: { mousePosition: {enabled: false}, diff --git a/web/client/product/pages/RulesManager.jsx b/web/client/product/pages/RulesManager.jsx new file mode 100644 index 0000000000..ca8eb44df0 --- /dev/null +++ b/web/client/product/pages/RulesManager.jsx @@ -0,0 +1,131 @@ +const PropTypes = require('prop-types'); +/** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); +const {connect} = require('react-redux'); + +const url = require('url'); +const urlQuery = url.parse(window.location.href, true).query; + +const ConfigUtils = require('../../utils/ConfigUtils'); + +const {loadMapConfig} = require('../../actions/config'); +const {resetControls} = require('../../actions/controls'); + +const HolyGrail = require('../../containers/HolyGrail'); +/** + * @name RulesManagerPage + * @memberof pages + * @class + * @classdesc + * Rules Manager allow a user with admin permissions to easly manage geofence's rules. + * Configure geoFenceUrl and geoFenceGeoServerInstance params in localConfig.
+ * Add services configuration to overwrite the default values used by the service and the request + * selectors. See the page's plugins configuration in the following example.
+ * The app is available at http://localhos:8081/#/rules-manager. + * + * @example + * // localConfig configuration example + * "geoFenceUrl": "http://localhost:8081/", + * "geoFenceGeoServerInstance": { + * "url": "geoserver/", + * "id" : 1 + * } + * "initialState": { + * "defaultState": { + * "rulesmanager": { + * "services": { + * "WFS": [ + * "DescribeFeatureType", + * "GetCapabilities", + * "GetFeature", + * "GetFeatureWithLock", + * "LockFeature", + * "Transaction" + * ], + * "WMS": [ + * "DescribeLayer", + * "GetCapabilities", + * "GetFeatureInfo", + * "GetLegendGraphic", + * "GetMap", + * "GetStyles" + * ] + * } + * } + * } + * },.... + * "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" + * }}] + * } +*/ + +class RulesManagerPage extends React.Component { + static propTypes = { + name: PropTypes.string, + mode: PropTypes.string, + match: PropTypes.object, + loadMaps: PropTypes.func, + reset: PropTypes.func, + plugins: PropTypes.object + }; + + static defaultProps = { + name: "rulesmanager", + mode: 'desktop', + loadMaps: () => {}, + reset: () => {} + }; + + render() { + let plugins = ConfigUtils.getConfigProp("plugins") || {}; + let pagePlugins = { + "desktop": [], // TODO mesh page plugins with other plugins + "mobile": [] + }; + let pluginsConfig = { + "desktop": plugins[this.props.name] || [], // TODO mesh page plugins with other plugins + "mobile": plugins[this.props.name] || [] + }; + + return pluginsConfig.desktop.length > 0 && () ||
; + } +} + +module.exports = connect((state) => ({ + mode: urlQuery.mobile || state.browser && state.browser.mobile ? 'mobile' : 'desktop' +}), + { + loadMapConfig, + reset: resetControls + })(RulesManagerPage); diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index 89f5a858e8..98689a1f9b 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -84,8 +84,11 @@ module.exports = { WidgetsPlugin: require('../plugins/Widgets'), WidgetsBuilderPlugin: require('../plugins/WidgetsBuilder'), TOCItemsSettingsPlugin: require('../plugins/TOCItemsSettings'), + RulesDataGridPlugin: require('../plugins/RulesDataGrid'), + RulesManagerFooter: require('../plugins/RulesManagerFooter'), FeaturedMaps: require('../plugins/FeaturedMaps'), - NavMenu: require('./plugins/NavMenu') + NavMenu: require('./plugins/NavMenu'), + RulesEditorPlugin: require('../plugins/RulesEditor') }, requires: { ReactSwipe: require('react-swipeable-views').default, diff --git a/web/client/reducers/rulesmanager.js b/web/client/reducers/rulesmanager.js index dcfe84073c..1e3be061bf 100644 --- a/web/client/reducers/rulesmanager.js +++ b/web/client/reducers/rulesmanager.js @@ -9,15 +9,49 @@ const assign = require('object-assign'); const { RULES_SELECTED, RULES_LOADED, UPDATE_ACTIVE_RULE, - ACTION_ERROR, OPTIONS_LOADED, UPDATE_FILTERS_VALUES } = require('../actions/rulesmanager'); + ACTION_ERROR, OPTIONS_LOADED, UPDATE_FILTERS_VALUES, + LOADING, EDIT_RULE, SET_FILTER, CLEAN_EDITING, RULE_SAVED} = require('../actions/rulesmanager'); const _ = require('lodash'); +const defaultState = { + services: { + WFS: [ + "DescribeFeatureType", + "GetCapabilities", + "GetFeature", + "GetFeatureWithLock", + "LockFeature", + "Transaction" + ], + WMS: [ + "DescribeLayer", + "GetCapabilities", + "GetFeatureInfo", + "GetLegendGraphic", + "GetMap", + "GetStyles" + ] + }, + triggerLoad: 0 +}; -function rulesmanager(state = {}, action) { +const getPosition = ({targetPosition = {}}, priority) => { + switch (priority) { + case -1: + return targetPosition.offsetFromTop; + case +1: + return targetPosition.offsetFromTop + 1; + default: + return 0; + } +}; + +function rulesmanager(state = defaultState, action) { switch (action.type) { case RULES_SELECTED: { if (!action.merge) { return assign({}, state, { - selectedRules: action.rules + selectedRules: action.rules, + targetPosition: action.targetPosition }); } const newRules = action.rules || []; @@ -29,8 +63,7 @@ function rulesmanager(state = {}, action) { }); } return assign({}, state, { - selectedRules: _(existingRules).concat(newRules).uniq(rule => rule.id).value() - }); + selectedRules: _(existingRules).concat(newRules).uniq(rule => rule.id).value()}); } case RULES_LOADED: { return assign({}, state, { @@ -84,6 +117,29 @@ function rulesmanager(state = {}, action) { }) }); } + case LOADING: + return assign({}, state, {loading: action.loading}); + case SET_FILTER: { + const {key, value} = action; + if (value) { + return assign({}, state, {filters: {...state.filters, [key]: value}}); + } + const {[key]: omit, ...newFilters} = state.filters; + return assign({}, state, {filters: newFilters}); + } + case EDIT_RULE: { + const {createNew, targetPriority} = action; + if (createNew) { + return assign({}, state, {activeRule: {position: {value: getPosition(state, targetPriority), position: "offsetFromTop"}}}); + } + return assign({}, state, {activeRule: {...(state.selectedRules[0] || {}), position: {value: state.targetPosition.offsetFromTop, position: "offsetFromTop"}}}); + } + case RULE_SAVED: { + return assign({}, state, {triggerLoad: (state.triggerLoad || 0) + 1, activeRule: undefined, selectedRules: [], targetPosition: undefined }); + } + case CLEAN_EDITING: { + return assign({}, state, {activeRule: undefined}); + } default: return state; } diff --git a/web/client/selectors/rulesmanager.js b/web/client/selectors/rulesmanager.js index 5a6c3ed4d5..34f4c93be4 100644 --- a/web/client/selectors/rulesmanager.js +++ b/web/client/selectors/rulesmanager.js @@ -8,6 +8,7 @@ const assign = require('object-assign'); const _ = require('lodash'); +const {createSelector} = require('reselect'); const rulesSelector = (state) => { if (!state.rulesmanager || !state.rulesmanager.rules) { @@ -42,8 +43,36 @@ const optionsSelector = (state) => { options.layersCount = stateOptions.layersCount || 0; return options; }; +const EMPTY_FILTERS = {}; +const filterSelector = (state) => state.rulesmanager && state.rulesmanager.filters || EMPTY_FILTERS; +const selectedRules = (state) => state.rulesmanager && state.rulesmanager.selectedRules || []; +const activeRuleSelector = (state) => state.rulesmanager && state.rulesmanager.activeRule; +const servicesConfigSel = (state) => state.rulesmanager && state.rulesmanager.services; +const servicesSelector = createSelector(servicesConfigSel, (services) => ( services && Object.keys(services).map(service => ({value: service, label: service})) +)); +const targetPositionSelector = (state) => state.rulesmanager && state.rulesmanager.targetPosition || EMPTY_FILTERS; +const rulesEditorToolbarSelector = createSelector(selectedRules, targetPositionSelector, (sel, {offsetFromTop}) => { + return { + showAdd: sel.length === 0, + showEdit: sel.length === 1, + showInsertBefore: sel.length === 1 && offsetFromTop !== 0, + showInsertAfter: sel.length === 1, + showDel: sel.length > 0, + showCache: sel.length === 0 + }; +}); +const isEditorActive = state => state.rulesmanager && !!state.rulesmanager.activeRule; +const triggerLoadSel = state => state.rulesmanager && state.rulesmanager.triggerLoad; module.exports = { rulesSelector, - optionsSelector + optionsSelector, + selectedRules, + filterSelector, + servicesSelector, + rulesEditorToolbarSelector, + isEditorActive, + activeRuleSelector, + servicesConfigSel, + triggerLoadSel }; diff --git a/web/client/test-resources/geofence/rest/rules/count b/web/client/test-resources/geofence/rest/rules/count new file mode 100644 index 0000000000..9a037142aa --- /dev/null +++ b/web/client/test-resources/geofence/rest/rules/count @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/web/client/test-resources/geofence/rest/rules/rules.xml b/web/client/test-resources/geofence/rest/rules/rules.xml new file mode 100644 index 0000000000..510fc9888f --- /dev/null +++ b/web/client/test-resources/geofence/rest/rules/rules.xml @@ -0,0 +1,62 @@ + + +21 +25 + +1 +default-gs + +WFS +GETFEATURE + +tasmania + + +4 +26 + +1 +default-gs + +WMS +GETMAP +tiger +poi + + +25 +27 + +1 +default-gs + +WFS +GETFEATURE +tiger +poly_landmarks + + +23 +28 + +1 +default-gs + +WMS +GETFEATURE +tiger +giant_polygon + + +16 +33 + +1 +default-gs + +WMS +GETFEATURE +sf +streams + + \ No newline at end of file diff --git a/web/client/themes/default/less/rulesmanager.less b/web/client/themes/default/less/rulesmanager.less new file mode 100644 index 0000000000..8ae317fd1c --- /dev/null +++ b/web/client/themes/default/less/rulesmanager.less @@ -0,0 +1,340 @@ +.rules-manager{ + position: absolute; + .hide-locked-cell .react-grid-Cell--locked:focus { + z-index: 9; + } + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + + .ms-vertical-toolbar.rules-editor.re-toolbar{ + order: -1; + width: 52px; + height: 100%; + padding: 10px; + border-left: 1px solid #dddddd; + } + #rules-editor { + border-right: 1px solid #dddddd; + border-left: none; + padding: 10px 8px; + } + .rules-manager .ms-vertical-toolbar.rules-editor.re-toolbar + + .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; + } + .mapstore-footer .ms-circle-loader-md { + margin: 13px 18px; + } + .mapstore-footer .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; + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); + -webkit-animation: mapstore-circle-load8 1.1s infinite linear; + animation: mapstore-circle-load8 1.1s infinite linear; + border-radius: 50%; + width: 26px; + 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; + } + + #home-button { + float: right; + } + .shadow-soft-inset { + -webkit-box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.06), inset 0 4px 8px rgba(0, 0, 0, 0.12); + -moz-box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.06), inset 0 4px 8px rgba(0, 0, 0, 0.12); + box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.06), inset 0 4px 8px rgba(0, 0, 0, 0.12); + } + + + .ms-header { + height: @square-btn-size; + border-bottom: 1px solid @ms2-color-shade-lighter; + display: flex; + .btn-group { + margin: auto; + } + } + .react-grid-Main { + outline: none; + .react-grid-Grid { + border: none; + } + } + +.ms-rules-side { + width: 500px; + .shadow-soft-inset; + .ms-wizard { + position: absolute; + width: 500px; + + height: 100%; + } +} +.react-grid-Cell { + + &:focus { + outline: none; + } +} +.ms-grid-card { + color: @ms2-color-text; + background-color: @ms2-color-background; + .shadow-soft; + flex: 1; + width: 100%; + padding: 0 8px; + box-sizing: border-box; + overflow: hidden; + margin-top: 8px; + transition: 0.3s; + & > * { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + padding: 0px 0 2px 0; + } + &:hover { + cursor: pointer; + z-index: 10; + transform: scale(1.1); + .shadow-far; + } +} +.ms-row-select { + + box-sizing: border-box; + .react-grid-Cell { + font-weight: bold; + + background-color: darken(@ms2-color-background, 10%); + color: @ms2-color-primary; + &:focus { + outline: none; + } + } +} +.ms-card-success { + border-bottom: 4px solid @ms2-color-success; + +} +.ms-card-danger { + border-bottom: 4px solid @ms2-color-danger; +} +.ms-allow-cell { + color: @ms2-color-text-primary; + background-color: @ms2-color-success; + text-align: center; + padding: 4px; +} + +.ms-deny-cell { + color: @ms2-color-text-primary; + background-color: @ms2-color-danger; + text-align: center; + padding: 4px; +} +.ms-check-cell { + text-align: center; +} + +.ms-grab-cell { + span { + display: block; + width: 8px; + overflow: hidden; + margin: 0 auto; + } +} +.ms-allow-row { + .react-grid-Cell { + border-bottom: 2px solid @ms2-color-success; + } +} + +.ms-deny-row { + .react-grid-Cell { + border-bottom: 2px solid @ms2-color-danger; + } +} + +.floating-btn { + position: absolute; + bottom: @square-btn-size + @square-btn-size / 2; + right: @square-btn-size / 2; + .shadow-far; +} + +.ms-overlay { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + z-index: 10; +} + +.ms2-border-layout-content { + .ms-rule-editor { + &.container-fluid { + padding: 0 30px; + } + .mapstore-switch-panel { + padding-left: 10px; + padding-right: 10px; + width: 420px; + margin-left: auto; + margin-right: auto; + .shadow-soft; + &:first-child { + margin-top: 20px; + } + .ReactCodeMirror { + margin-bottom: 10px; + height: @square-btn-size * 3; + } + .row { + &:last-child { + margin-bottom: 10px; + } + } + } + .row { + margin-top: 15px; + display: flex; + .col-sm-4 { + + display: flex; + align-items: center; + * { + flex: 1; + } + pre { + margin: 0; + padding: 6px; + } + + } + .col-xs-12 { + &:first-child { + display: flex; + align-items: center; + * { + flex: 1; + } + } + + .rw-state-disabled { + opacity: 0.3; + } + } + } + .form-group { + margin-bottom: 0; + } + } +} + +.ms-panel-header-container { + .nav-tabs { + .disabled { + a { + color: fade(@ms2-color-text, 20%); + } + } + } + .ms-toolbar-container { + height: @square-btn-size; + width: 100%; + display: flex; + .btn-group { + margin: auto; + } + } +} + + .mapstore-side-card { + &.ms-no-select { + &:hover { + cursor: default; + transform: none; + .shadow; + } + } + } + + .ms-style-modal { + & > div { + & > span { + .btn-group { + margin-top: 15px; + } + &:first-child { + padding: 15px; + } + } + } + .ms2-border-layout-content { + padding: 0 15px; + & > div:last-child { + margin-bottom: 15px; + } + } + } + + .ms-add-style { + padding: 0 15px; + display: flex; + & > div:first-child { + flex: 1; + } + .glyphicon { + + &:hover { + + } + } + } + +} \ No newline at end of file diff --git a/web/client/themes/default/ms2-theme.less b/web/client/themes/default/ms2-theme.less index 2bd5222b35..3e0eecdd90 100644 --- a/web/client/themes/default/ms2-theme.less +++ b/web/client/themes/default/ms2-theme.less @@ -31,6 +31,7 @@ @import "./less/print.less"; @import "./less/query-panel.less"; @import "./less/react-data-grid.less"; +@import "./less/rulesmanager.less"; @import "./less/select.less"; @import "./less/settings.less"; @import "./less/sliders.less"; diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE index dbf0aca7a4..f5ac70e79c 100644 --- a/web/client/translations/data.de-DE +++ b/web/client/translations/data.de-DE @@ -1326,9 +1326,46 @@ "layerlabel": "Ebene" }, "rulesmanager": { + "placeholders": { + "filter": "Suche...", + "role": "Nach Rollen suchen", + "user": "Benutzer suchen", + "ip": "Wählen Sie IP", + "service": "Suchdienste", + "request": "Type to serach Requests", + "workspace": "Suchanfragen", + "layer": "Suche Ebenen", + "access": "Suchzugriff", + "ip": "###.###.###.###/##", + "priority": "Wählen Sie Priorität" + }, + "menutitle": "GeoFence-Regeln Verwalten", + "tooltip": { + "addT": "Fügen Sie eine regel hinzu", + "editT": "Bearbeiten Sie die ausgewählte regel", + "addBeT": "Fügen Sie eine neue Regel hinzu, bevor Sie ausgewählt werden", + "addAfT": "Fügen Sie nach der Auswahl eine neue Regel hinzu", + "deleteT": "Entferne ausgewählte Regeln", + "cacheT": "Cache leeren", + "save": "Aktuelle Regel speichern", + "close": "Beenden Sie die Regel erstellen" + }, + "navItems": { + "main": "Allgemeine Regel", + "style": "Stil", + "filter": "Filter", + "attribute": "Attribute Regel" + }, + "cachetitle": "Cache leeren", + "cachemsg": "Möchten Sie den GeoFence-Cache wirklich löschen?", + "deltitle": "Regel Löschen", + "delmsg": "Möchten Sie diese Regel wirklich löschen?", + "invalidForm": "Das Formular ist ungültig. Überprüfen Sie die Felderwerte", + "ip": "IP", "title": "Zugangs Regeln", "role": "Rolle", "user": "Benutzer", + "priority": "Priorität", "service": "Service", "request": "Anfrage", "workspace": "Workspace", @@ -1343,6 +1380,7 @@ "close": "Schließen", "previous": "vorheriger", "next": "nächster", + "errorTitle": "Geofence", "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 8ebf89311e..272ab91163 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -1327,9 +1327,46 @@ "layerlabel": "Layer" }, "rulesmanager": { + "placeholders": { + "filter": "Search...", + "role": "Type to search Roles", + "user": "Type to search Users", + "ip": "Select IP", + "service": "Type to search Services", + "request": "Type to search Requests", + "workspace": "Type to search Workspaces", + "layer": "Type to search Layers", + "access": "Type to search Access", + "ip": "###.###.###.###/##", + "priority": "Select Priority" + }, + "menutitle": "Manage GeoFence Rules", + "tooltip": { + "addT": "Add a rule", + "editT": "Edit selected rule", + "addBeT": "Add new rule before selected", + "addAfT": "Add new rule after selected", + "deleteT": "Remove selected rules", + "cacheT": "Clear cache", + "save": "Save current rule", + "close": "Exit from create rule" + }, + "navItems": { + "main": "General Rule", + "style": "Style", + "filter": "Filters", + "attribute": "Attributes Rule" + }, + "cachetitle": "Clear Cache", + "cachemsg": "Are you sure to clear the GeoFence cache?", + "deltitle": "Delete Rule", + "delmsg": "Do you really want to delete this rule?", + "invalidForm": "The form is invalid check fields values", + "ip": "IP", "title": "Access Rules", "role": "Role", "user": "User", + "priority": "Priority", "service": "Service", "request": "Request", "workspace": "Workspace", @@ -1344,6 +1381,7 @@ "close": "Close", "previous": "previous", "next": "next", + "errorTitle": "Geofence", "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 a123ccd2b1..890f3c9c26 100644 --- a/web/client/translations/data.es-ES +++ b/web/client/translations/data.es-ES @@ -1326,9 +1326,46 @@ "layerlabel": "Capa" }, "rulesmanager": { + "placeholders": { + "filter": "Buscar...", + "role": "Escriba para buscar Roles", + "user": "Escriba para buscar usuarios", + "ip": "Seleccionar IP", + "service": "Escriba para buscar Servicios", + "request": "Escriba para buscar Solicitudes", + "workspace": "Escriba para buscar espacios de trabajo", + "layer": "Escriba para buscar Capas", + "access": "Escriba para buscar Acceso", + "ip": "###.###.###.###/##", + "priority": "Seleccionar prioridad" + }, + "menutitle": "Administrar reglas de GeoFence", + "tooltip": { + "addT": "Agregar una regla", + "editT": "Editar la regla seleccionada", + "addBeT": "Agregar nueva regla antes de seleccionar", + "addAfT": "Agregar nueva regla después de seleccionar", + "deleteT": "Eliminar las reglas seleccionadas", + "cacheT": "Limpiar cache", + "save": "Salir de crear regla", + "close": "Exit from create rule" + }, + "navItems": { + "main": "Regla general", + "style": "Estilo", + "filter": "Filtros", + "attribute": "Regla de atributos" + }, + "cachetitle": "Limpiar Cache", + "cachemsg": "¿Estás seguro de limpiar el caché GeoFence?", + "deltitle": "Eliminar Regla", + "delmsg": "¿De verdad quieres eliminar esta regla?", + "invalidForm": "El formulario no es válido para verificar los valores de los campos", + "ip": "IP", "title": "Reglas de acceso", "role": "Rol", "user": "Ususario", + "priority": "Prioridad", "service": "Servicio", "request": "Peticion", "workspace": "Espacio de trabajo", @@ -1343,6 +1380,7 @@ "close": "Cerrar", "previous": "previo", "next": "próximo", + "errorTitle": "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 c46eebd3d7..1c596947d2 100644 --- a/web/client/translations/data.fr-FR +++ b/web/client/translations/data.fr-FR @@ -1326,9 +1326,46 @@ "layerlabel": "Couche" }, "rulesmanager": { + "placeholders": { + "filter": "Chercher...", + "role": "Tapez pour rechercher des rôles", + "user": "Tapez pour rechercher des utilisateurs", + "ip": "Sélectionnez IP", + "service": "Tapez pour rechercher des services", + "request": "Tapez pour rechercher des demandes", + "workspace": "Tapez pour rechercher des espaces de travail", + "layer": "Tapez pour rechercher des calques", + "access": "Tapez pour rechercher Access", + "ip": "###.###.###.###/##", + "priority": "Sélectionnez Priorité" + }, + "menutitle": "Gérer les règles de GeoFence", + "tooltip": { + "addT": "Ajouter une règle", + "editT": "Modifier la règle sélectionnée", + "addBeT": "Ajouter une nouvelle règle avant de sélectionner", + "addAfT": "Ajouter une nouvelle règle après la sélection", + "deleteT": "Supprimer les règles sélectionnées", + "cacheT": "Vider le cache", + "save": "Enregistrer la règle actuelle", + "close": "Quitter de la règle de création" + }, + "navItems": { + "main": "Règle générale", + "style": "Style", + "filter": "Filters", + "attribute": "Règle des attributs" + }, + "cachetitle": "Vider le Cache", + "cachemsg": "Etes-vous sûr d'effacer le cache de GeoFence?", + "deltitle": "Supprimer la règle", + "delmsg": "Voulez-vous vraiment supprimer cette règle?", + "invalidForm": "Le formulaire est invalide vérifier les valeurs des champs", + "ip": "IP", "title": "Règles d'accès", "role": "Rôle", "user": "Utilisateur", + "priority": "Priorité", "service": "Service", "request": "Requête", "workspace": "Espace de travail", @@ -1343,6 +1380,7 @@ "close": "Fermer", "previous": "précédent", "next": "prochain", + "errorTitle": "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.hr-HR b/web/client/translations/data.hr-HR index 70a2d0e6ca..3f36e58fb5 100644 --- a/web/client/translations/data.hr-HR +++ b/web/client/translations/data.hr-HR @@ -1279,6 +1279,10 @@ "layerlabel": "Sloj" }, "rulesmanager": { + "placeholders": { + "filter": "Traži..." + }, + "menutitle": "Upravljanje pravilima GeoFence", "title": "Pravila pristupa", "role": "Rola", "user": "Korisnik", @@ -1296,6 +1300,7 @@ "close": "Zatvori", "previous": "prethodno", "next": "slijedeće", + "errorTitle": "Geofence", "errorLoadingRoles": "Greška prilikom učitavanja rola.", "errorLoadingUsers": "Greška prilikom učitavanja korisnika.", "errorLoadingWorkspaces": "Greška prilikom učitavanja radnih okolina.", diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index 2c8f04d1ab..8cae34b019 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -1325,38 +1325,76 @@ "paneltitle": "Styler", "layerlabel": "Layer" }, - "rulesmanager": { - "title": "Regole Di Accesso", - "role": "Ruolo", - "user": "Utente", - "service": "Servizio", - "request": "Richiesta", - "workspace": "Workspace", - "layer": "Layer", - "filters": "Filtri", - "rules": "Regole", - "access": "Accesso", - "newModal": "Nuova Regola", - "editModal": "Modifica Regola", - "newButton": "Crea", - "editButton": "Salva", - "close": "Chiudi", - "previous": "precedente", - "next": "prossima", - "errorLoadingRoles": "Errore durante il caricamento dei ruoli", - "errorLoadingUsers": "Errore durante il caricamento degli utenti.", - "errorLoadingWorkspaces": "Errore durante il caricamento dei workspaces.", - "errorLoadingLayers": "Errore durante il caricamento dei layers.", - "errorLoadingRules": "Errore durante il caricamento delle regole.", - "errorMovingRules": "Errore durante il caricamento delle regole.", - "errorDeletingRules": "Errore durante la cancellazione della regola.", - "errorAddingRule": "Errore durante la creazione della regola.", - "errorUpdatingRule": "Errore durante l'aggiornamento della regola.", - "deleteModal": "Elimina Regola", - "selectedRulesDelete": "Vuoi eliminare le regole selezionate?", - "deleteButton": "Elimina", - "cancelButton": "Annulla" + "rulesmanager": { + "placeholders": { + "filter": "Ricerca...", + "role": "Cerca Gruppo", + "user": "Cerca Utenti", + "ip": "Seleziona IP", + "service": "Cerca Servizi", + "request": "Cerca Richieste", + "workspace": "Cerca Workspacs", + "layer": "Cerca Layers", + "access": "Cerca Permesso", + "ip": "###.###.###.###/##", + "priority": "Seleziona Priorità" + }, + "menutitle": "Configura GeoFence Rules", + "tooltip": { + "addT": "Crea regola", + "editT": "Modifica regola selezionata", + "addBeT": "Crea regola prima di quella selezionata", + "addAfT": "Crea regola dopo quella selezionata", + "deleteT": "Rimuovi regole selezionate", + "cacheT": "Pulisci la cache", + "save": "Salva regola corrente", + "close": "Esci da modifica regola" }, + "navItems": { + "main": "Regola Generica", + "style": "Stile", + "filter": "Filtri", + "attribute": "Attributi" + }, + "cachetitle": "Pulisci Cache", + "cachemsg": "Sei sicuro di voler rimuovere la cache di Geofance?", + "deltitle": "Rimuovi Regola", + "delmsg": "Sei sicuro di voler rimuovere la regola?", + "invalidForm": "I campi inseriti non sono validi, controlla la form", + "ip": "IP", + "title": "Regole Di Accesso", + "role": "Ruolo", + "user": "Utente", + "priority": "Priorità", + "service": "Servizio", + "request": "Richiesta", + "workspace": "Workspace", + "layer": "Layer", + "filters": "Filtri", + "rules": "Regole", + "access": "Accesso", + "newModal": "Nuova Regola", + "editModal": "Modifica Regola", + "newButton": "Crea", + "editButton": "Salva", + "close": "Chiudi", + "previous": "precedente", + "next": "prossima", + "errorTitle": "Geofence", + "errorLoadingRoles": "Errore durante il caricamento dei ruoli", + "errorLoadingUsers": "Errore durante il caricamento degli utenti.", + "errorLoadingWorkspaces": "Errore durante il caricamento dei workspaces.", + "errorLoadingLayers": "Errore durante il caricamento dei layers.", + "errorLoadingRules": "Errore durante il caricamento delle regole.", + "errorMovingRules": "Errore durante il caricamento delle regole.", + "errorDeletingRules": "Errore durante la cancellazione della regola.", + "errorAddingRule": "Errore durante la creazione della regola.", + "errorUpdatingRule": "Errore durante l'aggiornamento della regola.", + "deleteModal": "Elimina Regola", + "selectedRulesDelete": "Vuoi eliminare le regole selezionate?", + "deleteButton": "Elimina", + "cancelButton": "Annulla" + }, "tutorial": { "title": "Tutorial", "back": "Indietro", diff --git a/web/client/translations/data.nl-NL b/web/client/translations/data.nl-NL index e63013c242..e2a1f31fd0 100644 --- a/web/client/translations/data.nl-NL +++ b/web/client/translations/data.nl-NL @@ -1059,6 +1059,10 @@ "layerlabel": "Laag" }, "rulesmanager": { + "placeholders": { + "filter": "Zoeken..." + }, + "menutitle": "Beheer GeoFence Rules", "title": "Toegangsregels", "role": "Rol", "user": "Gebruiker", @@ -1076,6 +1080,7 @@ "close": "Sluiten", "previous": "vorige", "next": "volgende", + "errorTitle": "Geofence", "errorLoadingRoles": "Fout bij laden van de rollen.", "errorLoadingUsers": "Fout bij laden van de gebruikers.", "errorLoadingWorkspaces": "Fout bij laden van workspaces.", diff --git a/web/client/translations/data.zh-ZH b/web/client/translations/data.zh-ZH index 92e38e4999..62b6134f62 100644 --- a/web/client/translations/data.zh-ZH +++ b/web/client/translations/data.zh-ZH @@ -1122,6 +1122,10 @@ "layerlabel": "Layer" }, "rulesmanager": { + "placeholders": { + "filter": "Search..." + }, + "menutitle": "Manage GeoFence Rules", "title": "Access Rules", "role": "Role", "user": "User", @@ -1139,6 +1143,7 @@ "close": "Close", "previous": "previous", "next": "next", + "errorTitle": "Geofence", "errorLoadingRoles": "Error loading roles.", "errorLoadingUsers": "Error loading users.", "errorLoadingWorkspaces": "Error loading workspaces.", diff --git a/web/client/utils/RulesGridUtils.js b/web/client/utils/RulesGridUtils.js new file mode 100644 index 0000000000..5b70d37225 --- /dev/null +++ b/web/client/utils/RulesGridUtils.js @@ -0,0 +1,82 @@ + /** + * Copyright 2018, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + + +const EMPTY_ROW = {id: "empty_row", get: () => undefined}; +const getPageIdx = (i, size) => Math.floor(i / size); +const getRow = (i, pages = {}, size) => { + const pIdx = getPageIdx(i, size); + return pages[pIdx] && pages[pIdx][i - (pIdx * size)] || EMPTY_ROW; +}; + +/** + * Sort old pages by index distance from the average needed pages + * @param {numeric} avgIdx The average page index + * @param {array} [pages=[]] old pages indexes + * @param {numeric} firstIdx index of the first needed page + * @param {numeric} lastIdx index of the latest needed page + * @returns {array} Array of old page indexes ordered by distance from requested pages + */ +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, + getPagesToLoad: (startPage, endPage, pages) => { + let needPages = []; + for (let i = startPage; i <= endPage; i++) { + if (!pages[i]) { + needPages.push(i); + } + } + return needPages; + }, + /** + * updates the pages and the rules of the request to support virtual scroll. + * This is virtual scroll with support for + * @param {object} newPages An object with page index as key and an array of rules ad value + * @param {object} requestOptions contains startPage and endPage needed. + * @param {object} oldPages pages previously loaded + * @param {numeric} maxStoredPages. the max number of page to cache, . + * + */ + updatePages: (newPages = {}, {startPage, endPage, pages: oldPages}, maxStoredPages = 5) => { + const newPageLength = Object.keys(newPages).length; + const oldPageLength = Object.keys(oldPages).length; + // Cached page should be less than the max of maxStoredPages or the number of page needed to fill the visible are of the grid + const nSpaces = oldPageLength + newPageLength - Math.max(maxStoredPages, (endPage - startPage + 1)); + let tempPages = {...oldPages}; + if (nSpaces > 0) { + // remove farhest pages + const pages = Object.keys(oldPages); + // Remove the farthest page from last loaded pages + const averageIdx = startPage + endPage / 2; + const farthestElindexes = getIdxFarthestEl(averageIdx, pages, startPage, endPage); + for (let i = 0; i < nSpaces; i++) { + delete tempPages[farthestElindexes[i]]; + } + } + 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); + return {prev: rows[idx - 1], next: rows[idx + 1]}; + } +};