From 2251a3d0ece596af3e8ce676a92ed31cbcff92de Mon Sep 17 00:00:00 2001 From: Suren Date: Fri, 26 Sep 2025 16:53:28 +0530 Subject: [PATCH 1/4] #2166: Fix - Time series information retrieved for read-only resources --- .../client/js/epics/gnresource.js | 15 ++++++++++++--- .../components/DetailsTimeSeries.jsx | 5 +++-- .../client/js/selectors/resource.js | 7 ++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/geonode_mapstore_client/client/js/epics/gnresource.js b/geonode_mapstore_client/client/js/epics/gnresource.js index 77cfe1d480..fdc3cc551b 100644 --- a/geonode_mapstore_client/client/js/epics/gnresource.js +++ b/geonode_mapstore_client/client/js/epics/gnresource.js @@ -740,11 +740,20 @@ export const gnSelectResourceEpic = (action$, store) => .then((compactPermissions) => compactPermissions) .catch(() => null) : Promise.resolve(null) - ])) - .switchMap(([resource, compactPermissions]) => { + ]) + .then((response) => { + const [resource] = response ?? []; + if (resource?.has_time) { + return getDatasetTimeSettingsByPk(pk) + .then((timeseries) => response.concat(timeseries)); + } + return response; + })) + .switchMap((response) => { + const [resource, compactPermissions, timeseries] = response ?? []; return Observable.of( setResourceType(resourceType), - setResource(getResourceWithDetail(resource)), + setResource(getResourceWithDetail({...resource, timeseries})), ...(compactPermissions ? [setResourceCompactPermissions(compactPermissions)] : []) ); }) diff --git a/geonode_mapstore_client/client/js/plugins/ResourceDetails/components/DetailsTimeSeries.jsx b/geonode_mapstore_client/client/js/plugins/ResourceDetails/components/DetailsTimeSeries.jsx index 9f5cad0880..84e86cf56a 100644 --- a/geonode_mapstore_client/client/js/plugins/ResourceDetails/components/DetailsTimeSeries.jsx +++ b/geonode_mapstore_client/client/js/plugins/ResourceDetails/components/DetailsTimeSeries.jsx @@ -17,7 +17,7 @@ import HTML from '@mapstore/framework/components/I18N/HTML'; import { getMessageById } from '@mapstore/framework/utils/LocaleUtils'; import InfoPopover from '@mapstore/framework/components/widgets/widget/InfoPopover'; -import { TIME_SERIES_PROPERTIES, TIME_ATTRIBUTE_TYPES, TIME_PRECISION_STEPS } from '@js/utils/ResourceUtils'; +import { TIME_SERIES_PROPERTIES, TIME_ATTRIBUTE_TYPES, TIME_PRECISION_STEPS, resourceHasPermission } from '@js/utils/ResourceUtils'; import FlexBox from '@mapstore/framework/components/layout/FlexBox'; import Text from '@mapstore/framework/components/layout/Text'; @@ -25,11 +25,12 @@ const TimeSeriesSettings = ({ resource, onChange }, context) => { const timeAttributes = (resource?.attribute_set ?? []) .filter((attribute) => TIME_ATTRIBUTE_TYPES.includes(attribute.attribute_type)) .map((attribute)=> ({value: attribute.pk, label: attribute.attribute})); + const canChangeResourcebase = resourceHasPermission(resource, 'change_resourcebase'); const [timeseries, setTimeSeries] = useState(resource.timeseries); const [error, setError] = useState(); - if (isEmpty(timeAttributes)) return null; + if (!canChangeResourcebase || isEmpty(timeAttributes)) return null; const onChangeTimeSettings = (key, value) => { const _timeseries = { diff --git a/geonode_mapstore_client/client/js/selectors/resource.js b/geonode_mapstore_client/client/js/selectors/resource.js index 56cb720f21..ca1ad38b47 100644 --- a/geonode_mapstore_client/client/js/selectors/resource.js +++ b/geonode_mapstore_client/client/js/selectors/resource.js @@ -107,6 +107,10 @@ export const getSelectedLayerDataset = (state) => { return state?.gnresource?.selectedLayerDataset; }; +export const isResourceDetail = (state) => { + return get(state, 'gnresource.data["@ms-detail"]', false); +}; + export const getCompactPermissions = (state) => { const compactPermissions = state?.gnresource?.compactPermissions || {}; return compactPermissions; @@ -338,7 +342,8 @@ export const getResourceDirtyState = (state) => { if (resourceType === ResourceTypes.DATASET) { metadataKeys = metadataKeys.concat('timeseries'); } - const { data: initialData = {}, ...resource } = pick(state?.gnresource?.initialResource || {}, metadataKeys); + let { data: initialData = {}, ...resource } = pick(state?.gnresource?.initialResource || {}, metadataKeys); + if (isResourceDetail(state)) initialData = {}; // detail page allows only metadata editing. Data is not editable. const { compactPermissions, geoLimits } = getPermissionsPayload(state); const currentData = JSON.parse(JSON.stringify(getDataPayload(state) || {})); // JSON stringify is needed to remove undefined values // omitting data on thumbnail From e2d4bfdb86c3cce2056de490bd4060d859e108e8 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 9 Oct 2025 15:37:53 +0530 Subject: [PATCH 2/4] Avoid timeseries call when lacking permission --- geonode_mapstore_client/client/js/epics/gnresource.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/geonode_mapstore_client/client/js/epics/gnresource.js b/geonode_mapstore_client/client/js/epics/gnresource.js index fdc3cc551b..4870bf48d3 100644 --- a/geonode_mapstore_client/client/js/epics/gnresource.js +++ b/geonode_mapstore_client/client/js/epics/gnresource.js @@ -91,7 +91,8 @@ import { ResourceTypes, toMapStoreMapConfig, getCataloguePath, - isDefaultDatasetSubtype + isDefaultDatasetSubtype, + resourceHasPermission } from '@js/utils/ResourceUtils'; import { canAddResource, @@ -146,7 +147,7 @@ const resourceTypes = { ]) .then((response) => { const [, gnLayer] = response ?? []; - if (gnLayer?.has_time) { + if (gnLayer?.has_time && resourceHasPermission(gnLayer, 'change_resourcebase')) { // fetch timeseries when applicable return getDatasetTimeSettingsByPk(pk) .then((timeseries) => response.concat(timeseries)); @@ -743,7 +744,7 @@ export const gnSelectResourceEpic = (action$, store) => ]) .then((response) => { const [resource] = response ?? []; - if (resource?.has_time) { + if (resource?.has_time && resourceHasPermission(resource, 'change_resourcebase')) { return getDatasetTimeSettingsByPk(pk) .then((timeseries) => response.concat(timeseries)); } From cf2386334e1e26e7bf4e3e5b08b81f639ccd235e Mon Sep 17 00:00:00 2001 From: Suren Date: Fri, 10 Oct 2025 18:34:01 +0530 Subject: [PATCH 3/4] update dimensions in payload based on has_time --- geonode_mapstore_client/client/js/epics/gnsave.js | 7 +++++-- geonode_mapstore_client/client/js/selectors/resource.js | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/geonode_mapstore_client/client/js/epics/gnsave.js b/geonode_mapstore_client/client/js/epics/gnsave.js index 30e9b7d49b..e2ffc14aa4 100644 --- a/geonode_mapstore_client/client/js/epics/gnsave.js +++ b/geonode_mapstore_client/client/js/epics/gnsave.js @@ -163,6 +163,10 @@ const SaveAPI = { const timeseries = currentResource?.timeseries; const updatedBody = { ...body, + data: { + ...body?.data, + dimensions: timeseries?.has_time ? getDimensions({...body?.data, has_time: true}) : [] + }, ...(timeseries && { has_time: timeseries?.has_time }) }; const { request, actions } = setDefaultStyle(state, id); // set default style, if modified @@ -172,10 +176,9 @@ const SaveAPI = { .then(([_resource]) => { let resource = omit(_resource, 'default_style'); if (timeseries) { - const dimensions = resource?.has_time ? getDimensions({...resource, has_time: true}) : []; const layerId = layersSelector(state)?.find((l) => l.pk === resource?.pk)?.id; // actions to be dispacted are added to response array - return [resource, updateNode(layerId, 'layers', { dimensions: dimensions?.length > 0 ? dimensions : undefined }), ...actions]; + return [resource, updateNode(layerId, 'layers', { dimensions: get(resource, 'data.dimensions', []) }), ...actions]; } return [resource, ...actions]; })); diff --git a/geonode_mapstore_client/client/js/selectors/resource.js b/geonode_mapstore_client/client/js/selectors/resource.js index ca1ad38b47..114c0509f9 100644 --- a/geonode_mapstore_client/client/js/selectors/resource.js +++ b/geonode_mapstore_client/client/js/selectors/resource.js @@ -309,7 +309,8 @@ function isResourceDataEqual(state, initialData = {}, currentData = {}) { const selectedLayerInitial = getSelectedLayer(state); const initialLayerData = {...selectedLayerInitial, ...initialData}; - const isSettingsEqual = compareObjects(omit(currentData, ['style', 'fields']), omit(initialLayerData, ['style', 'fields'])); + const isSettingsEqual = compareObjects(omit(currentData, ['style', 'fields']), + omit(initialLayerData, ['style', 'fields', 'extendedParams', 'pk', '_v_', 'isDataset', 'perms'])); const isStyleEqual = isEmpty(selectedLayer?.availableStyles) || isEmpty(selectedLayer?.style) ? true : selectedLayer?.style === selectedLayer?.availableStyles?.[0]?.name; const isAttributesEqual = isEmpty(selectedLayer) ? true : !isEmpty(initialLayerData) && isEqual(initialLayerData?.fields, selectedLayer.fields); From 6dfb3a7b8fb07a15aae06a1b6cb28ef0275f25f7 Mon Sep 17 00:00:00 2001 From: Suren Date: Fri, 10 Oct 2025 19:22:14 +0530 Subject: [PATCH 4/4] perform update calls sequentially --- geonode_mapstore_client/client/js/epics/gnsave.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/geonode_mapstore_client/client/js/epics/gnsave.js b/geonode_mapstore_client/client/js/epics/gnsave.js index e2ffc14aa4..8c8399a784 100644 --- a/geonode_mapstore_client/client/js/epics/gnsave.js +++ b/geonode_mapstore_client/client/js/epics/gnsave.js @@ -170,10 +170,13 @@ const SaveAPI = { ...(timeseries && { has_time: timeseries?.has_time }) }; const { request, actions } = setDefaultStyle(state, id); // set default style, if modified + return request().then(() => (id - ? axios.all([updateDataset(id, updatedBody), updateDatasetTimeSeries(id, timeseries)]) - : Promise.resolve()) - .then(([_resource]) => { + // perform dataset and timeseries updates sequentially to avoid race conditions + ? updateDataset(id, updatedBody).then((resource) => + updateDatasetTimeSeries(id, timeseries).then(() => resource) + ) : Promise.resolve()) + .then((_resource) => { let resource = omit(_resource, 'default_style'); if (timeseries) { const layerId = layersSelector(state)?.find((l) => l.pk === resource?.pk)?.id;