From 1fe140958d4bf41d68966b28a9c7b11fa0027ff3 Mon Sep 17 00:00:00 2001 From: Marc Milard Date: Thu, 22 Jan 2026 19:36:07 +0100 Subject: [PATCH 01/15] feat(plugin): initialization of autorefresh's plugin --- web/client/configs/pluginsConfig.json | 8 ++ .../plugins/AutoRefresh/AutoRefresh.jsx | 85 ++++++++++++++ .../AutoRefresh/actions/autorefresh.js | 32 +++++ .../components/AutoRefreshForm.jsx | 57 +++++++++ .../components/AutoRefreshMenu.jsx | 22 ++++ web/client/plugins/AutoRefresh/constants.js | 5 + .../AutoRefresh/containers/AutoRefresh.jsx | 109 ++++++++++++++++++ .../plugins/AutoRefresh/epics/autorefresh.js | 31 +++++ web/client/plugins/AutoRefresh/index.js | 11 ++ .../AutoRefresh/reducers/autorefresh.js | 41 +++++++ .../AutoRefresh/selectors/autorefresh.js | 9 ++ .../services/AutoRefreshService.js | 70 +++++++++++ web/client/product/plugins.js | 3 +- .../themes/default/less/autorefresh.less | 17 +++ web/client/themes/default/less/mapstore.less | 1 + web/client/translations/data.en-US.json | 10 ++ 16 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 web/client/plugins/AutoRefresh/AutoRefresh.jsx create mode 100644 web/client/plugins/AutoRefresh/actions/autorefresh.js create mode 100644 web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx create mode 100644 web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx create mode 100644 web/client/plugins/AutoRefresh/constants.js create mode 100644 web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx create mode 100644 web/client/plugins/AutoRefresh/epics/autorefresh.js create mode 100644 web/client/plugins/AutoRefresh/index.js create mode 100644 web/client/plugins/AutoRefresh/reducers/autorefresh.js create mode 100644 web/client/plugins/AutoRefresh/selectors/autorefresh.js create mode 100644 web/client/plugins/AutoRefresh/services/AutoRefreshService.js create mode 100644 web/client/themes/default/less/autorefresh.less diff --git a/web/client/configs/pluginsConfig.json b/web/client/configs/pluginsConfig.json index c0919881847..6c24f9f5b3e 100644 --- a/web/client/configs/pluginsConfig.json +++ b/web/client/configs/pluginsConfig.json @@ -599,6 +599,14 @@ "title": "plugins.Isochrone.title", "description": "plugins.Isochrone.description", "dependencies": ["SidebarMenu"] + }, + { + "name": "AutoRefresh", + "glyph": "refresh", + "title": "plugins.AutoRefresh.title", + "description": "plugins.AutoRefresh.description", + "dependencies": ["MapFooter"], + "defaultConfig": {} } ] } diff --git a/web/client/plugins/AutoRefresh/AutoRefresh.jsx b/web/client/plugins/AutoRefresh/AutoRefresh.jsx new file mode 100644 index 00000000000..fde54c92185 --- /dev/null +++ b/web/client/plugins/AutoRefresh/AutoRefresh.jsx @@ -0,0 +1,85 @@ +/* + * Copyright 2026, 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. + */ + +import React from 'react'; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; + +import usePluginItems from "../../hooks/usePluginItems"; +import AutoRefreshContainer from './containers/AutoRefresh'; +import { createPlugin } from '../../utils/PluginsUtils'; +import { createStructuredSelector } from 'reselect'; +import { layersSelector } from '../../selectors/layers'; +import { isLoggedIn, userRoleSelector } from '../../selectors/security'; +import { CONTROL_NAME } from './constants'; +import { registerCustomSaveHandler } from '../../selectors/mapsave'; +import { setEnabled, setInterval, setLayerIds } from './actions/autorefresh'; +import { autorefreshEnabledSelector, autorefreshIntervalSelector, autorefreshLayerIdsSelector } from './selectors/autorefresh'; +import * as epics from './epics/autorefresh'; +import autorefresh from './reducers/autorefresh'; + +registerCustomSaveHandler(CONTROL_NAME, (state) => (state?.[CONTROL_NAME])); + +const AutoRefresh = ({ items, ...props }, context) => { + const { loadedPlugins } = context; + const configuredItems = usePluginItems({ items, loadedPlugins }); + + return ( + + ); +}; + +AutoRefresh.contextTypes = { + loadedPlugins: PropTypes.object +}; + +const autoRefreshConnect = connect( + createStructuredSelector({ + isLoggedIn: isLoggedIn, + userRoles: userRoleSelector, + map: state => state?.mapConfigRawData, + layers: layersSelector, + activeLayerIds: autorefreshLayerIdsSelector, + enabled: autorefreshEnabledSelector, + interval: autorefreshIntervalSelector + }), { + onSetLayerIds: setLayerIds, + onSetEnabled: setEnabled, + onSetInterval: setInterval + } +); + +const AutoRefreshComponent = autoRefreshConnect(AutoRefresh); + +AutoRefreshComponent.propTypes = { + items: PropTypes.array +}; + +export default createPlugin( + 'AutoRefresh', + { + component: AutoRefreshComponent, + reducers: { + autorefresh + }, + epics, + containers: { + SidebarMenu: {}, + BurgerMenu: {}, + MapFooter: { + name: "autorefresh", + position: 20, + target: 'right-footer', + priority: 1 + } + } + } +); diff --git a/web/client/plugins/AutoRefresh/actions/autorefresh.js b/web/client/plugins/AutoRefresh/actions/autorefresh.js new file mode 100644 index 00000000000..177de9320f9 --- /dev/null +++ b/web/client/plugins/AutoRefresh/actions/autorefresh.js @@ -0,0 +1,32 @@ +/* + * Copyright 2026, 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. + */ + +export const SET_LAYERIDS = 'AUTOREFRESH:SET_LAYERIDS'; +export const SET_ENABLED = 'AUTOREFRESH:SET_ENABLED'; +export const SET_INTERVAL = 'AUTOREFRESH:SET_INTERVAL'; + +export const setLayerIds = (layerIds) => { + return { + type: SET_LAYERIDS, + layerIds + }; +}; + +export const setEnabled = (enabled) => { + return { + type: SET_ENABLED, + enabled + }; +}; + +export const setInterval = (interval) => { + return { + type: SET_INTERVAL, + interval + }; +}; diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx new file mode 100644 index 00000000000..53d01e71d37 --- /dev/null +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx @@ -0,0 +1,57 @@ +import React, {useEffect} from 'react'; +import { ControlLabel, Form, FormGroup, FormControl, Checkbox } from "react-bootstrap"; + +import Message from '../../../components/I18N/Message'; +import AutoRefreshService from '../services/AutoRefreshService'; +import { AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC, AUTOREFRESH_MIN_INTERVAL_IN_SEC, AUTOREFRESH_MAX_INTERVAL_IN_SEC, AUTOREFRESH_STEP_INTERVAL_IN_SEC } from '../constants'; + +const AutoRefreshForm = ({ + interval = AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC, + layers = [], + activeLayerIds = [], + handleLayerEnabilityChange, + handleIntervalChange +}) => { + + useEffect(() => { + AutoRefreshService.updateIntervalInSec(interval); + }, [interval]); + + useEffect(() => { + console.debug('[arxit] layers to follow', layers); + AutoRefreshService.setActiveLayers(layers); + }, [layers]); + + return ( +
+
+ + + + + + + {layers.map(l => ( + handleLayerEnabilityChange(e, l)}> + {l.title} + + ))} +
+
+ ); +}; + +export default AutoRefreshForm; diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx new file mode 100644 index 00000000000..5db541a54cf --- /dev/null +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const AutoRefreshMenu = React.forwardRef((props, ref) => { + return ( +
+ {props.children} +
+ ); +}); + +export default AutoRefreshMenu; diff --git a/web/client/plugins/AutoRefresh/constants.js b/web/client/plugins/AutoRefresh/constants.js new file mode 100644 index 00000000000..8ac7fb0926d --- /dev/null +++ b/web/client/plugins/AutoRefresh/constants.js @@ -0,0 +1,5 @@ +export const CONTROL_NAME = "autorefresh"; +export const AUTOREFRESH_STEP_INTERVAL_IN_SEC = 5; +export const AUTOREFRESH_MIN_INTERVAL_IN_SEC = 60; +export const AUTOREFRESH_MAX_INTERVAL_IN_SEC = 300; +export const AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC = 120; diff --git a/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx new file mode 100644 index 00000000000..fc7039f2a02 --- /dev/null +++ b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx @@ -0,0 +1,109 @@ +/* + * Copyright 2025, 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. + */ + +import React, { useEffect, useState } from 'react'; +import { Dropdown, Form, FormGroup, Glyphicon, Checkbox } from "react-bootstrap"; + +import AutoRefreshForm from '../components/AutoRefreshForm'; +import Message from '../../../components/I18N/Message'; +import tooltip from '../../../components/misc/enhancers/tooltip'; +import ButtonRB from '../../../components/misc/Button'; +import AutoRefreshMenu from '../components/AutoRefreshMenu'; +import AutoRefreshService from '../services/AutoRefreshService'; +import { CONTROL_NAME } from '../constants'; + +const Button = tooltip(ButtonRB); +const LAYER_TYPES_TO_FOLLOW = ['wms', "wfs"]; +const AUTHORIZED_ACCESS_ROLES = ['ADMIN']; + +const AutoRefreshContainer = ({ + userRoles, + map, + layers, + enabled, + interval, + activeLayerIds, + onSetLayerIds, + onSetEnabled, + onSetInterval}) => { + + const [layersToFollow, setLayersToFollow] = useState([]); + const [lastUpdatedText, setLastUpdatedText] = useState(null); + const [settingsToggled, setSettingsToggled] = useState(false); + + const handleAutorefreshActivated = (event) => { + const { checked } = event.target || {}; + console.debug('[arxit] Activated change', checked); + + onSetEnabled(checked); + }; + + const handleIntervalChange = (event) => { + const { value } = event.target || {}; + console.debug('[arxit] Interval change', value); + + onSetInterval(value); + }; + + const handleLayerEnabilityChange = (event, layer) => { + const { checked } = event.target || {}; + + if (checked) { + onSetLayerIds([...activeLayerIds, layer.id]); + } else { + onSetLayerIds(activeLayerIds.filter(x => x !== layer.id)); + } + }; + + useEffect(() => { + console.debug('[arxit] layers', layers); + const copy = layers.filter(l => LAYER_TYPES_TO_FOLLOW.includes(l.type)).map(x => structuredClone(x)); + for (let i = 0; i < 1000; i++) { + const c = structuredClone(copy[0]); + c.id = crypto.randomUUID(); + copy.push(c); + } + setLayersToFollow(copy); + }, [layers]); + + useEffect(() => { + AutoRefreshService.onLastUpdated(lastUpdated => setLastUpdatedText(lastUpdated?.toLocaleDateString())); + }, []); + + return (
+
+ + + + + +
+ {AUTHORIZED_ACCESS_ROLES.includes(userRoles) && setSettingsToggled(toggled)}> + + + + + } +
); +}; + +export default AutoRefreshContainer; diff --git a/web/client/plugins/AutoRefresh/epics/autorefresh.js b/web/client/plugins/AutoRefresh/epics/autorefresh.js new file mode 100644 index 00000000000..620ec9f3049 --- /dev/null +++ b/web/client/plugins/AutoRefresh/epics/autorefresh.js @@ -0,0 +1,31 @@ + +import { Observable } from "rxjs"; +import { SET_LAYERIDS, SET_ENABLED, SET_INTERVAL, setLayerIds, setEnabled, setInterval } from "../actions/autorefresh"; +import { CONTROL_NAME } from '../constants'; + +export const autorefreshSetLayerIdsEpic = (action$, store) => + action$.ofType(SET_LAYERIDS) + .switchMap(({layerIds}) => { + console.debug('[arxit][epic] layerIds', layerIds); + + return Observable.empty(); + }); + + +export const autorefreshSetEnabledEpic = (action$, store) => + action$.ofType(SET_ENABLED) + .switchMap(({enabled}) => { + console.debug('[arxit][epic] enabled', enabled); + + return Observable.empty(); + }); + + +export const autorefreshSetIntervalEpic = (action$, store) => + action$.ofType(SET_INTERVAL) + .switchMap(({interval}) => { + console.debug('[arxit][epic] interval', interval); + + return Observable.empty(); + }); + diff --git a/web/client/plugins/AutoRefresh/index.js b/web/client/plugins/AutoRefresh/index.js new file mode 100644 index 00000000000..6af648436d4 --- /dev/null +++ b/web/client/plugins/AutoRefresh/index.js @@ -0,0 +1,11 @@ +/* + * Copyright 2025, 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. + */ + +import AutoRefresh from './AutoRefresh'; + +export default AutoRefresh; diff --git a/web/client/plugins/AutoRefresh/reducers/autorefresh.js b/web/client/plugins/AutoRefresh/reducers/autorefresh.js new file mode 100644 index 00000000000..aa1f5bf31c0 --- /dev/null +++ b/web/client/plugins/AutoRefresh/reducers/autorefresh.js @@ -0,0 +1,41 @@ +/* + * Copyright 2026, 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. + */ + +import { SET_ENABLED, SET_INTERVAL, SET_LAYERIDS } from "../actions/autorefresh"; +import { AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC } from "../constants"; + + +const defaultState = { + enabled: false, + layerIds: [], + interval: AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC +}; + +const autorefresh = (state = {...defaultState}, action) => { + switch (action.type) { + case SET_LAYERIDS: + return { + ...state, + layerIds: Array.from(new Set(action.layerIds)) + }; + case SET_ENABLED: + return { + ...state, + enabled: action.enabled + }; + case SET_INTERVAL: + return { + ...state, + interval: action.interval + }; + default: + return state; + } +}; + +export default autorefresh; diff --git a/web/client/plugins/AutoRefresh/selectors/autorefresh.js b/web/client/plugins/AutoRefresh/selectors/autorefresh.js new file mode 100644 index 00000000000..c4c5ebb33ff --- /dev/null +++ b/web/client/plugins/AutoRefresh/selectors/autorefresh.js @@ -0,0 +1,9 @@ +import get from "lodash/get"; +import { AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC, CONTROL_NAME } from "../constants"; +import { createControlEnabledSelector } from "../../../selectors/controls"; + +export const enabledSelector = createControlEnabledSelector(CONTROL_NAME); + +export const autorefreshLayerIdsSelector = (state) => get(state, `${CONTROL_NAME}.layerIds`, []); +export const autorefreshEnabledSelector = (state) => get(state, `${CONTROL_NAME}.enabled`, false); +export const autorefreshIntervalSelector = (state) => get(state, `${CONTROL_NAME}.interval`, AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC); diff --git a/web/client/plugins/AutoRefresh/services/AutoRefreshService.js b/web/client/plugins/AutoRefresh/services/AutoRefreshService.js new file mode 100644 index 00000000000..7634490830a --- /dev/null +++ b/web/client/plugins/AutoRefresh/services/AutoRefreshService.js @@ -0,0 +1,70 @@ +import { AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC } from "../constants"; + +let isOn = false; +let lastUpdated = null; +let intervalId = null; +let intervalInSecond = AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC; +const activeLayers = {}; + +const doUpdateLayers = () => { + Object.values(activeLayers).forEach(layer => { + console.debug('[arxit] doUpdateLayer', layer.id, layer); + }); + + lastUpdated = new Date(); +}; + +const updateActiveLayer = (layer) => { + if (layer.autorefresh?.enabed) { + activeLayers[layer.id] = layer; + } else { + delete activeLayers[layer.id]; + } +}; + +const setActiveLayers = (layers) => { + layers.forEach(l => { + updateActiveLayer(l); + }); +}; + +const start = () => { + intervalId = setInterval(doUpdateLayers, intervalInSecond * 1000); +}; + +const stop = () => { + clearInterval(intervalId); + intervalId = null; +}; + +const updateIntervalInSec = (interval) => { + if (isOn) { + stop(); + intervalInSecond = interval; + start(); + } else { + intervalInSecond = interval; + } +}; + +const onLastUpdated = (callback) => { + callback(lastUpdated); +}; + +const getIsOn = () => { + return isOn; +}; + +const activate = (shouldActivate) => { + isOn = shouldActivate; + updateIntervalInSec(intervalInSecond); +}; + +export default { + activate, + getIsOn, + onLastUpdated, + updateIntervalInSec, + setActiveLayers +}; + diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index 69e4b2fd1ec..e97c71045f4 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -145,7 +145,8 @@ export const plugins = { ZoomOutPlugin: toModulePlugin('ZoomOut', () => import(/* webpackChunkName: 'plugins/zoomOut' */ '../plugins/ZoomOut')), AddWidgetDashboardPlugin: toModulePlugin('AddWidgetDashboard', () => import(/* webpackChunkName: 'plugins/AddWidgetDashboard' */ '../plugins/AddWidgetDashboard')), MapConnectionDashboardPlugin: toModulePlugin('MapConnectionDashboard', () => import(/* webpackChunkName: 'plugins/MapConnectionDashboard' */ '../plugins/MapConnectionDashboard')), - IPManagerPlugin: toModulePlugin('IPManager', () => import(/* webpackChunkName: 'plugins/IPManager' */ '../plugins/ResourcesCatalog/IPManager')) + IPManagerPlugin: toModulePlugin('IPManager', () => import(/* webpackChunkName: 'plugins/IPManager' */ '../plugins/ResourcesCatalog/IPManager')), + AutoRefreshPlugin: toModulePlugin('AutoRefresh', () => import(/* webpackChunkName: 'plugins/AutoRefresh' */ '../plugins/AutoRefresh')) }; const pluginsDefinition = { diff --git a/web/client/themes/default/less/autorefresh.less b/web/client/themes/default/less/autorefresh.less new file mode 100644 index 00000000000..bff5986fbed --- /dev/null +++ b/web/client/themes/default/less/autorefresh.less @@ -0,0 +1,17 @@ +// ************** +// Theme +// ************** +#ms-components-theme(@theme-vars) { + .ms-autorefresh-divider { + .border-color-var(@theme-vars[main-border-color]); + } +} + +// ************** +// Layout +// ************** +.ms-autorefresh-wrapper { + display: flex; + gap: 1ch; + align-items: center; +} diff --git a/web/client/themes/default/less/mapstore.less b/web/client/themes/default/less/mapstore.less index 4cfb9160d69..0a57b4405a0 100644 --- a/web/client/themes/default/less/mapstore.less +++ b/web/client/themes/default/less/mapstore.less @@ -11,6 +11,7 @@ @import "ag-grid.less"; @import "autocomplete.less"; +@import "autorefresh.less"; @import "cookie.less"; @import "catalog.less"; @import "code-editor.less"; diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 6a0df487933..b795985f935 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -49,6 +49,12 @@ "descriptionAdmin": "Please contact the administrator" }, "autorefresh": { + "selector":"Set layers auto refresh interval", + "label": { + "default":"Activate AutoRefresh" , + "lastUpdated": "Last Refresh", + "interval" : "Interval (sec)" + }, "of": "of", "updating": "Updating...", "layers": "layers" @@ -3725,6 +3731,10 @@ "Isochrone": { "description": "This plugin allows to calculate isochrones and isodistances", "title": "Isochrone" + }, + "AutoRefresh": { + "description": "This plugin allows to set an auto refresh time interval on layers", + "title": "Auto Refresh" } }, "contextCreator": { From 20c3c6534222616f4d7b256b38e0b32873a316a9 Mon Sep 17 00:00:00 2001 From: Marc Milard Date: Thu, 30 Apr 2026 13:21:46 +0200 Subject: [PATCH 02/15] feat(AutoRefresh): components and logic --- .../plugins/AutoRefresh/AutoRefresh.jsx | 14 +-- .../components/AutoRefreshForm.jsx | 111 ++++++++++++------ .../components/AutoRefreshInformations.jsx | 48 ++++++++ web/client/plugins/AutoRefresh/constants.js | 7 +- .../AutoRefresh/containers/AutoRefresh.jsx | 102 +++++++++------- .../plugins/AutoRefresh/epics/autorefresh.js | 22 +--- .../AutoRefresh/reducers/autorefresh.js | 17 +-- .../AutoRefresh/selectors/autorefresh.js | 4 +- .../services/AutoRefreshService.js | 4 +- .../themes/default/less/autorefresh.less | 33 ++++++ web/client/translations/data.en-US.json | 8 +- 11 files changed, 241 insertions(+), 129 deletions(-) create mode 100644 web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx diff --git a/web/client/plugins/AutoRefresh/AutoRefresh.jsx b/web/client/plugins/AutoRefresh/AutoRefresh.jsx index fde54c92185..f0eea33aa3e 100644 --- a/web/client/plugins/AutoRefresh/AutoRefresh.jsx +++ b/web/client/plugins/AutoRefresh/AutoRefresh.jsx @@ -18,8 +18,9 @@ import { layersSelector } from '../../selectors/layers'; import { isLoggedIn, userRoleSelector } from '../../selectors/security'; import { CONTROL_NAME } from './constants'; import { registerCustomSaveHandler } from '../../selectors/mapsave'; -import { setEnabled, setInterval, setLayerIds } from './actions/autorefresh'; -import { autorefreshEnabledSelector, autorefreshIntervalSelector, autorefreshLayerIdsSelector } from './selectors/autorefresh'; +import { setEnabled } from './actions/autorefresh'; +import { updateNode } from '../../actions/layers'; +import { autorefreshEnabledSelector } from './selectors/autorefresh'; import * as epics from './epics/autorefresh'; import autorefresh from './reducers/autorefresh'; @@ -47,13 +48,10 @@ const autoRefreshConnect = connect( userRoles: userRoleSelector, map: state => state?.mapConfigRawData, layers: layersSelector, - activeLayerIds: autorefreshLayerIdsSelector, - enabled: autorefreshEnabledSelector, - interval: autorefreshIntervalSelector + enabled: autorefreshEnabledSelector }), { - onSetLayerIds: setLayerIds, onSetEnabled: setEnabled, - onSetInterval: setInterval + onUpdateNode: updateNode } ); @@ -75,7 +73,7 @@ export default createPlugin( SidebarMenu: {}, BurgerMenu: {}, MapFooter: { - name: "autorefresh", + name: CONTROL_NAME, position: 20, target: 'right-footer', priority: 1 diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx index 53d01e71d37..688734d3634 100644 --- a/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx @@ -1,54 +1,97 @@ import React, {useEffect} from 'react'; -import { ControlLabel, Form, FormGroup, FormControl, Checkbox } from "react-bootstrap"; +import { Glyphicon, ControlLabel, Form, FormGroup, FormControl, Col, InputGroup } from "react-bootstrap"; +import tooltip from '../../../components/misc/enhancers/tooltip'; +import ButtonRB from '../../../components/misc/Button'; import Message from '../../../components/I18N/Message'; import AutoRefreshService from '../services/AutoRefreshService'; -import { AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC, AUTOREFRESH_MIN_INTERVAL_IN_SEC, AUTOREFRESH_MAX_INTERVAL_IN_SEC, AUTOREFRESH_STEP_INTERVAL_IN_SEC } from '../constants'; +import { AUTOREFRESH_STEP_INTERVAL_IN_SECONDS } from '../constants'; + +const Button = tooltip(ButtonRB); const AutoRefreshForm = ({ - interval = AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC, - layers = [], - activeLayerIds = [], - handleLayerEnabilityChange, + defaultRefreshInterval, + minRefreshInterval, + availableLayers = {}, + activeLayers = {}, + handleAddLayer, + handleRemoveLayer, handleIntervalChange }) => { + const onIntervalChange = (event, layer) => { + const { value } = event.target || {}; + + handleIntervalChange(value, layer.id); + }; + + const onAddLayer = (e) => { + if (e.target.value === "none") { + return; + } + + handleAddLayer(e.target.value, defaultRefreshInterval); + e.target.value = "none"; + }; + useEffect(() => { - AutoRefreshService.updateIntervalInSec(interval); - }, [interval]); + AutoRefreshService.updateIntervalInSec(defaultRefreshInterval); + }, [defaultRefreshInterval]); useEffect(() => { - console.debug('[arxit] layers to follow', layers); - AutoRefreshService.setActiveLayers(layers); - }, [layers]); + AutoRefreshService.setActiveLayers(Object.values(availableLayers)); + }, [availableLayers]); + return ( -
+
- - - + + + - + + + {Object.values(availableLayers).map(l => ())} + - {layers.map(l => ( - handleLayerEnabilityChange(e, l)}> - {l.title} - - ))} + +
+ + {Object.values(activeLayers).map(layer => { + + return ( + + + + + + {layer.title} + + + + onIntervalChange(e, layer)} + min={minRefreshInterval} + step={AUTOREFRESH_STEP_INTERVAL_IN_SECONDS} + /> + + s + + + + + ); + })}
); diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx new file mode 100644 index 00000000000..7883a9cbdba --- /dev/null +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx @@ -0,0 +1,48 @@ +import React, {useState} from 'react'; +import { Button, Glyphicon, Dropdown, MenuItem } from 'react-bootstrap'; + +import Message from '../../../components/I18N/Message'; + +const AutoRefreshInformationsMenu = React.forwardRef((props, ref) => { + return ( +
+ + + {props.children} +
+ ); +}); + +const AutoRefreshInformations = ({ + layers = [] +}) => { + const [toggled, setToggled] = useState(false); + + return (
+ setToggled(tg)}> + + +
+ {layers.length === 0 && } + {layers.length > 0 && + layers.map(l => (- {l.title} ({l.autorefreshInterval}s)))} +
+
+
+
); +}; + +export default AutoRefreshInformations; diff --git a/web/client/plugins/AutoRefresh/constants.js b/web/client/plugins/AutoRefresh/constants.js index 8ac7fb0926d..5b965d5c117 100644 --- a/web/client/plugins/AutoRefresh/constants.js +++ b/web/client/plugins/AutoRefresh/constants.js @@ -1,5 +1,4 @@ export const CONTROL_NAME = "autorefresh"; -export const AUTOREFRESH_STEP_INTERVAL_IN_SEC = 5; -export const AUTOREFRESH_MIN_INTERVAL_IN_SEC = 60; -export const AUTOREFRESH_MAX_INTERVAL_IN_SEC = 300; -export const AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC = 120; +export const AUTOREFRESH_STEP_INTERVAL_IN_SECONDS = 5; +export const AUTOREFRESH_MINIMUM_REFRESH_INTERVAL = 30000; +export const AUTOREFRESH_DEFAULT_REFRESH_INTERVAL = 60000; diff --git a/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx index fc7039f2a02..81efb961abc 100644 --- a/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx +++ b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx @@ -7,7 +7,11 @@ */ import React, { useEffect, useState } from 'react'; -import { Dropdown, Form, FormGroup, Glyphicon, Checkbox } from "react-bootstrap"; +import { Dropdown, Glyphicon, Checkbox } from "react-bootstrap"; + +import { + NodeTypes +} from '../../../utils/LayersUtils'; import AutoRefreshForm from '../components/AutoRefreshForm'; import Message from '../../../components/I18N/Message'; @@ -15,60 +19,71 @@ import tooltip from '../../../components/misc/enhancers/tooltip'; import ButtonRB from '../../../components/misc/Button'; import AutoRefreshMenu from '../components/AutoRefreshMenu'; import AutoRefreshService from '../services/AutoRefreshService'; -import { CONTROL_NAME } from '../constants'; +import { AUTOREFRESH_DEFAULT_REFRESH_INTERVAL, AUTOREFRESH_MINIMUM_REFRESH_INTERVAL } from '../constants'; +import AutoRefreshInformations from '../components/AutoRefreshInformations'; const Button = tooltip(ButtonRB); const LAYER_TYPES_TO_FOLLOW = ['wms', "wfs"]; +// Do not consider background layers, since they are not expected to be updated frequently +// and they are not visible in the layer switcher, +// so they cannot be selected by the user in the settings +const LAYER_GROUPS_TO_IGNORE = ['background']; const AUTHORIZED_ACCESS_ROLES = ['ADMIN']; +/** + * AutoRefresh container component. It manages the state and the logic of the AutoRefresh plugin. + * @param {object} props - The props of the container + * @param {number} props.defaultRefreshInterval - The default refresh interval in milliseconds + * @param {number} props.minimumRefreshInterval - The minimum refresh interval in milliseconds + */ const AutoRefreshContainer = ({ + defaultRefreshInterval = AUTOREFRESH_DEFAULT_REFRESH_INTERVAL, + minimumRefreshInterval = AUTOREFRESH_MINIMUM_REFRESH_INTERVAL, userRoles, - map, layers, enabled, - interval, - activeLayerIds, - onSetLayerIds, onSetEnabled, - onSetInterval}) => { + onUpdateNode +}) => { + // const { layers } = splitMapAndLayers(map) || {}; - const [layersToFollow, setLayersToFollow] = useState([]); + const [filteredLayers, setFilteredLayers] = useState({}); + const [activeLayers, setActiveLayers] = useState({}); const [lastUpdatedText, setLastUpdatedText] = useState(null); const [settingsToggled, setSettingsToggled] = useState(false); const handleAutorefreshActivated = (event) => { const { checked } = event.target || {}; - console.debug('[arxit] Activated change', checked); - onSetEnabled(checked); }; - const handleIntervalChange = (event) => { - const { value } = event.target || {}; - console.debug('[arxit] Interval change', value); - - onSetInterval(value); + const handleIntervalChange = (interval, layerId) => { + onUpdateNode(layerId, NodeTypes.LAYER, { autorefreshInterval: Number(interval) }); }; - const handleLayerEnabilityChange = (event, layer) => { - const { checked } = event.target || {}; + const handleAddLayer = (layerId, interval) => { + onUpdateNode(layerId, NodeTypes.LAYER, { autorefreshInterval: Number(interval) }); + }; - if (checked) { - onSetLayerIds([...activeLayerIds, layer.id]); - } else { - onSetLayerIds(activeLayerIds.filter(x => x !== layer.id)); - } + const handleRemoveLayer = (layerId) =>{ + onUpdateNode(layerId, NodeTypes.LAYER, { autorefreshInterval: -1 }); }; useEffect(() => { - console.debug('[arxit] layers', layers); - const copy = layers.filter(l => LAYER_TYPES_TO_FOLLOW.includes(l.type)).map(x => structuredClone(x)); - for (let i = 0; i < 1000; i++) { - const c = structuredClone(copy[0]); - c.id = crypto.randomUUID(); - copy.push(c); - } - setLayersToFollow(copy); + const availables = layers.filter(l => !LAYER_GROUPS_TO_IGNORE.includes(l.group) && LAYER_TYPES_TO_FOLLOW.includes(l.type)); + const actives = availables.filter(l => l.autorefreshInterval > -1); + + const availableLayers = availables.filter(l => !actives.includes(l)).reduce((acc, l) => { + acc[l.id] = l; + return acc; + }, {}); + + setFilteredLayers(availableLayers); + + setActiveLayers(actives.reduce((acc, l) => { + acc[l.id] = l; + return acc; + }, {})); }, [layers]); useEffect(() => { @@ -76,16 +91,17 @@ const AutoRefreshContainer = ({ }, []); return (
-
- - - - - -
+ {/* Only show the layers summary to non-admin users, + since for admin users the summary is already visible in the settings dropdown + */} + {!AUTHORIZED_ACCESS_ROLES.includes(userRoles) && } + + + + + {AUTHORIZED_ACCESS_ROLES.includes(userRoles) && setSettingsToggled(toggled)}>
); diff --git a/web/client/plugins/AutoRefresh/epics/autorefresh.js b/web/client/plugins/AutoRefresh/epics/autorefresh.js index 620ec9f3049..921f1efad14 100644 --- a/web/client/plugins/AutoRefresh/epics/autorefresh.js +++ b/web/client/plugins/AutoRefresh/epics/autorefresh.js @@ -1,31 +1,15 @@ import { Observable } from "rxjs"; -import { SET_LAYERIDS, SET_ENABLED, SET_INTERVAL, setLayerIds, setEnabled, setInterval } from "../actions/autorefresh"; -import { CONTROL_NAME } from '../constants'; - -export const autorefreshSetLayerIdsEpic = (action$, store) => - action$.ofType(SET_LAYERIDS) - .switchMap(({layerIds}) => { - console.debug('[arxit][epic] layerIds', layerIds); - - return Observable.empty(); - }); +import { SET_ENABLED } from "../actions/autorefresh"; export const autorefreshSetEnabledEpic = (action$, store) => action$.ofType(SET_ENABLED) .switchMap(({enabled}) => { - console.debug('[arxit][epic] enabled', enabled); + // TODO: this epic should be responsible for starting and stopping the autorefresh service, + // but for now the service is always active and the epic only returns an empty observable return Observable.empty(); }); -export const autorefreshSetIntervalEpic = (action$, store) => - action$.ofType(SET_INTERVAL) - .switchMap(({interval}) => { - console.debug('[arxit][epic] interval', interval); - - return Observable.empty(); - }); - diff --git a/web/client/plugins/AutoRefresh/reducers/autorefresh.js b/web/client/plugins/AutoRefresh/reducers/autorefresh.js index aa1f5bf31c0..1f5e67238e9 100644 --- a/web/client/plugins/AutoRefresh/reducers/autorefresh.js +++ b/web/client/plugins/AutoRefresh/reducers/autorefresh.js @@ -6,33 +6,20 @@ * LICENSE file in the root directory of this source tree. */ -import { SET_ENABLED, SET_INTERVAL, SET_LAYERIDS } from "../actions/autorefresh"; -import { AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC } from "../constants"; +import { SET_ENABLED } from "../actions/autorefresh"; const defaultState = { - enabled: false, - layerIds: [], - interval: AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC + enabled: false }; const autorefresh = (state = {...defaultState}, action) => { switch (action.type) { - case SET_LAYERIDS: - return { - ...state, - layerIds: Array.from(new Set(action.layerIds)) - }; case SET_ENABLED: return { ...state, enabled: action.enabled }; - case SET_INTERVAL: - return { - ...state, - interval: action.interval - }; default: return state; } diff --git a/web/client/plugins/AutoRefresh/selectors/autorefresh.js b/web/client/plugins/AutoRefresh/selectors/autorefresh.js index c4c5ebb33ff..17547209191 100644 --- a/web/client/plugins/AutoRefresh/selectors/autorefresh.js +++ b/web/client/plugins/AutoRefresh/selectors/autorefresh.js @@ -1,9 +1,7 @@ import get from "lodash/get"; -import { AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC, CONTROL_NAME } from "../constants"; +import { CONTROL_NAME } from "../constants"; import { createControlEnabledSelector } from "../../../selectors/controls"; export const enabledSelector = createControlEnabledSelector(CONTROL_NAME); -export const autorefreshLayerIdsSelector = (state) => get(state, `${CONTROL_NAME}.layerIds`, []); export const autorefreshEnabledSelector = (state) => get(state, `${CONTROL_NAME}.enabled`, false); -export const autorefreshIntervalSelector = (state) => get(state, `${CONTROL_NAME}.interval`, AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC); diff --git a/web/client/plugins/AutoRefresh/services/AutoRefreshService.js b/web/client/plugins/AutoRefresh/services/AutoRefreshService.js index 7634490830a..31963afb3df 100644 --- a/web/client/plugins/AutoRefresh/services/AutoRefreshService.js +++ b/web/client/plugins/AutoRefresh/services/AutoRefreshService.js @@ -1,9 +1,9 @@ -import { AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC } from "../constants"; +import { AUTOREFRESH_DEFAULT_REFRESH_DELAY } from "../constants"; let isOn = false; let lastUpdated = null; let intervalId = null; -let intervalInSecond = AUTOREFRESH_DEFAULT_INTERVAL_IN_SEC; +let intervalInSecond = AUTOREFRESH_DEFAULT_REFRESH_DELAY; const activeLayers = {}; const doUpdateLayers = () => { diff --git a/web/client/themes/default/less/autorefresh.less b/web/client/themes/default/less/autorefresh.less index bff5986fbed..113af4572ed 100644 --- a/web/client/themes/default/less/autorefresh.less +++ b/web/client/themes/default/less/autorefresh.less @@ -15,3 +15,36 @@ gap: 1ch; align-items: center; } + +.ms-autorefresh-form-container{ + min-width: 300px; + overflow: hidden; +} + +.ms-autorefresh-layer-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0; + margin: 0; + + span { + vertical-align: sub; + } +} + +.ms-autorefresh-layers-summary { + display: flex; + flex-direction: column; + gap: 0.5ch; + align-items: flex-start; + max-height: 50vh; + overflow-y: auto; + flex-wrap: nowrap; + + span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index e5d574cc968..7e054afe756 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -52,8 +52,12 @@ "selector":"Set layers auto refresh interval", "label": { "default":"Activate AutoRefresh" , - "lastUpdated": "Last Refresh", - "interval" : "Interval (sec)" + "lastUpdated": "Last Refresh", + "addLayer": "Add layer", + "addLayerPlaceholder": "Select...", + "removeLayer": "Remove layer", + "layersSummary": "Layers automatically refreshed", + "noLayers":"None found" }, "of": "of", "updating": "Updating...", From 3341c9c9f2e7f05372921a4a4c143d9a01238a6a Mon Sep 17 00:00:00 2001 From: Marc Milard Date: Thu, 30 Apr 2026 14:09:32 +0200 Subject: [PATCH 03/15] feat(AutoRefresh): Layers should detect the change on attribute autorefreshInterval --- web/client/utils/LayersUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js index 58df487e228..e8cfaa50268 100644 --- a/web/client/utils/LayersUtils.js +++ b/web/client/utils/LayersUtils.js @@ -739,7 +739,8 @@ export const saveLayer = (layer) => { legendOptions: layer.legendOptions, tileSize: layer.tileSize, version: layer.version, - expanded: layer.expanded || false + expanded: layer.expanded || false, + autorefreshInterval: layer.autorefreshInterval ?? -1 }, layer?.enableInteractiveLegend !== undefined ? { enableInteractiveLegend: layer?.enableInteractiveLegend } : {}, layer?.enableDynamicLegend !== undefined ? { enableDynamicLegend: layer?.enableDynamicLegend } : {}, From 9102be352bead1f68102d2959696aa455d2aa918 Mon Sep 17 00:00:00 2001 From: Marc Milard Date: Fri, 1 May 2026 17:36:18 +0200 Subject: [PATCH 04/15] feat(autorefresh): WFS, WMS updated when autorefresh on / off --- .../components/map/openlayers/Layer.jsx | 43 +++++++++--- .../map/openlayers/plugins/WFSLayer.js | 7 ++ .../map/openlayers/plugins/WMSLayer.js | 15 ++++ .../plugins/AutoRefresh/AutoRefresh.jsx | 6 +- .../AutoRefresh/actions/autorefresh.js | 24 ++----- .../components/AutoRefreshForm.jsx | 14 +--- web/client/plugins/AutoRefresh/constants.js | 4 ++ .../AutoRefresh/containers/AutoRefresh.jsx | 27 +++---- .../plugins/AutoRefresh/epics/autorefresh.js | 15 ---- .../AutoRefresh/reducers/autorefresh.js | 10 +-- .../services/AutoRefreshService.js | 70 ------------------- web/client/plugins/map/index.js | 6 +- .../themes/default/less/autorefresh.less | 7 +- web/client/utils/LayersUtils.js | 2 +- web/client/utils/openlayers/Layers.js | 9 +++ 15 files changed, 106 insertions(+), 153 deletions(-) delete mode 100644 web/client/plugins/AutoRefresh/epics/autorefresh.js delete mode 100644 web/client/plugins/AutoRefresh/services/AutoRefreshService.js diff --git a/web/client/components/map/openlayers/Layer.jsx b/web/client/components/map/openlayers/Layer.jsx index c25ad48a511..19016d7ab04 100644 --- a/web/client/components/map/openlayers/Layer.jsx +++ b/web/client/components/map/openlayers/Layer.jsx @@ -74,6 +74,7 @@ export default class OpenlayersLayer extends React.Component { } if (this.props.options) { this.updateLayer(newProps, this.props); + this.toggleAutorefresh(newProps.options); } } @@ -93,8 +94,8 @@ export default class OpenlayersLayer extends React.Component { this.props.map.removeLayer(this.layer); } } - if (this.refreshTimer) { - clearInterval(this.refreshTimer); + if (this.autorefreshTimer) { + clearInterval(this.autorefreshTimer); } Layers.removeLayer(this.props.type, this.props.options, this.props.map, this.props.mapId, this.layer); } @@ -344,13 +345,6 @@ export default class OpenlayersLayer extends React.Component { this.props.onLayerLoad(options.id, {error: true}); } }); - - if (options.refresh) { - let counter = 0; - this.refreshTimer = setInterval(() => { - this.layer.getSource().updateParams(Object.assign({}, options.params, {_refreshCounter: counter++})); - }, options.refresh); - } } }; @@ -359,4 +353,35 @@ export default class OpenlayersLayer extends React.Component { this.valid = valid; return valid; }; + + toggleAutorefresh = (options) => { + if (!options.autorefreshInterval || this.props.autorefreshEnabled === false || options.autorefreshInterval === -1) { + this.stopAutorefresh(); + return; + } + + this.startAutorefresh(options); + } + + startAutorefresh = (options) => { + if (!options.autorefreshInterval || this.props.autorefreshEnabled === false || options.autorefreshInterval === -1) { + this.stopAutorefresh(); + return; + } + + if (this.autorefreshTimer) { + return; + } + + this.autorefreshTimer = setInterval(() => { + Layers.refreshLayer(this.props.type, this.layer); + }, options.autorefreshInterval * 1000); + } + + stopAutorefresh = () => { + if (this.autorefreshTimer) { + clearInterval(this.autorefreshTimer); + this.autorefreshTimer = null; + } + } } diff --git a/web/client/components/map/openlayers/plugins/WFSLayer.js b/web/client/components/map/openlayers/plugins/WFSLayer.js index af2fd406356..1daf6036762 100644 --- a/web/client/components/map/openlayers/plugins/WFSLayer.js +++ b/web/client/components/map/openlayers/plugins/WFSLayer.js @@ -190,6 +190,13 @@ Layers.registerType('wfs', { layer.setMaxResolution(options.maxResolution === undefined ? Infinity : options.maxResolution); } }, + refresh: (layer) => { + const source = layer.getSource(); + if (source) { + source.clear(); + source.refresh(); + } + }, render: () => { return null; } diff --git a/web/client/components/map/openlayers/plugins/WMSLayer.js b/web/client/components/map/openlayers/plugins/WMSLayer.js index d478e48ce76..af4d939333c 100644 --- a/web/client/components/map/openlayers/plugins/WMSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMSLayer.js @@ -291,5 +291,20 @@ Layers.registerType('wms', { } } return null; + }, + refresh: (layer) => { + const wmsSource = layer.get('wmsSource'); + const vectorSource = layer.getSource(); + + if (wmsSource) { + wmsSource.tileCache?.pruneExceptNewestZ?.(); + wmsSource.clear(); + wmsSource.refresh(); + } + + if (vectorSource) { + vectorSource.clear(); + vectorSource.refresh(); + } } }); diff --git a/web/client/plugins/AutoRefresh/AutoRefresh.jsx b/web/client/plugins/AutoRefresh/AutoRefresh.jsx index f0eea33aa3e..3f07040e4f4 100644 --- a/web/client/plugins/AutoRefresh/AutoRefresh.jsx +++ b/web/client/plugins/AutoRefresh/AutoRefresh.jsx @@ -18,10 +18,9 @@ import { layersSelector } from '../../selectors/layers'; import { isLoggedIn, userRoleSelector } from '../../selectors/security'; import { CONTROL_NAME } from './constants'; import { registerCustomSaveHandler } from '../../selectors/mapsave'; -import { setEnabled } from './actions/autorefresh'; +import { autorefreshSetEnabled } from './actions/autorefresh'; import { updateNode } from '../../actions/layers'; import { autorefreshEnabledSelector } from './selectors/autorefresh'; -import * as epics from './epics/autorefresh'; import autorefresh from './reducers/autorefresh'; registerCustomSaveHandler(CONTROL_NAME, (state) => (state?.[CONTROL_NAME])); @@ -50,7 +49,7 @@ const autoRefreshConnect = connect( layers: layersSelector, enabled: autorefreshEnabledSelector }), { - onSetEnabled: setEnabled, + onSetEnabled: autorefreshSetEnabled, onUpdateNode: updateNode } ); @@ -68,7 +67,6 @@ export default createPlugin( reducers: { autorefresh }, - epics, containers: { SidebarMenu: {}, BurgerMenu: {}, diff --git a/web/client/plugins/AutoRefresh/actions/autorefresh.js b/web/client/plugins/AutoRefresh/actions/autorefresh.js index 177de9320f9..023a499f7b1 100644 --- a/web/client/plugins/AutoRefresh/actions/autorefresh.js +++ b/web/client/plugins/AutoRefresh/actions/autorefresh.js @@ -6,27 +6,13 @@ * LICENSE file in the root directory of this source tree. */ -export const SET_LAYERIDS = 'AUTOREFRESH:SET_LAYERIDS'; -export const SET_ENABLED = 'AUTOREFRESH:SET_ENABLED'; -export const SET_INTERVAL = 'AUTOREFRESH:SET_INTERVAL'; +export const AUTOREFRESH_SET_ENABLED = 'AUTOREFRESH:SET_ENABLED'; -export const setLayerIds = (layerIds) => { +export const autorefreshSetEnabled = (enabled, layers) => { return { - type: SET_LAYERIDS, - layerIds + type: AUTOREFRESH_SET_ENABLED, + enabled, + layers }; }; -export const setEnabled = (enabled) => { - return { - type: SET_ENABLED, - enabled - }; -}; - -export const setInterval = (interval) => { - return { - type: SET_INTERVAL, - interval - }; -}; diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx index 688734d3634..8c2d024929f 100644 --- a/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx @@ -1,10 +1,9 @@ -import React, {useEffect} from 'react'; +import React from 'react'; import { Glyphicon, ControlLabel, Form, FormGroup, FormControl, Col, InputGroup } from "react-bootstrap"; import tooltip from '../../../components/misc/enhancers/tooltip'; import ButtonRB from '../../../components/misc/Button'; import Message from '../../../components/I18N/Message'; -import AutoRefreshService from '../services/AutoRefreshService'; import { AUTOREFRESH_STEP_INTERVAL_IN_SECONDS } from '../constants'; const Button = tooltip(ButtonRB); @@ -21,8 +20,9 @@ const AutoRefreshForm = ({ const onIntervalChange = (event, layer) => { const { value } = event.target || {}; + const numericValue = Number(value); - handleIntervalChange(value, layer.id); + handleIntervalChange(numericValue < minRefreshInterval ? minRefreshInterval : numericValue, layer.id); }; const onAddLayer = (e) => { @@ -34,14 +34,6 @@ const AutoRefreshForm = ({ e.target.value = "none"; }; - useEffect(() => { - AutoRefreshService.updateIntervalInSec(defaultRefreshInterval); - }, [defaultRefreshInterval]); - - useEffect(() => { - AutoRefreshService.setActiveLayers(Object.values(availableLayers)); - }, [availableLayers]); - return (
diff --git a/web/client/plugins/AutoRefresh/constants.js b/web/client/plugins/AutoRefresh/constants.js index 5b965d5c117..e184fc42375 100644 --- a/web/client/plugins/AutoRefresh/constants.js +++ b/web/client/plugins/AutoRefresh/constants.js @@ -2,3 +2,7 @@ export const CONTROL_NAME = "autorefresh"; export const AUTOREFRESH_STEP_INTERVAL_IN_SECONDS = 5; export const AUTOREFRESH_MINIMUM_REFRESH_INTERVAL = 30000; export const AUTOREFRESH_DEFAULT_REFRESH_INTERVAL = 60000; + +export const generateAutorefreshLayerOptions = (interval) => ({ + autorefreshInterval: interval +}); diff --git a/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx index 81efb961abc..46e6edc3677 100644 --- a/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx +++ b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx @@ -18,8 +18,7 @@ import Message from '../../../components/I18N/Message'; import tooltip from '../../../components/misc/enhancers/tooltip'; import ButtonRB from '../../../components/misc/Button'; import AutoRefreshMenu from '../components/AutoRefreshMenu'; -import AutoRefreshService from '../services/AutoRefreshService'; -import { AUTOREFRESH_DEFAULT_REFRESH_INTERVAL, AUTOREFRESH_MINIMUM_REFRESH_INTERVAL } from '../constants'; +import { AUTOREFRESH_DEFAULT_REFRESH_INTERVAL, AUTOREFRESH_MINIMUM_REFRESH_INTERVAL, generateAutorefreshLayerOptions } from '../constants'; import AutoRefreshInformations from '../components/AutoRefreshInformations'; const Button = tooltip(ButtonRB); @@ -39,34 +38,32 @@ const AUTHORIZED_ACCESS_ROLES = ['ADMIN']; const AutoRefreshContainer = ({ defaultRefreshInterval = AUTOREFRESH_DEFAULT_REFRESH_INTERVAL, minimumRefreshInterval = AUTOREFRESH_MINIMUM_REFRESH_INTERVAL, - userRoles, - layers, enabled, onSetEnabled, + userRoles, + layers, onUpdateNode }) => { - // const { layers } = splitMapAndLayers(map) || {}; - const [filteredLayers, setFilteredLayers] = useState({}); const [activeLayers, setActiveLayers] = useState({}); - const [lastUpdatedText, setLastUpdatedText] = useState(null); + const [lastUpdatedText] = useState(null); const [settingsToggled, setSettingsToggled] = useState(false); const handleAutorefreshActivated = (event) => { const { checked } = event.target || {}; - onSetEnabled(checked); + onSetEnabled(checked, activeLayers); }; const handleIntervalChange = (interval, layerId) => { - onUpdateNode(layerId, NodeTypes.LAYER, { autorefreshInterval: Number(interval) }); + onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(interval)); }; const handleAddLayer = (layerId, interval) => { - onUpdateNode(layerId, NodeTypes.LAYER, { autorefreshInterval: Number(interval) }); + onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(interval)); }; const handleRemoveLayer = (layerId) =>{ - onUpdateNode(layerId, NodeTypes.LAYER, { autorefreshInterval: -1 }); + onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(-1)); }; useEffect(() => { @@ -86,10 +83,6 @@ const AutoRefreshContainer = ({ }, {})); }, [layers]); - useEffect(() => { - AutoRefreshService.onLastUpdated(lastUpdated => setLastUpdatedText(lastUpdated?.toLocaleDateString())); - }, []); - return (
{/* Only show the layers summary to non-admin users, since for admin users the summary is already visible in the settings dropdown @@ -100,12 +93,12 @@ const AutoRefreshContainer = ({ - {AUTHORIZED_ACCESS_ROLES.includes(userRoles) && setSettingsToggled(toggled)}> - - - {layer.title} - - - - onIntervalChange(e, layer)} - min={minRefreshInterval} - step={AUTOREFRESH_STEP_INTERVAL_IN_SECONDS} - /> - - s - - - - +
+ + {layer.title} + + onIntervalChange(e, layer)} + min={minRefreshInterval} + step={AUTOREFRESH_STEP_INTERVAL_IN_SECONDS} + /> + + s + + + {ticks[layer.id] && layer.visibility && { formatDate(ticks[layer.id]) }} +
); })} - +
); }; diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx index 7883a9cbdba..f53bec84692 100644 --- a/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx @@ -1,7 +1,12 @@ -import React, {useState} from 'react'; -import { Button, Glyphicon, Dropdown, MenuItem } from 'react-bootstrap'; +import React, { useCallback } from 'react'; +import { Glyphicon, Dropdown, MenuItem } from 'react-bootstrap'; import Message from '../../../components/I18N/Message'; +import tooltip from '../../../components/misc/enhancers/tooltip'; +import ButtonRB from '../../../components/misc/Button'; +import { formatDate } from '../constants'; + +const Button = tooltip(ButtonRB); const AutoRefreshInformationsMenu = React.forwardRef((props, ref) => { return ( @@ -23,22 +28,39 @@ const AutoRefreshInformationsMenu = React.forwardRef((props, ref) => { ); }); + const AutoRefreshInformations = ({ - layers = [] + layers = [], + ticks = {} }) => { - const [toggled, setToggled] = useState(false); + const getFullyQualifiedLayerTitle = useCallback((layer) => { + const title = layer.title; + const interval = `(${layer.autorefreshInterval}s)`; + const lastUpdate = ticks[layer.id] && layer.visibility ? formatDate(ticks[layer.id]) : ''; + return `${title} ${interval} ${lastUpdate}`; + }, [ticks]); return (
- setToggled(tg)}> -
{layers.length === 0 && } {layers.length > 0 && - layers.map(l => (- {l.title} ({l.autorefreshInterval}s)))} + layers.map(l => (
+ {l.title} + {ticks[l.id] && l.visibility && {formatDate(ticks[l.id])}} + {l.autorefreshInterval}s +
))}
diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshSettings.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshSettings.jsx new file mode 100644 index 00000000000..3c8b1e0a7bb --- /dev/null +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshSettings.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Glyphicon, Dropdown } from 'react-bootstrap'; + +import Message from '../../../components/I18N/Message'; +import AutoRefreshMenu from './AutoRefreshMenu'; +import AutoRefreshForm from './AutoRefreshForm'; +import { NodeTypes } from '../../../utils/LayersUtils'; +import { generateAutorefreshLayerOptions } from '../constants'; +import tooltip from '../../../components/misc/enhancers/tooltip'; +import ButtonRB from '../../../components/misc/Button'; + +const Button = tooltip(ButtonRB); + +const AutoRefreshSettings = ({ + defaultRefreshInterval, + minimumRefreshInterval, + availableLayers, + activeLayers, + ticks, + onUpdateNode +}) => { + + const handleIntervalChange = (interval, layerId) => { + onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(interval)); + }; + + const handleAddLayer = (layerId, interval) => { + onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(interval)); + }; + + const handleRemoveLayer = (layerId) =>{ + onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(-1)); + }; + + return ( + + + + + ); +}; + +export default AutoRefreshSettings; diff --git a/web/client/plugins/AutoRefresh/constants.js b/web/client/plugins/AutoRefresh/constants.js index e184fc42375..9097caebeac 100644 --- a/web/client/plugins/AutoRefresh/constants.js +++ b/web/client/plugins/AutoRefresh/constants.js @@ -6,3 +6,22 @@ export const AUTOREFRESH_DEFAULT_REFRESH_INTERVAL = 60000; export const generateAutorefreshLayerOptions = (interval) => ({ autorefreshInterval: interval }); + +export const formatDate = (timestamp) => { + if (!timestamp) { + return null; + } + + const date = new Date(timestamp); + + const options = { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + timeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone + }; + return `(${date.toLocaleString(navigator.language, options)})`; +}; diff --git a/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx index 1a6d8f6bd4a..c231baf3d8b 100644 --- a/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx +++ b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx @@ -7,26 +7,19 @@ */ import React, { useEffect, useState } from 'react'; -import { Dropdown, Glyphicon, Checkbox } from "react-bootstrap"; +import {Checkbox } from "react-bootstrap"; import { - hasAutoRefreshCapability, - NodeTypes + hasAutoRefreshCapability } from '../../../utils/LayersUtils'; - -import AutoRefreshForm from '../components/AutoRefreshForm'; import Message from '../../../components/I18N/Message'; -import tooltip from '../../../components/misc/enhancers/tooltip'; -import ButtonRB from '../../../components/misc/Button'; -import AutoRefreshMenu from '../components/AutoRefreshMenu'; import { AUTOREFRESH_DEFAULT_REFRESH_INTERVAL, - AUTOREFRESH_MINIMUM_REFRESH_INTERVAL, - generateAutorefreshLayerOptions + AUTOREFRESH_MINIMUM_REFRESH_INTERVAL } from '../constants'; import AutoRefreshInformations from '../components/AutoRefreshInformations'; +import AutoRefreshSettings from '../components/AutoRefreshSettings'; -const Button = tooltip(ButtonRB); // Do not consider background layers, since they are not expected to be updated frequently // and they are not visible in the layer switcher, // so they cannot be selected by the user in the settings @@ -53,6 +46,7 @@ const AutoRefreshContainer = ({ enabled, availableLayers, activeLayers, + ticks, // Local actions onStart, @@ -63,7 +57,6 @@ const AutoRefreshContainer = ({ onUpdateNode }) => { const [lastUpdatedText] = useState(null); - const [settingsToggled, setSettingsToggled] = useState(false); const handleAutorefreshActivated = (event) => { const { checked } = event.target || {}; @@ -74,18 +67,6 @@ const AutoRefreshContainer = ({ } }; - const handleIntervalChange = (interval, layerId) => { - onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(interval)); - }; - - const handleAddLayer = (layerId, interval) => { - onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(interval)); - }; - - const handleRemoveLayer = (layerId) =>{ - onUpdateNode(layerId, NodeTypes.LAYER, generateAutorefreshLayerOptions(-1)); - }; - useEffect(() => { const availables = layers.filter(l => !LAYER_GROUPS_TO_IGNORE.includes(l.group) && hasAutoRefreshCapability(l.type, mapType)); @@ -100,33 +81,21 @@ const AutoRefreshContainer = ({ {/* Only show the layers summary to non-admin users, since for admin users the summary is already visible in the settings dropdown */} - {!AUTHORIZED_ACCESS_ROLES.includes(userRoles) && } - - {AUTHORIZED_ACCESS_ROLES.includes(userRoles) && setSettingsToggled(toggled)}> - - - - - } + {AUTHORIZED_ACCESS_ROLES.includes(userRoles) && } + {!AUTHORIZED_ACCESS_ROLES.includes(userRoles) && }
); }; diff --git a/web/client/plugins/AutoRefresh/epics/autorefresh.js b/web/client/plugins/AutoRefresh/epics/autorefresh.js index 2db601b948f..05ea6fcfb80 100644 --- a/web/client/plugins/AutoRefresh/epics/autorefresh.js +++ b/web/client/plugins/AutoRefresh/epics/autorefresh.js @@ -14,7 +14,6 @@ import { AUTOREFRESH_TICK, } from "../actions/autorefresh"; import { REMOVE_NODE, UPDATE_NODE } from '../../../actions/layers'; import { hasAutoRefreshCapability, NodeTypes } from '../../../utils/LayersUtils'; -import { isNumber } from 'lodash'; import { VISUALIZATION_MODE_CHANGED } from '../../../actions/maptype'; import { layersSelector } from '../../../selectors/layers'; import { mapTypeSelector } from '../../../selectors/maptype'; @@ -59,17 +58,24 @@ export const autorefreshActiveLayerChangeEpicCreation = (action$, store) => acti export const autorefreshUpdateNodeEpicCreation = (action$, store) => action$ .ofType(UPDATE_NODE) + .filter(nodeConfig => nodeConfig.nodeType === NodeTypes.LAYER) .filter(nodeConfig => { const activeLayers = store.getState()?.autorefresh.activeLayers || {}; - return nodeConfig.nodeType === NodeTypes.LAYER && - isNumber(nodeConfig.options?.autorefreshInterval) && - activeLayers[nodeConfig.node]; + const isActiveLayer = activeLayers[nodeConfig.node] !== undefined; + const isAutorefreshIntervalChange = 'autorefreshInterval' in nodeConfig.options; + + return isActiveLayer && isAutorefreshIntervalChange; }) .switchMap((nodeConfig) => { + const autorefreshInterval = 'autorefreshInterval' in nodeConfig.options ? + nodeConfig.options.autorefreshInterval : + layersSelector(store.getState()) + .find(l => l.id === nodeConfig.node)?.autorefreshInterval || -1; + return Observable.of( autorefreshUpdateActiveLayer({ id: nodeConfig.node, - autorefreshInterval: nodeConfig.options.autorefreshInterval + autorefreshInterval }) ); }); diff --git a/web/client/plugins/AutoRefresh/reducers/autorefresh.js b/web/client/plugins/AutoRefresh/reducers/autorefresh.js index 40804a4203c..2f8cc7e9e1c 100644 --- a/web/client/plugins/AutoRefresh/reducers/autorefresh.js +++ b/web/client/plugins/AutoRefresh/reducers/autorefresh.js @@ -20,7 +20,8 @@ const defaultState = { enabled: false, availableLayers: {}, activeLayers: {}, - ticks: {} + ticks: {}, + archivedTicks: {} }; const autorefresh = (state = {...defaultState}, action) => { @@ -30,18 +31,26 @@ const autorefresh = (state = {...defaultState}, action) => { case AUTOREFRESH_START: return { ...state, - enabled: true + enabled: true, + ticks: {} }; case AUTOREFRESH_STOP: return { ...state, enabled: false, - ticks: {} + archivedTicks: { + ...state.archivedTicks, + ...state.ticks + } }; case AUTOREFRESH_TICK: return { ...state, - ticks: action.ticks + ticks: action.ticks, + archivedTicks: { + ...state.archivedTicks, + ...action.ticks + } }; case AUTOREFRESH_UPDATE_ACTIVE_LAYERS: return { diff --git a/web/client/plugins/AutoRefresh/selectors/autorefresh.js b/web/client/plugins/AutoRefresh/selectors/autorefresh.js index dee5c8a1732..ddbcf272cac 100644 --- a/web/client/plugins/AutoRefresh/selectors/autorefresh.js +++ b/web/client/plugins/AutoRefresh/selectors/autorefresh.js @@ -10,5 +10,7 @@ export const autorefreshLayersSelector = (state) => get(state, `${CONTROL_NAME}. export const autorefreshTicksSelector = (state) => get(state, `${CONTROL_NAME}.ticks`, {}); +export const autorefreshArchivedTicksSelector = (state) => get(state, `${CONTROL_NAME}.archivedTicks`, {}); + export const autorefreshAvailableLayersSelector = (state) => get(state, `${CONTROL_NAME}.availableLayers`, {}); diff --git a/web/client/themes/default/less/autorefresh.less b/web/client/themes/default/less/autorefresh.less index 7bee23ad54c..02c45322256 100644 --- a/web/client/themes/default/less/autorefresh.less +++ b/web/client/themes/default/less/autorefresh.less @@ -12,13 +12,102 @@ // ************** .ms-autorefresh-wrapper { display: flex; - gap: 1ch; + gap: 0.5ch; + align-items: center; + height: 24px; + border-width: 1px; + border-style: solid; + padding: 2px; + border-radius: 4px; + border-color: var(--ms-main-border-color); + background-color: var(--ms-main-bg); +} + +.ms-autorefresh-button { + display: flex; + align-items: center; + justify-content: center; + height: 20px; + width: 20px; + background-color: transparent; + border: none; + box-shadow: none; + text-decoration: none; + color: var(--ms-main-color); + + &:hover, + &:focus, + &:active, + &:active:focus { + background-color: var(--ms-main-hover-bg); + box-shadow: none; + outline: none; + border: none; + color: inherit; + text-decoration: none; + } +} + +.ms-autorefresh-informations { + display: flex; align-items: center; } .ms-autorefresh-form-container{ min-width: 300px; overflow: hidden; + + .ms-autorefresh-form-group { + display: grid; + align-items: center; + column-gap: 0.5ch; + grid-template-areas: + "button title input" + "button summary input"; + grid-template-columns: auto 1fr 100px; + + &.ms-autorefresh-form-group-hidden { + .ms-autorefresh-form-group__title, + .ms-autorefresh-form-group__input, + .ms-autorefresh-form-group__summary { + opacity: 0.6; + } + } + + &.ms-autorefresh-form-group-inactive { + .ms-autorefresh-form-group__title { + grid-row-end: summary; + } + } + + .ms-autorefresh-form-group__button { + grid-area: button; + } + .ms-autorefresh-form-group__title { + display: flex; + align-items: center; + grid-area: title; + font-size: 12px; + + .glyphicon-eye-close { + margin-right: 0.5ch; + } + } + + .ms-autorefresh-form-group__input { + grid-area: input; + } + .ms-autorefresh-form-group__summary { + grid-area: summary; + font-size: 10px; + } + } + + > form { + display: flex; + flex-direction: column; + gap: 1ch; + } } .ms-autorefresh-layer-title { @@ -34,18 +123,50 @@ } .ms-autorefresh-layers-summary { - display: flex; + display: grid; flex-direction: column; gap: 0.5ch; - align-items: flex-start; max-height: 50vh; overflow-y: auto; - flex-wrap: nowrap; - span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + .ms-autorefresh-layer-summary__row{ + display: grid; + grid-template-areas: + "title interval" + "summary interval"; + grid-template-columns: 1fr 70px; + column-gap: 1ch; + align-items: center; + + .glyphicon { + margin-right: 0; + } + + em { + font-size: 10px; + grid-area: summary; + } + + .ms-autorefresh-layer-summary__row__title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + grid-area: title; + } + + .ms-autorefresh-layer-summary__row__interval { + grid-area: interval; + } + } + + .ms-autorefresh-layer-summary__row-hidden { + opacity: 0.6; + } + + .ms-autorefresh-layer-summary__row-inactive { + .ms-autorefresh-layer-summary__row__title { + grid-row-end: summary; + } } } diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 546c33a5418..e623b9a94b1 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -52,12 +52,13 @@ "selector":"Set layers auto refresh interval", "label": { "default":"Activate AutoRefresh" , - "lastUpdated": "Last Refresh", + "lastUpdated": "Last Refresh", "addLayer": "Add layer", "addLayerPlaceholder": "Select...", "removeLayer": "Remove layer", "layersSummary": "Layers automatically refreshed", - "noLayers":"None found" + "noLayers":"None found", + "informations": "Which layers are configured" }, "of": "of", "updating": "Updating...", From d3b6604de90220be4455b078fc0a5c2e1f5f9772 Mon Sep 17 00:00:00 2001 From: Marc Milard Date: Wed, 3 Jun 2026 17:08:20 +0200 Subject: [PATCH 14/15] fix: old code to remove for openlayers --- web/client/components/map/openlayers/Layer.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/client/components/map/openlayers/Layer.jsx b/web/client/components/map/openlayers/Layer.jsx index 053ef80d45a..242bebe8a05 100644 --- a/web/client/components/map/openlayers/Layer.jsx +++ b/web/client/components/map/openlayers/Layer.jsx @@ -98,9 +98,7 @@ export default class OpenlayersLayer extends React.Component { this.props.map.removeLayer(this.layer); } } - if (this.autorefreshTimer) { - clearInterval(this.autorefreshTimer); - } + Layers.removeLayer(this.props.type, this.props.options, this.props.map, this.props.mapId, this.layer); } From ac0364056fd84160c28d22643b69c355b5e55890 Mon Sep 17 00:00:00 2001 From: Marc Milard Date: Wed, 3 Jun 2026 17:13:17 +0200 Subject: [PATCH 15/15] chore: some file are missing header comment and comments are missing --- .../map/openlayers/plugins/TMSLayer.js | 2 +- .../openlayers/plugins/TileProviderLayer.js | 2 +- .../map/openlayers/plugins/WMSLayer.js | 2 ++ .../map/openlayers/plugins/WMTSLayer.js | 2 +- .../components/AutoRefreshForm.jsx | 8 ++++++ .../components/AutoRefreshInformations.jsx | 8 ++++++ .../components/AutoRefreshMenu.jsx | 8 ++++++ .../components/AutoRefreshSettings.jsx | 8 ++++++ web/client/plugins/AutoRefresh/constants.js | 8 ++++++ .../AutoRefresh/containers/AutoRefresh.jsx | 24 ++++++++-------- .../plugins/AutoRefresh/epics/autorefresh.js | 28 +++++++++++++++++++ 11 files changed, 85 insertions(+), 15 deletions(-) diff --git a/web/client/components/map/openlayers/plugins/TMSLayer.js b/web/client/components/map/openlayers/plugins/TMSLayer.js index 37b10ab8bfe..81f685b6771 100644 --- a/web/client/components/map/openlayers/plugins/TMSLayer.js +++ b/web/client/components/map/openlayers/plugins/TMSLayer.js @@ -96,7 +96,7 @@ Layers.registerType('tms', { // Looks like the cache of the browser is not cleared with source.clear() or source.refresh(). // So, if the map is static, the same tiles are requested, // hence the browser doesn't request new tiles to the server (no https request made). - // if you disable caching in the browser's dev tools, we can see the new tiles are requested to the server. + // if you disable caching in the browser's dev tools, the new tiles are requested to the server. source.refresh(); } } diff --git a/web/client/components/map/openlayers/plugins/TileProviderLayer.js b/web/client/components/map/openlayers/plugins/TileProviderLayer.js index 4c80312016e..2798b3163a0 100644 --- a/web/client/components/map/openlayers/plugins/TileProviderLayer.js +++ b/web/client/components/map/openlayers/plugins/TileProviderLayer.js @@ -125,7 +125,7 @@ Layers.registerType('tileprovider', { // Looks like the cache of the browser is not cleared with source.clear() or source.refresh(). // So, if the map is static, the same tiles are requested, // hence the browser doesn't request new tiles to the server (no https request made). - // if you disable caching in the browser's dev tools, we can see the new tiles are requested to the server. + // if you disable caching in the browser's dev tools, the new tiles are requested to the server. source.refresh(); } } diff --git a/web/client/components/map/openlayers/plugins/WMSLayer.js b/web/client/components/map/openlayers/plugins/WMSLayer.js index 97e982e20d8..d0efbe7526b 100644 --- a/web/client/components/map/openlayers/plugins/WMSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMSLayer.js @@ -307,6 +307,8 @@ Layers.registerType('wms', { return null; }, refresh: (layer) => { + // *source.refresh() doesn't trigger an HTTPS request to reload tiles + // *source.updateParams() with a dummy parameter forces the source to reload tiles const wmsSource = layer.get('wmsSource'); if (wmsSource) { wmsSource.updateParams( diff --git a/web/client/components/map/openlayers/plugins/WMTSLayer.js b/web/client/components/map/openlayers/plugins/WMTSLayer.js index 6d43566d470..8ce0a0364ab 100644 --- a/web/client/components/map/openlayers/plugins/WMTSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMTSLayer.js @@ -208,7 +208,7 @@ const refreshLayer = (layer) => { // Looks like the cache of the browser is not cleared with source.clear() or source.refresh(). // So, if the map is static, the same tiles are requested, // hence the browser doesn't request new tiles to the server (no https request made). - // if you disable caching in the browser's dev tools, we can see the new tiles are requested to the server. + // if you disable caching in the browser's dev tools, the new tiles are requested to the server. source.refresh(); } }; diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx index b5726a54b65..18f70cd7ee1 100644 --- a/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshForm.jsx @@ -1,3 +1,11 @@ +/* + * Copyright 2026, 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. + */ + import React from 'react'; import { Glyphicon, ControlLabel, Form, FormGroup, FormControl, InputGroup } from "react-bootstrap"; diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx index f53bec84692..6ffc8ab1ad3 100644 --- a/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshInformations.jsx @@ -1,3 +1,11 @@ +/* + * Copyright 2026, 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. + */ + import React, { useCallback } from 'react'; import { Glyphicon, Dropdown, MenuItem } from 'react-bootstrap'; diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx index 5db541a54cf..44794a6c07f 100644 --- a/web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshMenu.jsx @@ -1,3 +1,11 @@ +/* + * Copyright 2026, 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. + */ + import React from 'react'; const AutoRefreshMenu = React.forwardRef((props, ref) => { diff --git a/web/client/plugins/AutoRefresh/components/AutoRefreshSettings.jsx b/web/client/plugins/AutoRefresh/components/AutoRefreshSettings.jsx index 3c8b1e0a7bb..028c8648b0f 100644 --- a/web/client/plugins/AutoRefresh/components/AutoRefreshSettings.jsx +++ b/web/client/plugins/AutoRefresh/components/AutoRefreshSettings.jsx @@ -1,3 +1,11 @@ +/* + * Copyright 2026, 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. + */ + import React from 'react'; import { Glyphicon, Dropdown } from 'react-bootstrap'; diff --git a/web/client/plugins/AutoRefresh/constants.js b/web/client/plugins/AutoRefresh/constants.js index 9097caebeac..2db4cad92f7 100644 --- a/web/client/plugins/AutoRefresh/constants.js +++ b/web/client/plugins/AutoRefresh/constants.js @@ -1,3 +1,11 @@ +/* + * Copyright 2026, 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. + */ + export const CONTROL_NAME = "autorefresh"; export const AUTOREFRESH_STEP_INTERVAL_IN_SECONDS = 5; export const AUTOREFRESH_MINIMUM_REFRESH_INTERVAL = 30000; diff --git a/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx index c231baf3d8b..a4ad6ad773c 100644 --- a/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx +++ b/web/client/plugins/AutoRefresh/containers/AutoRefresh.jsx @@ -33,28 +33,28 @@ const AUTHORIZED_ACCESS_ROLES = ['ADMIN']; * @param {number} props.minimumRefreshInterval - The minimum refresh interval in milliseconds */ const AutoRefreshContainer = ({ + // Configured by the user when added to a Context + defaultRefreshInterval = AUTOREFRESH_DEFAULT_REFRESH_INTERVAL, + minimumRefreshInterval = AUTOREFRESH_MINIMUM_REFRESH_INTERVAL, + // Common store userRoles, mapType, layers, - // Configured by the user - defaultRefreshInterval = AUTOREFRESH_DEFAULT_REFRESH_INTERVAL, - minimumRefreshInterval = AUTOREFRESH_MINIMUM_REFRESH_INTERVAL, + // Common actions + onUpdateNode, - // Local store + // AutoRefresh store enabled, availableLayers, activeLayers, ticks, - // Local actions + // AutoRefresh actions onStart, onStop, - onUpdateAvailableLayers, - - // Common actions - onUpdateNode + onUpdateAvailableLayers }) => { const [lastUpdatedText] = useState(null); @@ -78,13 +78,13 @@ const AutoRefreshContainer = ({ return (
- {/* Only show the layers summary to non-admin users, - since for admin users the summary is already visible in the settings dropdown - */} + {/* Non-admin users see the layers information's panel, + Admin users see the settings panel + */} {AUTHORIZED_ACCESS_ROLES.includes(userRoles) && acti autorefreshStart() ) : Observable.of(autorefreshStop())); +/** + * Follow update on nodes (layers) to update the AutoRefresh plugin accordingly + * (add/update layer on AutoRefresh) + * @param {*} action$ + * @param {*} store + * @returns + */ export const autorefreshUpdateNodeEpicCreation = (action$, store) => action$ .ofType(UPDATE_NODE) .filter(nodeConfig => nodeConfig.nodeType === NodeTypes.LAYER) @@ -80,6 +95,12 @@ export const autorefreshUpdateNodeEpicCreation = (action$, store) => action$ ); }); +/** + * Follow removal of nodes (layers) to update the AutoRefresh plugin accordingly + * (remove layer on AutoRefresh) + * @param {*} action$ + * @returns + */ export const autorefreshRemoveNodeEpicCreation = (action$) => action$ .ofType(REMOVE_NODE) .filter(nodeConfig => nodeConfig.nodeType === NodeTypes.LAYER) @@ -89,6 +110,13 @@ export const autorefreshRemoveNodeEpicCreation = (action$) => action$ ); }); +/** + * Follow changes in the map visualization mode (2D/3D) to update the AutoRefresh plugin accordingly + * (update available and active layers on AutoRefresh) + * @param {*} action$ + * @param {*} store + * @returns + */ export const autorefreshMapVisualisationModeChangeEpicCreation = (action$, store) => action$ .ofType(VISUALIZATION_MODE_CHANGED) .switchMap(() => {