diff --git a/docs/developer-guide/expressions.md b/docs/developer-guide/expressions.md index ce63b168af4..52270803c9f 100644 --- a/docs/developer-guide/expressions.md +++ b/docs/developer-guide/expressions.md @@ -33,11 +33,11 @@ The expression syntax can potentially be applied to every configuration. However ## State Access and `monitorState` -The `state('name')` function allows you to access a "slice" of the Redux store. However, for performance and security reasons, only specific parts of the state are exposed. These are defined in the `monitorState` section of your `localConfig.json`. +The `state('name')` function allows you to access a "slice" of the Redux store. However, for performance and security reasons, only specific parts of the state are exposed. ### Default Monitored States -The following aliases are usually available out-of-the-box in the standard version of MapStore: +The following aliases are available out-of-the-box in the standard version of MapStore: | Alias | Path in Redux Store | Purpose | | --- | --- | --- | @@ -48,12 +48,13 @@ The following aliases are usually available out-of-the-box in the standard versi | `userrole` | `security.user.role` | Current user's role | | `printEnabled` | `print.capabilities` | Printing availability | | `resourceCanEdit` | `resources.initialSelectedResource.canEdit` | Permission on current resource | +| `usergroups` | (internal selector) | User's groups (`groupName`) array (only enabled ones) | -They are configured by default in the `monitoredState` section of the standard `localConfig.json`, allowing them to be customized. +They are available by default and they do not require any additional configuration. You can use them directly in your expressions, for example: `{state('userrole') == 'admin'}`. ### Customizing Monitored State -You can extend this list in `localConfig.json` to expose any part of the MapStore state to your expressions: +You can extend the set of monitored states by adding entries in `localConfig.json` `monitorState` property. Each entry should specify a `name` (the alias used in expressions) and a `path` (the path in the Redux store). For example: ```json "monitorState": [ @@ -64,6 +65,8 @@ You can extend this list in `localConfig.json` to expose any part of the MapStor *Usage:* `{state('myCustomValue') == 'active'}` +Redefining an existing alias (e.g., `userrole`) will override the default path, allowing you to point it to a different part of the state if needed. + ## Core Syntax & Operators The [engine](https://www.npmjs.com/package/filtrex) supports standard mathematical operations, string concatenation, and logic. diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index 258b2ff5c66..954b1111b38 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -20,6 +20,16 @@ This is a list of things to check if you want to update from a previous version - Optionally check also accessory files like `.eslinrc`, if you want to keep aligned with lint standards. - Follow the instructions below, in order, from your version to the one you want to update to. +## Migration from 2026.01.00 to 2026.02.00 + +### Monitored state available by default + +Several monitored state entries have been added to the default configuration of MapStore and they are now available by default without the need to add them in the `monitorState` section of `localConfig.json`. + +The entries you have configured will still work by overriding the default ones, anyway, in order to reduce the configuration, you should remove the entries if they are already available by default. + +The entries that you can remove because are available by default are documented in the [State Access and monitorState](./expressions.md#state-access-and-monitorstate) guide. + ## Migration from 2025.02.02 to 2026.01.00 ### Database update diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index 0cbac63e5f1..6d3658eee4e 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -66,22 +66,7 @@ } } ], - "monitorState": [ - { "name": "router", "path": "router.location.pathname" }, - { "name": "browser", "path": "browser" }, - { "name": "geostorymode", "path": "geostory.mode" }, - { "name": "featuregridmode", "path": "featuregrid.mode" }, - { "name": "userrole", "path": "security.user.role" }, - { "name": "printEnabled", "path": "print.capabilities" }, - { - "name": "resourceCanEdit", - "path": "resources.initialSelectedResource.canEdit" - }, - { - "name": "resourceDetails", - "path": "resources.initialSelectedResource.attributes.details" - } - ], + "monitorState": [], "userSessions": { "enabled": true }, diff --git a/web/client/selectors/__tests__/context-test.js b/web/client/selectors/__tests__/context-test.js index f7b704d3586..4fe62c38974 100644 --- a/web/client/selectors/__tests__/context-test.js +++ b/web/client/selectors/__tests__/context-test.js @@ -10,7 +10,6 @@ import ConfigUtils from '../../utils/ConfigUtils'; import { currentContextSelector, - contextMonitoredStateSelector, isLoadingSelector, pluginsSelector, resourceSelector, @@ -31,9 +30,7 @@ describe('context selectors', () => { it('currentContextSelector', () => { expect(currentContextSelector(stateMocker(setContext(CONTEXT_DATA)))).toEqual(CONTEXT_DATA); }); - it('contextMonitoredStateSelector', () => { - expect(contextMonitoredStateSelector(stateMocker())).toBe('{}'); - }); + it('isLoadingSelector', () => { expect(isLoadingSelector(stateMocker(loading(true)))).toBe(true); }); diff --git a/web/client/utils/PluginsUtils.js b/web/client/utils/PluginsUtils.js index 31e18181e06..8fec01c2d27 100644 --- a/web/client/utils/PluginsUtils.js +++ b/web/client/utils/PluginsUtils.js @@ -16,6 +16,7 @@ import {combineReducers as originalCombineReducers} from 'redux'; import {wrapEpics} from "./EpicsUtils"; import { randomInt } from './RandomUtils'; import { pluginUtilsExpressionEvaluation } from './ExpressionUtils'; +import { userGroupsEnabledSelector } from '../selectors/security'; /** * Loads a script inside the current page. @@ -76,7 +77,19 @@ const dynamicFederation = (scope, module) => { }) }; -const defaultMonitoredState = [{name: "mapType", path: 'maptype.mapType'}, {name: "user", path: 'security.user'}]; +const defaultMonitoredState = [ + { name: "browser", path: "browser" }, + { name: "router", path: "router.location.pathname" }, + { name: "user", path: 'security.user'}, + { name: "userrole", path: "security.user.role" }, + { name: "usergroups", selector: userGroupsEnabledSelector }, + { name: "resourceCanEdit", path: "resources.initialSelectedResource.canEdit" }, + { name: "resourceDetails", path: "resources.initialSelectedResource.attributes.details" }, + { name: "printEnabled", path: "print.capabilities" }, + { name: "geostorymode", path: "geostory.mode" }, + { name: "featuregridmode", path: "featuregrid.mode" }, + {name: "mapType", path: 'maptype.mapType'} +]; export const getFromPlugins = curry((selector, plugins) => Object.keys(plugins).map((name) => plugins[name][selector]) .reduce((previous, current) => ({ ...previous, ...current }), {})); @@ -128,13 +141,15 @@ export const combineEpics = (plugins, epics = {}, epicWrapper) => { */ export const filterState = memoize((state, monitor) => { return monitor.reduce((previous, current) => { + const value = current.selector ? current.selector(state) : get(state, current.path); return Object.assign(previous, { - [current.name]: get(state, current.path) + [current.name]: value }); }, {}); }, (state, monitor) => { return monitor.reduce((previous, current) => { - return previous + JSON.stringify(get(state, current.path)); + const value = current.selector ? current.selector(state) : get(state, current.path); + return previous + JSON.stringify(value); }, ''); }); diff --git a/web/client/utils/__tests__/PluginsUtils-test.js b/web/client/utils/__tests__/PluginsUtils-test.js index e05763e3f82..1da236b2a49 100644 --- a/web/client/utils/__tests__/PluginsUtils-test.js +++ b/web/client/utils/__tests__/PluginsUtils-test.js @@ -116,6 +116,33 @@ describe('PluginsUtils', () => { it('getMonitoredState', () => { expect(PluginsUtils.getMonitoredState({maptype: {mapType: "leaflet"}}).mapType).toBe("leaflet"); }); + it('getMonitoredState includes usergroups from security state', () => { + const state = { + security: { + user: { + groups: { + group: [ + {groupName: "everyone", enabled: true}, + {groupName: "editors", enabled: true}, + {groupName: "disabled-group", enabled: false} + ] + } + } + } + }; + const monitored = PluginsUtils.getMonitoredState(state); + expect(monitored.usergroups).toExist(); + expect(monitored.usergroups.length).toBe(2); + expect(monitored.usergroups).toInclude("everyone"); + expect(monitored.usergroups).toInclude("editors"); + expect(monitored.usergroups).toExclude("disabled-group"); + }); + it('getMonitoredState returns empty usergroups when user has no groups', () => { + const state = { security: { user: {} } }; + const monitored = PluginsUtils.getMonitoredState(state); + expect(monitored.usergroups).toExist(); + expect(monitored.usergroups.length).toBe(0); + }); it('handleExpression in case there is a chaining within the expression that needs to access available state', () => { const state = {groups: ["ADMIN", "NORMAL_USER"]};