diff --git a/geonode_mapstore_client/client/js/observables/persistence/index.js b/geonode_mapstore_client/client/js/observables/persistence/index.js
index b9d71d91ac..c8a897f5fb 100644
--- a/geonode_mapstore_client/client/js/observables/persistence/index.js
+++ b/geonode_mapstore_client/client/js/observables/persistence/index.js
@@ -80,9 +80,11 @@ const persistence = {
config,
customFilters
}).then(({ resources, ...response }) => {
+ const filteredResources = resources.filter(resource => resource.resource_type);
return {
...response,
- resources: resources.map(parseCatalogResource)
+ total: response.total - (resources.length - filteredResources.length),
+ resources: filteredResources.map((resource) => parseCatalogResource(resource, monitoredState.user))
};
});
});
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..1678b37ce2
--- /dev/null
+++ b/geonode_mapstore_client/client/js/plugins/ExecutionTracker/index.jsx
@@ -0,0 +1,141 @@
+/*
+ * 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 { getResources } from '@mapstore/framework/plugins/ResourcesCatalog/selectors/resources';
+
+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` and, when it finds executions and,
+ * dispatches `startAsyncProcess({ resource, output, processType })` once per execution.
+ *
+ */
+function ExecutionTracker({
+ resources,
+ user,
+ onStartAsyncProcess,
+ resourceData,
+ processes
+}) {
+ const startedKeys = useRef(new Set());
+ const redirected = useRef(false);
+
+ useEffect(() => {
+ const username = user?.info?.preferred_username;
+ const resourcesToTrack = !isEmpty(resourceData) ? [...resources, resourceData] : resources;
+ 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 key = `${pk}:${processType}:${statusUrl}`;
+ if (!startedKeys.current.has(key)) {
+ startedKeys.current.add(key);
+ onStartAsyncProcess(process);
+ }
+ });
+ }, [resources, user, onStartAsyncProcess, resourceData]);
+
+ 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;
+ }, [processes, resourceData]);
+
+ return msgId ? (
+
+
+
+
+
+
+ ) : null;
+}
+
+const ExecutionTrackerPlugin = connect(
+ createSelector(
+ [
+ (state) => getResources(state, { id: 'catalog' }),
+ userSelector,
+ getResourceData,
+ getCurrentProcesses
+ ],
+ (resources, user, resourceData, processes) => ({
+ resources,
+ 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 077962ca23..3b59751ea6 100644
--- a/geonode_mapstore_client/client/js/plugins/index.js
+++ b/geonode_mapstore_client/client/js/plugins/index.js
@@ -23,6 +23,7 @@ import Itinerary from "@mapstore/framework/plugins/Itinerary";
import SecurityPopup from "@mapstore/framework/plugins/SecurityPopup";
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';
@@ -78,6 +79,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..37cbfdc9d1 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'].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 13af8b33d1..b2bab53703 100644
--- a/geonode_mapstore_client/client/js/utils/ResourceUtils.js
+++ b/geonode_mapstore_client/client/js/utils/ResourceUtils.js
@@ -460,11 +460,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,
@@ -878,7 +878,7 @@ export const getResourceAdditionalProperties = (_resource = {}) => {
};
};
-export const parseCatalogResource = (resource) => {
+export const parseCatalogResource = (resource, user) => {
const {
formatDetailUrl,
icon,
@@ -886,7 +886,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);
@@ -911,7 +911,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..d138442750 100644
--- a/geonode_mapstore_client/client/themes/geonode/less/geonode.less
+++ b/geonode_mapstore_client/client/themes/geonode/less/geonode.less
@@ -28,6 +28,8 @@
@import '~font-awesome/css/font-awesome.min.css';
@import '~ol/ol.css';
+@import '_execution-tracker.less';
+
:root {
font-size: 16px;
}
diff --git a/geonode_mapstore_client/static/mapstore/configs/localConfig.json b/geonode_mapstore_client/static/mapstore/configs/localConfig.json
index 5e3ce4e6d8..b03dfc286b 100644
--- a/geonode_mapstore_client/static/mapstore/configs/localConfig.json
+++ b/geonode_mapstore_client/static/mapstore/configs/localConfig.json
@@ -1236,6 +1236,12 @@
},
{
"name": "MetadataViewer"
+ },
+ {
+ "name": "ExecutionTracker",
+ "cfg": {
+ "containerPosition": "header"
+ }
}
],
"dataset_edit_data_viewer": [
@@ -3485,6 +3491,12 @@
},
{
"name": "Favorites"
+ },
+ {
+ "name": "ExecutionTracker",
+ "cfg": {
+ "containerPosition": "header"
+ }
}
],
"viewer": [