From e1cad7e826b97a1d04f9db733701ff4af276d844 Mon Sep 17 00:00:00 2001 From: allyoucanmap Date: Thu, 7 Oct 2021 10:20:23 +0200 Subject: [PATCH 1/4] replace get capabilities request with GeoServer rest api to get information about styles --- docs/user-guide/layer-settings.md | 2 +- .../epics/__tests__/styleeditor-test.js | 102 +++++++++++++ web/client/epics/styleeditor.js | 142 ++++++++++-------- 3 files changed, 186 insertions(+), 60 deletions(-) diff --git a/docs/user-guide/layer-settings.md b/docs/user-guide/layer-settings.md index 8129dc21be..93fc0d03ea 100644 --- a/docs/user-guide/layer-settings.md +++ b/docs/user-guide/layer-settings.md @@ -98,7 +98,7 @@ In this case the user is allowed to: * Delete an existing style !!!note - By the default [service security rules](https://docs.geoserver.org/stable/en/user/security/service.html#service-security) the GeoServer's REST APIs are available only for the GeoServer administrators, so a basic authentication form will appears in MapStore to enter the *Admin* credentials. Without Admin rights, the editing capabilities on styles are not available and only the list of available styles will appear to allow the user to select one of them to the layer. + By the default [service security rules](https://docs.geoserver.org/stable/en/user/security/service.html#service-security) the GeoServer's REST APIs are available only for the GeoServer administrators, so a basic authentication form will appears in MapStore to enter the *Admin* credentials. Without Admin rights, the editing capabilities on styles are not available and only the list of available styles will appear to allow the user to select one of them to the layer. The list of available styles for users without admin rights could show a misalignment in the title and description due to a different request to GeoServer that does not contain the metadata but only the information included in the style body. Take a look at the [User Integration with GeoServer](../developer-guide/integrations/users/geoserver.md) section of [Developer Guide](../developer-guide/index.md) in order to understand how to configure the way MapStore and GeoServer share users, groups and roles. If the users integration between GeoServer and MapStore is configured, the editing functionalities of the styles will be available according to the role of the authenticated user in MapStore in a more transparent way. diff --git a/web/client/epics/__tests__/styleeditor-test.js b/web/client/epics/__tests__/styleeditor-test.js index 447cad61f3..7a2c5ab3ea 100644 --- a/web/client/epics/__tests__/styleeditor-test.js +++ b/web/client/epics/__tests__/styleeditor-test.js @@ -23,6 +23,7 @@ import { import { REMOVE_ADDITIONAL_LAYER, + UPDATE_ADDITIONAL_LAYER, UPDATE_OPTIONS_BY_OWNER } from '../../actions/additionallayers'; @@ -1378,4 +1379,105 @@ describe('Test styleeditor epics, with mock axios', () => { }); + it('toggleStyleEditorEpic: test request via GeoServer rest api', (done) => { + + mockAxios.onGet(/\/manifest/).reply(() => { + return [ 200, { about: { resource: [{ '@name': 'gt-css-2.16' }]} }]; + }); + + mockAxios.onGet(/\/version/).reply(() => { + return [ 200, { about: { resource: [{ '@name': 'GeoServer', version: '2.16' }] } }]; + }); + + mockAxios.onGet(/\/fonts/).reply(() => { + return [ 200, { fonts: ['Arial'] }]; + }); + + mockAxios.onGet(/\/rest\/layers/).reply(() => { + return [ 200, { layer: { + defaultStyle: { + name: 'wrksp:style_01' + }, + styles: { + style: [{ + name: 'wrksp:style_01' + }, { + name: 'wrksp:style_02' + }] + } + }}]; + }); + + mockAxios.onGet(/\/layerWorkspace/).reply(() => { + return [ 200, '']; + }); + + const state = { + layers: { + flat: [ + { + id: 'layerId', + name: 'layerWorkspace:layerName', + url: 'protocol://style-editor/geoserver/' + } + ], + selected: [ + 'layerId' + ], + settings: { + options: { + opacity: 1 + } + } + } + }; + const NUMBER_OF_ACTIONS = 5; + + const results = (actions) => { + try { + const [ + loadingStyleAction, + resetStyle, + initStyleServiceAction, + updateAdditionalLayerAction, + updateSettingsParamsAction + ] = actions; + + expect(resetStyle.type).toBe(RESET_STYLE_EDITOR); + expect(loadingStyleAction.type).toBe(LOADING_STYLE); + expect(initStyleServiceAction.type).toBe(INIT_STYLE_SERVICE); + expect(initStyleServiceAction.service).toEqual({ + baseUrl: 'protocol://style-editor/geoserver/', + version: '2.16', + formats: [ 'css', 'sld' ], + availableUrls: [], + fonts: ['Arial'], + classificationMethods: { + vector: [ 'equalInterval', 'quantile', 'jenks' ], + raster: [ 'equalInterval', 'quantile', 'jenks' ] + } + }); + expect(updateAdditionalLayerAction.type).toBe(UPDATE_ADDITIONAL_LAYER); + expect(updateSettingsParamsAction.type).toBe(UPDATE_SETTINGS_PARAMS); + expect(updateSettingsParamsAction.newParams).toEqual({ + availableStyles: [ + { name: 'wrksp:style_01' }, + { name: 'wrksp:style_02' } + ] + }); + } catch (e) { + done(e); + } + done(); + }; + + testEpic( + toggleStyleEditorEpic, + NUMBER_OF_ACTIONS, + toggleStyleEditor(undefined, true), + results, + state); + + }); + }); diff --git a/web/client/epics/styleeditor.js b/web/client/epics/styleeditor.js index 05e019584c..ac4724731a 100644 --- a/web/client/epics/styleeditor.js +++ b/web/client/epics/styleeditor.js @@ -8,7 +8,7 @@ import Rx from 'rxjs'; -import { get, head, isArray, template } from 'lodash'; +import { get, head, isArray, template, uniqBy } from 'lodash'; import { success, error } from '../actions/notifications'; import { UPDATE_NODE, updateNode, updateSettingsParams } from '../actions/layers'; import { updateAdditionalLayer, removeAdditionalLayer, updateOptionsByOwner } from '../actions/additionallayers'; @@ -174,6 +174,39 @@ const updateLayerSettingsObservable = (action$, store, filter = () => true, star .takeUntil(action$.ofType(LOADED_STYLE)) ); + +function getAvailableStylesFromLayerCapabilities(layer, reset) { + if (!reset && layer.availableStyles) { + return Rx.Observable.of( + updateSettingsParams({ availableStyles: layer.availableStyles }), + loadedStyle() + ); + } + return getLayerCapabilities(layer) + .switchMap((capabilities) => { + const layerCapabilities = formatCapabitiliesOptions(capabilities); + if (!layerCapabilities.availableStyles) { + return Rx.Observable.of( + errorStyle('availableStyles', { status: 401 }), + loadedStyle() + ); + } + + return Rx.Observable.of( + updateSettingsParams({ availableStyles: layerCapabilities.availableStyles }), + updateNode(layer.id, 'layer', { ...layerCapabilities }), + loadedStyle() + ); + + }) + .catch((err) => { + const errorType = err.message.indexOf("could not be unmarshalled") !== -1 ? "parsingCapabilities" : "global"; + return Rx.Observable.of(errorStyle(errorType, err), loadedStyle()); + }) + .startWith(loadingStyle('global')); +} + + /** * Epics for Style Editor * @name epics.styleeditor @@ -206,73 +239,65 @@ export const toggleStyleEditorEpic = (action$, store) => const geoserverName = findGeoServerName(layer); if (!geoserverName) { - if (layer.availableStyles) { - return Rx.Observable.of( - updateSettingsParams({ availableStyles: layer.availableStyles }), - loadedStyle() - ); - } - return getLayerCapabilities(layer) - .switchMap((capabilities) => { - const layerCapabilities = formatCapabitiliesOptions(capabilities); - if (!layerCapabilities.availableStyles) { - return Rx.Observable.of( - errorStyle('availableStyles', { status: 401 }), - loadedStyle() - ); - } - - return Rx.Observable.of( - updateSettingsParams({ availableStyles: layerCapabilities.availableStyles }), - updateNode(layer.id, 'layer', { ...layerCapabilities }), - loadedStyle() - ); - - }).startWith(loadingStyle('global')); + return getAvailableStylesFromLayerCapabilities(layer); } const layerUrl = layer.url.split(geoserverName); const baseUrl = `${layerUrl[0]}${geoserverName}`; const lastStyleService = styleServiceSelector(state); - return Rx.Observable - .defer(() => updateStyleService({ + return Rx.Observable.concat( + Rx.Observable.of( + loadingStyle('global'), + resetStyleEditor() + ), + Rx.Observable.defer(() => updateStyleService({ baseUrl, styleService: lastStyleService })) - .switchMap((styleService) => { - const initialAction = [ initStyleService(styleService) ]; - return getLayerCapabilities(layer) - .switchMap((capabilities) => { - const layerCapabilities = formatCapabitiliesOptions(capabilities); - if (!layerCapabilities.availableStyles) { - return Rx.Observable.of( - errorStyle('availableStyles', { status: 401 }), - loadedStyle() - ); - } - const setAdditionalLayers = (availableStyles = []) => - Rx.Observable.of( - updateAdditionalLayer(layer.id, STYLE_OWNER_NAME, 'override', {}), - updateSettingsParams({ availableStyles }), - updateNode(layer.id, 'layer', {...layerCapabilities, availableStyles}), - loadedStyle() - ); - return Rx.Observable.defer(() => - StylesAPI.getStylesInfo({ - baseUrl, - styles: layerCapabilities && layerCapabilities.availableStyles || [] - }) + .switchMap((styleService) => { + return Rx.Observable.concat( + Rx.Observable.of(initStyleService(styleService)), + Rx.Observable.defer(() => + // use rest API to get the correct name and workspace of the styles + LayersAPI.getLayer(baseUrl + 'rest/', layer.name) ) - .switchMap(availableStyles => setAdditionalLayers(availableStyles)); - }) - .startWith(...initialAction) - .catch((err) => { - const errorType = err.message.indexOf("could not be unmarshalled") !== -1 ? "parsingCapabilities" : "global"; - return Rx.Observable.of(errorStyle(errorType, err), loadedStyle()); - }); - }) - .startWith(loadingStyle('global'), resetStyleEditor()); + .switchMap((layerConfig) => { + const stylesConfig = layerConfig?.styles?.style || []; + const layerConfigAvailableStyles = uniqBy([ + layerConfig.defaultStyle, + ...stylesConfig + ], 'name').filter(({ name } = {}) => name); + + if (layerConfigAvailableStyles.length === 0) { + return Rx.Observable.of( + errorStyle('availableStyles', { status: 401 }), + loadedStyle() + ); + } + + return Rx.Observable.defer(() => + StylesAPI.getStylesInfo({ + baseUrl, + styles: layerConfigAvailableStyles + }) + ) + .switchMap(availableStyles => { + return Rx.Observable.of( + updateAdditionalLayer(layer.id, STYLE_OWNER_NAME, 'override', {}), + updateSettingsParams({ availableStyles }), + updateNode(layer.id, 'layer', { availableStyles }), + loadedStyle() + ); + }); + }) + .catch(() => { + // fallback to get capabilities to get list of styles + return getAvailableStylesFromLayerCapabilities(layer, true); + }) + ); + }) + ); }); /** * Gets every `UPDATE_STATUS` event. @@ -711,4 +736,3 @@ export default { deleteStyleEpic, setDefaultStyleEpic }; - From 3f1b9d196c07db48d5a7561edce051d5c3056b90 Mon Sep 17 00:00:00 2001 From: allyoucanmap Date: Mon, 11 Oct 2021 10:44:19 +0200 Subject: [PATCH 2/4] get missing styles information from get capabilities --- docs/user-guide/layer-settings.md | 2 +- web/client/api/geoserver/Styles.js | 4 +- web/client/configs/localConfig.json | 4 +- .../epics/__tests__/styleeditor-test.js | 20 +++++----- web/client/epics/styleeditor.js | 39 +++++++++++++++---- 5 files changed, 49 insertions(+), 20 deletions(-) diff --git a/docs/user-guide/layer-settings.md b/docs/user-guide/layer-settings.md index 93fc0d03ea..8129dc21be 100644 --- a/docs/user-guide/layer-settings.md +++ b/docs/user-guide/layer-settings.md @@ -98,7 +98,7 @@ In this case the user is allowed to: * Delete an existing style !!!note - By the default [service security rules](https://docs.geoserver.org/stable/en/user/security/service.html#service-security) the GeoServer's REST APIs are available only for the GeoServer administrators, so a basic authentication form will appears in MapStore to enter the *Admin* credentials. Without Admin rights, the editing capabilities on styles are not available and only the list of available styles will appear to allow the user to select one of them to the layer. The list of available styles for users without admin rights could show a misalignment in the title and description due to a different request to GeoServer that does not contain the metadata but only the information included in the style body. + By the default [service security rules](https://docs.geoserver.org/stable/en/user/security/service.html#service-security) the GeoServer's REST APIs are available only for the GeoServer administrators, so a basic authentication form will appears in MapStore to enter the *Admin* credentials. Without Admin rights, the editing capabilities on styles are not available and only the list of available styles will appear to allow the user to select one of them to the layer. Take a look at the [User Integration with GeoServer](../developer-guide/integrations/users/geoserver.md) section of [Developer Guide](../developer-guide/index.md) in order to understand how to configure the way MapStore and GeoServer share users, groups and roles. If the users integration between GeoServer and MapStore is configured, the editing functionalities of the styles will be available according to the role of the authenticated user in MapStore in a more transparent way. diff --git a/web/client/api/geoserver/Styles.js b/web/client/api/geoserver/Styles.js index 1db3afb5cb..9b914b5a35 100644 --- a/web/client/api/geoserver/Styles.js +++ b/web/client/api/geoserver/Styles.js @@ -223,8 +223,8 @@ export const getStylesInfo = ({baseUrl: geoserverBaseUrl, styles = []}) => { if (!styles || styles.length === 0) { resolve([]); } else { - styles.forEach(({name}, idx) => - axios.get(getStyleBaseUrl({...getNameParts(name), geoserverBaseUrl})) + styles.forEach(({ name, href }, idx) => + axios.get(href || getStyleBaseUrl({...getNameParts(name), geoserverBaseUrl})) .then(({data}) => { responses[idx] = assign({}, styles[idx], data && data.style && { ...data.style, diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index 359f6e2a40..055f5bfc30 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -8,7 +8,9 @@ "https://nominatim.openstreetmap.org", "http://cloudsdi.geo-solutions.it", "https://gs-stable.geo-solutions.it/geoserver", - "https://gs-stable.geo-solutions.it:443/geoserver" + "https://gs-stable.geo-solutions.it:443/geoserver", + "http://gs-stable.geo-solutions.it/geoserver", + "http://gs-stable.geo-solutions.it:443/geoserver" ] }, "geoStoreUrl": "rest/geostore/", diff --git a/web/client/epics/__tests__/styleeditor-test.js b/web/client/epics/__tests__/styleeditor-test.js index 7a2c5ab3ea..3ec3587bba 100644 --- a/web/client/epics/__tests__/styleeditor-test.js +++ b/web/client/epics/__tests__/styleeditor-test.js @@ -1396,22 +1396,24 @@ describe('Test styleeditor epics, with mock axios', () => { mockAxios.onGet(/\/rest\/layers/).reply(() => { return [ 200, { layer: { defaultStyle: { - name: 'wrksp:style_01' + name: 'layerWorkspace:style_01', + workspace: 'layerWorkspace' }, styles: { style: [{ - name: 'wrksp:style_01' + name: 'layerWorkspace:style_01', + workspace: 'layerWorkspace' }, { - name: 'wrksp:style_02' + name: 'layerWorkspace:style_02', + workspace: 'layerWorkspace' + }, { + name: 'notLayerWorkspace:style_03', + workspace: 'notLayerWorkspace' }] } }}]; }); - mockAxios.onGet(/\/layerWorkspace/).reply(() => { - return [ 200, '']; - }); - const state = { layers: { flat: [ @@ -1461,8 +1463,8 @@ describe('Test styleeditor epics, with mock axios', () => { expect(updateSettingsParamsAction.type).toBe(UPDATE_SETTINGS_PARAMS); expect(updateSettingsParamsAction.newParams).toEqual({ availableStyles: [ - { name: 'wrksp:style_01' }, - { name: 'wrksp:style_02' } + { name: 'layerWorkspace:style_01', workspace: 'layerWorkspace' }, + { name: 'layerWorkspace:style_02', workspace: 'layerWorkspace' } ] }); } catch (e) { diff --git a/web/client/epics/styleeditor.js b/web/client/epics/styleeditor.js index ac4724731a..60d8e4f2a8 100644 --- a/web/client/epics/styleeditor.js +++ b/web/client/epics/styleeditor.js @@ -263,12 +263,14 @@ export const toggleStyleEditorEpic = (action$, store) => LayersAPI.getLayer(baseUrl + 'rest/', layer.name) ) .switchMap((layerConfig) => { + const { workspace: layerWorkspace } = getNameParts(layer.name); const stylesConfig = layerConfig?.styles?.style || []; const layerConfigAvailableStyles = uniqBy([ layerConfig.defaultStyle, ...stylesConfig - ], 'name').filter(({ name } = {}) => name); - + ], 'name') + // show only styles included in the same workspace + .filter((style) => style?.name && style?.workspace === layerWorkspace); if (layerConfigAvailableStyles.length === 0) { return Rx.Observable.of( errorStyle('availableStyles', { status: 401 }), @@ -277,12 +279,35 @@ export const toggleStyleEditorEpic = (action$, store) => } return Rx.Observable.defer(() => - StylesAPI.getStylesInfo({ - baseUrl, - styles: layerConfigAvailableStyles - }) + Promise.all([ + StylesAPI.getStylesInfo({ + baseUrl, + styles: layerConfigAvailableStyles + }), + getLayerCapabilities(layer) + .toPromise() + .then(cap => cap) + .catch(() => null) + ]) ) - .switchMap(availableStyles => { + .switchMap(([availableStylesRest, capabilities]) => { + const layerCapabilities = capabilities && formatCapabitiliesOptions(capabilities); + const availableStylesCap = (layerCapabilities?.availableStyles || []) + .map((style) => ({ ...style, ...getNameParts(style.name) })) + .filter(({ name } = {}) => name); + + // get title information from capabilities + const availableStyles = availableStylesCap.length > 0 + ? availableStylesRest.map(restStyle => { + const parts = getNameParts(restStyle.name); + const { name, workspace, ...capStyle} = availableStylesCap.find(style => style.name === parts.name) || {}; + if (capStyle) { + return { ...capStyle, ...restStyle }; + } + return restStyle; + }) + : availableStylesRest; + return Rx.Observable.of( updateAdditionalLayer(layer.id, STYLE_OWNER_NAME, 'override', {}), updateSettingsParams({ availableStyles }), From 91a1d4a7283384232be555ad497468049b1352ee Mon Sep 17 00:00:00 2001 From: allyoucanmap Date: Mon, 11 Oct 2021 16:19:07 +0200 Subject: [PATCH 3/4] store the current default style name --- web/client/components/styleeditor/StyleList.jsx | 2 +- web/client/components/styleeditor/__tests__/StyleList-test.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/client/components/styleeditor/StyleList.jsx b/web/client/components/styleeditor/StyleList.jsx index a97b101d19..4b5bc07716 100644 --- a/web/client/components/styleeditor/StyleList.jsx +++ b/web/client/components/styleeditor/StyleList.jsx @@ -78,7 +78,7 @@ const StyleList = ({ }> onSelect({ style: defaultStyle === name ? '' : name }, true)} + onItemClick={({ name }) => onSelect({ style: name }, true)} items={availableStyles .filter(({name = '', title = '', _abstract = '', metadata = {} }) => !filterText || filterText && ( diff --git a/web/client/components/styleeditor/__tests__/StyleList-test.jsx b/web/client/components/styleeditor/__tests__/StyleList-test.jsx index 784f5537a5..a0882f2bc3 100644 --- a/web/client/components/styleeditor/__tests__/StyleList-test.jsx +++ b/web/client/components/styleeditor/__tests__/StyleList-test.jsx @@ -159,7 +159,7 @@ describe('test StyleList module component', () => { TestUtils.Simulate.click(cards[0]); - expect(spyOnSelect).toHaveBeenCalledWith({style: ''}, true); + expect(spyOnSelect).toHaveBeenCalledWith({ style: 'point' }, true); }); From e2a402dc0f2e97ebc1a18ec11ce90cba0ec3ea27 Mon Sep 17 00:00:00 2001 From: allyoucanmap Date: Tue, 12 Oct 2021 12:41:49 +0200 Subject: [PATCH 4/4] remove restriction on workspace for editing --- web/client/epics/__tests__/styleeditor-test.js | 7 ++++--- web/client/epics/styleeditor.js | 5 +---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/web/client/epics/__tests__/styleeditor-test.js b/web/client/epics/__tests__/styleeditor-test.js index 3ec3587bba..cc69895b83 100644 --- a/web/client/epics/__tests__/styleeditor-test.js +++ b/web/client/epics/__tests__/styleeditor-test.js @@ -1407,8 +1407,8 @@ describe('Test styleeditor epics, with mock axios', () => { name: 'layerWorkspace:style_02', workspace: 'layerWorkspace' }, { - name: 'notLayerWorkspace:style_03', - workspace: 'notLayerWorkspace' + name: 'style_03', + workspace: '' }] } }}]; @@ -1464,7 +1464,8 @@ describe('Test styleeditor epics, with mock axios', () => { expect(updateSettingsParamsAction.newParams).toEqual({ availableStyles: [ { name: 'layerWorkspace:style_01', workspace: 'layerWorkspace' }, - { name: 'layerWorkspace:style_02', workspace: 'layerWorkspace' } + { name: 'layerWorkspace:style_02', workspace: 'layerWorkspace' }, + { name: 'style_03', workspace: '' } ] }); } catch (e) { diff --git a/web/client/epics/styleeditor.js b/web/client/epics/styleeditor.js index 60d8e4f2a8..717407e24a 100644 --- a/web/client/epics/styleeditor.js +++ b/web/client/epics/styleeditor.js @@ -263,14 +263,11 @@ export const toggleStyleEditorEpic = (action$, store) => LayersAPI.getLayer(baseUrl + 'rest/', layer.name) ) .switchMap((layerConfig) => { - const { workspace: layerWorkspace } = getNameParts(layer.name); const stylesConfig = layerConfig?.styles?.style || []; const layerConfigAvailableStyles = uniqBy([ layerConfig.defaultStyle, ...stylesConfig - ], 'name') - // show only styles included in the same workspace - .filter((style) => style?.name && style?.workspace === layerWorkspace); + ], 'name'); if (layerConfigAvailableStyles.length === 0) { return Rx.Observable.of( errorStyle('availableStyles', { status: 401 }),