diff --git a/geonode_mapstore_client/client/js/apps/gn-components.js b/geonode_mapstore_client/client/js/apps/gn-components.js
index 983b964a2c..505dbb3214 100644
--- a/geonode_mapstore_client/client/js/apps/gn-components.js
+++ b/geonode_mapstore_client/client/js/apps/gn-components.js
@@ -36,6 +36,7 @@ import resourceservice from '@js/reducers/resourceservice';
import notifications from '@mapstore/framework/reducers/notifications';
import '@js/observables/persistence';
+import { gnListenToResourcesPendingExecution } from '@js/epics';
const requires = {};
@@ -73,7 +74,8 @@ document.addEventListener('DOMContentLoaded', function() {
const appEpics = cleanEpics({
...configEpics,
...gnresourceEpics,
- ...resourceServiceEpics
+ ...resourceServiceEpics,
+ gnListenToResourcesPendingExecution
});
storeEpicsNamesToExclude(appEpics);
diff --git a/geonode_mapstore_client/client/js/epics/index.js b/geonode_mapstore_client/client/js/epics/index.js
index 58f577e641..817fdfaba8 100644
--- a/geonode_mapstore_client/client/js/epics/index.js
+++ b/geonode_mapstore_client/client/js/epics/index.js
@@ -21,6 +21,11 @@ import { SELECT_NODE, updateNode, ADD_LAYER } from '@mapstore/framework/actions/
import { setSelectedDatasetPermissions, setSelectedLayer, updateLayerDataset, setLayerDataset } from '@js/actions/gnresource';
import { updateMapLayoutEpic as msUpdateMapLayoutEpic } from '@mapstore/framework/epics/maplayout';
import isEmpty from 'lodash/isEmpty';
+import { userSelector } from "@mapstore/framework/selectors/security";
+import { getCurrentProcesses } from "@js/selectors/resourceservice";
+import { extractExecutionsFromResources } from "@js/utils/ResourceServiceUtils";
+import { UPDATE_RESOURCES } from "@mapstore/framework/plugins/ResourcesCatalog/actions/resources";
+import { startAsyncProcess } from "@js/actions/resourceservice";
// We need to include missing epics. The plugins that normally include this epic is not used.
@@ -124,8 +129,38 @@ export const gnSetDatasetsPermissions = (actions$, { getState = () => {}} = {})
export const updateMapLayoutEpic = msUpdateMapLayoutEpic;
+export const gnListenToResourcesPendingExecution = (actions$, { getState = () => {} } = {}) =>
+ actions$.ofType(UPDATE_RESOURCES)
+ .switchMap((action) => {
+ const processes = getCurrentProcesses(getState());
+ const username = userSelector(getState())?.info?.preferred_username;
+ const resourcesToTrack = action.resources;
+ if (!resourcesToTrack?.length || !username) {
+ return Rx.Observable.empty();
+ }
+ const executions = extractExecutionsFromResources(resourcesToTrack, username) || [];
+ if (!executions.length) {
+ return Rx.Observable.empty();
+ }
+ const processesToStart = executions.map((process) => {
+ const pk = process?.resource?.pk ?? process?.resource?.id;
+ const processType = process?.processType;
+ const statusUrl = process?.output?.status_url;
+ if (!pk || !processType || !statusUrl) {
+ return null;
+ }
+ const foundProcess = processes.find((p) => p?.resource?.pk === pk && p?.processType === processType);
+ if (!foundProcess) {
+ return startAsyncProcess({ ...process });
+ }
+ return null;
+ }).filter((process) => process);
+ return Rx.Observable.of(...processesToStart);
+ });
+
export default {
gnCheckSelectedDatasetPermissions,
updateMapLayoutEpic,
- gnSetDatasetsPermissions
+ gnSetDatasetsPermissions,
+ gnListenToResourcesPendingExecution
};
diff --git a/geonode_mapstore_client/client/js/observables/persistence/index.js b/geonode_mapstore_client/client/js/observables/persistence/index.js
index b9d71d91ac..a89df96fd7 100644
--- a/geonode_mapstore_client/client/js/observables/persistence/index.js
+++ b/geonode_mapstore_client/client/js/observables/persistence/index.js
@@ -82,7 +82,7 @@ const persistence = {
}).then(({ resources, ...response }) => {
return {
...response,
- resources: resources.map(parseCatalogResource)
+ resources: resources.map((resource) => parseCatalogResource(resource, monitoredState.user))
};
});
});
diff --git a/geonode_mapstore_client/client/js/plugins/ActionNavbar/buttons.jsx b/geonode_mapstore_client/client/js/plugins/ActionNavbar/buttons.jsx
index cce81eea56..f20391352a 100644
--- a/geonode_mapstore_client/client/js/plugins/ActionNavbar/buttons.jsx
+++ b/geonode_mapstore_client/client/js/plugins/ActionNavbar/buttons.jsx
@@ -31,9 +31,6 @@ import { exportDataResultsControlEnabledSelector, checkingExportDataEntriesSelec
import { currentLocaleSelector } from '@mapstore/framework/selectors/locale';
import { checkExportDataEntries, removeExportDataResult } from '@mapstore/framework/actions/layerdownload';
import ExportDataResultsComponent from '@mapstore/framework/components/data/download/ExportDataResultsComponent';
-import FlexBox from '@mapstore/framework/components/layout/FlexBox';
-import Spinner from '@mapstore/framework/components/layout/Spinner';
-import { getCurrentResourceCopyLoading, getCurrentResourceClonedUrl } from '@js/selectors/resourceservice';
// buttons override to use in ActionNavbar for plugin imported from mapstore
@@ -196,30 +193,3 @@ export const AddWidgetActionButton = connect(
);
});
-
-export const ResourceCloningIndicator = connect(
- (state) => ({
- isCopying: getCurrentResourceCopyLoading(state),
- clonedResourceUrl: getCurrentResourceClonedUrl(state)
- })
-)(({ isCopying, clonedResourceUrl }) => {
- const className = 'text-primary ms-text _font-size-sm _strong';
- if (isCopying) {
- return (
-
-
-
-
- );
- }
-
- if (clonedResourceUrl) {
- return (
-
-
-
- );
- }
-
- return null;
-});
diff --git a/geonode_mapstore_client/client/js/plugins/ExecutionTracker/index.jsx b/geonode_mapstore_client/client/js/plugins/ExecutionTracker/index.jsx
new file mode 100644
index 0000000000..995a7909c7
--- /dev/null
+++ b/geonode_mapstore_client/client/js/plugins/ExecutionTracker/index.jsx
@@ -0,0 +1,135 @@
+/*
+ * 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, useMemo, useRef } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { createPlugin } from '@mapstore/framework/utils/PluginsUtils';
+import { userSelector } from '@mapstore/framework/selectors/security';
+
+import { startAsyncProcess } from '@js/actions/resourceservice';
+import { extractExecutionsFromResources, ProcessTypes } from '@js/utils/ResourceServiceUtils';
+import { getResourceData } from '@js/selectors/resource';
+import isEmpty from 'lodash/isEmpty';
+import { getCurrentProcesses } from '@js/selectors/resourceservice';
+import FlexBox from '@mapstore/framework/components/layout/FlexBox';
+import Spinner from '@mapstore/framework/components/layout/Spinner';
+import Message from '@mapstore/framework/components/I18N/Message';
+
+/**
+ * Plugin that monitors async executions embedded in resources and
+ * triggers the executions API using the existing resourceservice epics.
+ *
+ * It reads `resources[*].executions` checks for the executions, if found it
+ * dispatches `startAsyncProcess({ resource, output, processType })` once per execution.
+ *
+ * @param {Object} user - The user object
+ * @param {Function} onStartAsyncProcess - The function to start an async process
+ * @param {Object} resourceData - The resource data (details page)
+ * @param {Array} processes - The processes to track
+ */
+function ExecutionTracker({
+ user,
+ onStartAsyncProcess,
+ resourceData,
+ processes
+}) {
+ const redirected = useRef(false);
+
+ useEffect(() => {
+ const username = user?.info?.preferred_username;
+ const resourcesToTrack = [resourceData];
+ if (!resourcesToTrack?.length || !username) {
+ return;
+ }
+ const executions = extractExecutionsFromResources(resourcesToTrack, username) || [];
+ if (!executions.length) {
+ return;
+ }
+ executions.forEach((process) => {
+ const pk = process?.resource?.pk ?? process?.resource?.id;
+ const processType = process?.processType;
+ const statusUrl = process?.output?.status_url;
+ if (!pk || !processType || !statusUrl) {
+ return;
+ }
+ const foundProcess = processes.find((p) => p?.resource?.pk === pk && p?.processType === processType);
+ if (!foundProcess) {
+ onStartAsyncProcess(process);
+ }
+ });
+ }, [user, onStartAsyncProcess, resourceData, processes]);
+
+ useEffect(() => {
+ if (redirected.current) {
+ return;
+ }
+ const resourcePk = resourceData?.pk ?? resourceData?.id;
+ if (!resourcePk) {
+ return;
+ }
+ const clonedResourceUrl = (processes || [])
+ .find((p) => p?.resource?.pk === resourcePk && !!p?.clonedResourceUrl)
+ ?.clonedResourceUrl;
+
+ if (clonedResourceUrl && window?.location?.href !== clonedResourceUrl) {
+ redirected.current = true;
+ window.location.assign(clonedResourceUrl);
+ }
+ }, [processes, resourceData]);
+
+ const msgId = useMemo(() => {
+ if (isEmpty(resourceData)) {
+ return null;
+ }
+ const resourcePk = resourceData?.pk ?? resourceData?.id;
+ if (!resourcePk) {
+ return null;
+ }
+ const foundProcess = processes.filter((p) => p?.resource?.pk === resourcePk);
+ if (!foundProcess?.length) {
+ return null;
+ }
+ const copying = foundProcess.some((p) => [ProcessTypes.COPY_RESOURCE, 'copy', 'copy_geonode_resource'].includes(p?.processType));
+ const deleting = foundProcess.some((p) => [ProcessTypes.DELETE_RESOURCE, 'delete'].includes(p?.processType));
+ if (copying) {
+ return 'gnviewer.cloning';
+ }
+ if (deleting) {
+ return 'gnviewer.deleting';
+ }
+ return null;
+ }, [resourceData, processes]);
+
+ return msgId ? (
+
+
+
+
+
+
+ ) : null;
+}
+
+const ExecutionTrackerPlugin = connect(
+ createSelector(
+ [userSelector, getResourceData, getCurrentProcesses],
+ (user, resourceData, processes) => ({
+ user,
+ resourceData,
+ processes
+ })
+ ),
+ {
+ onStartAsyncProcess: startAsyncProcess
+ }
+)(ExecutionTracker);
+
+export default createPlugin('ExecutionTracker', {
+ component: ExecutionTrackerPlugin
+});
diff --git a/geonode_mapstore_client/client/js/plugins/SaveAs.jsx b/geonode_mapstore_client/client/js/plugins/SaveAs.jsx
index d63c1ec178..55cf7341d8 100644
--- a/geonode_mapstore_client/client/js/plugins/SaveAs.jsx
+++ b/geonode_mapstore_client/client/js/plugins/SaveAs.jsx
@@ -36,7 +36,6 @@ import { canCopyResource } from '@js/utils/ResourceUtils';
import { processResources } from '@js/actions/gnresource';
import { getCurrentResourceCopyLoading } from '@js/selectors/resourceservice';
import withPrompt from '@js/plugins/save/withPrompt';
-import { ResourceCloningIndicator } from './ActionNavbar/buttons';
function SaveAs({
resources,
@@ -218,11 +217,6 @@ export default createPlugin('SaveAs', {
ActionNavbar: [{
name: 'SaveAs',
Component: ConnectedSaveAsButton
- }, {
- name: 'ResourceCloningIndicator',
- Component: ResourceCloningIndicator,
- target: 'right-menu',
- position: 1
}],
ResourcesGrid: {
name: ProcessTypes.COPY_RESOURCE,
diff --git a/geonode_mapstore_client/client/js/plugins/index.js b/geonode_mapstore_client/client/js/plugins/index.js
index 3d1326da51..e1a5a96071 100644
--- a/geonode_mapstore_client/client/js/plugins/index.js
+++ b/geonode_mapstore_client/client/js/plugins/index.js
@@ -25,6 +25,7 @@ import BackgroundSelector from '@mapstore/framework/plugins/BackgroundSelector';
import MetadataExplorer from '@mapstore/framework/plugins/MetadataExplorer';
import OperationPlugin from '@js/plugins/Operation';
+import ExecutionTrackerPlugin from '@js/plugins/ExecutionTracker';
import MetadataEditorPlugin from '@js/plugins/MetadataEditor';
import MetadataViewerPlugin from '@js/plugins/MetadataEditor/MetadataViewer';
import FavoritesPlugin from '@js/plugins/Favorites';
@@ -80,6 +81,7 @@ const toModulePlugin = (...args) => {
export const plugins = {
TOCPlugin,
OperationPlugin,
+ ExecutionTrackerPlugin,
MetadataEditorPlugin,
MetadataViewerPlugin,
ResourcesGridPlugin,
diff --git a/geonode_mapstore_client/client/js/utils/ResourceServiceUtils.js b/geonode_mapstore_client/client/js/utils/ResourceServiceUtils.js
index 93bf1321d8..9f23f897a3 100644
--- a/geonode_mapstore_client/client/js/utils/ResourceServiceUtils.js
+++ b/geonode_mapstore_client/client/js/utils/ResourceServiceUtils.js
@@ -55,7 +55,7 @@ export const extractExecutionsFromResources = (resources, username) => {
status_url: statusUrl,
user
}) =>
- funcName === 'copy'
+ ['copy', 'copy_geonode_resource', 'delete', ProcessTypes.DELETE_RESOURCE, ProcessTypes.COPY_RESOURCE].includes(funcName)
&& statusUrl && user && user === username
).map((output) => {
return {
diff --git a/geonode_mapstore_client/client/js/utils/ResourceUtils.js b/geonode_mapstore_client/client/js/utils/ResourceUtils.js
index 8d1909167e..359bc2fb15 100644
--- a/geonode_mapstore_client/client/js/utils/ResourceUtils.js
+++ b/geonode_mapstore_client/client/js/utils/ResourceUtils.js
@@ -376,6 +376,15 @@ export const isDocumentExternalSource = (resource) => {
};
export const getResourceTypesInfo = () => ({
+ 'null': {
+ icon: { glyph: 'dataset' },
+ name: '',
+ canPreviewed: () => false,
+ formatEmbedUrl: () => undefined,
+ formatDetailUrl: () => undefined,
+ formatMetadataUrl: () => undefined,
+ formatMetadataDetailUrl: () => undefined
+ },
[ResourceTypes.DATASET]: {
icon: { glyph: 'dataset' },
canPreviewed: (resource) => resourceHasPermission(resource, 'view_resourcebase'),
@@ -460,11 +469,11 @@ export const getResourceStatuses = (resource, userInfo) => {
const isPublished = isApproved && resource?.is_published;
const runningExecutions = executions.filter(({ func_name: funcName, status, user }) =>
[ProcessStatus.RUNNING, ProcessStatus.READY].includes(status)
- && ['delete', 'copy', ProcessTypes.DELETE_RESOURCE, ProcessTypes.COPY_RESOURCE].includes(funcName)
+ && ['delete', 'copy', 'copy_geonode_resource', ProcessTypes.DELETE_RESOURCE, ProcessTypes.COPY_RESOURCE].includes(funcName)
&& (user === undefined || user === userInfo?.info?.preferred_username));
const isProcessing = !!runningExecutions.length;
const isDeleting = runningExecutions.some(({ func_name: funcName }) => ['delete', ProcessTypes.DELETE_RESOURCE].includes(funcName));
- const isCopying = runningExecutions.some(({ func_name: funcName }) => ['copy', ProcessTypes.COPY_RESOURCE].includes(funcName));
+ const isCopying = runningExecutions.some(({ func_name: funcName }) => ['copy', 'copy_geonode_resource', ProcessTypes.COPY_RESOURCE].includes(funcName));
return {
isApproved,
isPublished,
@@ -859,7 +868,7 @@ export const getResourceAdditionalProperties = (_resource = {}) => {
};
};
-export const parseCatalogResource = (resource) => {
+export const parseCatalogResource = (resource, user) => {
const {
formatDetailUrl,
icon,
@@ -867,7 +876,7 @@ export const parseCatalogResource = (resource) => {
canPreviewed,
hasPermission,
name
- } = getResourceTypesInfo(resource)[resource.resource_type];
+ } = getResourceTypesInfo(resource)[resource.resource_type] || {};
const resourceCanPreviewed = resource?.pk && canPreviewed && canPreviewed(resource);
const embedUrl = resourceCanPreviewed && formatEmbedUrl && resource?.embed_url && formatEmbedUrl(resource);
const canView = resource?.pk && hasPermission && hasPermission(resource);
@@ -892,7 +901,7 @@ export const parseCatalogResource = (resource) => {
metadataDetailUrl,
typeName: name
},
- status: getResourceStatuses(resource)
+ status: getResourceStatuses(resource, user)
}
};
};
diff --git a/geonode_mapstore_client/client/themes/geonode/less/_execution-tracker.less b/geonode_mapstore_client/client/themes/geonode/less/_execution-tracker.less
new file mode 100644
index 0000000000..47f9b3e579
--- /dev/null
+++ b/geonode_mapstore_client/client/themes/geonode/less/_execution-tracker.less
@@ -0,0 +1,22 @@
+#ms-components-theme(@theme-vars) {
+ .gn-main-execution-container {
+ .background-color-var(@theme-vars[main-variant-bg]);
+ .gn-execution-tracker-content {
+ .background-color-var(@theme-vars[main-variant-bg]);
+ }
+ }
+}
+
+.gn-execution-tracker {
+ position: absolute;
+ z-index: 5000;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background-color: rgba(0, 0, 0, 0.85);
+ color: #eeeeee;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
\ No newline at end of file
diff --git a/geonode_mapstore_client/client/themes/geonode/less/geonode.less b/geonode_mapstore_client/client/themes/geonode/less/geonode.less
index b37eced28d..5ac0cdecf8 100644
--- a/geonode_mapstore_client/client/themes/geonode/less/geonode.less
+++ b/geonode_mapstore_client/client/themes/geonode/less/geonode.less
@@ -5,6 +5,7 @@
@import '_action-navbar.less';
@import '_brand-navbar.less';
+@import '_execution-tracker.less';
@import '_footer.less';
@import '_hero.less';
@import '_legend.less';
diff --git a/geonode_mapstore_client/static/mapstore/configs/localConfig.json b/geonode_mapstore_client/static/mapstore/configs/localConfig.json
index 3f99a8f32a..52d006aab8 100644
--- a/geonode_mapstore_client/static/mapstore/configs/localConfig.json
+++ b/geonode_mapstore_client/static/mapstore/configs/localConfig.json
@@ -1240,6 +1240,12 @@
},
{
"name": "MetadataViewer"
+ },
+ {
+ "name": "ExecutionTracker",
+ "cfg": {
+ "containerPosition": "header"
+ }
}
],
"dataset_edit_data_viewer": [