From c8378f852b00ca6f9154843a35552f91fb710ee8 Mon Sep 17 00:00:00 2001 From: Sovas Tiwari Date: Fri, 19 Dec 2025 15:49:43 +0545 Subject: [PATCH 1/2] Improve the clone functionality for the catalog and details page Fix the issue to display the status on refresh and call execution tracker api after refresh --- .../js/observables/persistence/index.js | 8 +- .../js/plugins/ExecutionTracker/index.jsx | 143 ++++++++++++++++++ .../client/js/plugins/SaveAs.jsx | 6 - .../client/js/plugins/index.js | 2 + .../client/js/utils/ResourceServiceUtils.js | 2 +- .../client/js/utils/ResourceUtils.js | 10 +- .../geonode/less/_execution-tracker.less | 37 +++++ .../client/themes/geonode/less/geonode.less | 2 + .../static/mapstore/configs/localConfig.json | 12 ++ 9 files changed, 209 insertions(+), 13 deletions(-) create mode 100644 geonode_mapstore_client/client/js/plugins/ExecutionTracker/index.jsx create mode 100644 geonode_mapstore_client/client/themes/geonode/less/_execution-tracker.less diff --git a/geonode_mapstore_client/client/js/observables/persistence/index.js b/geonode_mapstore_client/client/js/observables/persistence/index.js index b9d71d91ac..0dc4a84eda 100644 --- a/geonode_mapstore_client/client/js/observables/persistence/index.js +++ b/geonode_mapstore_client/client/js/observables/persistence/index.js @@ -80,9 +80,15 @@ const persistence = { config, customFilters }).then(({ resources, ...response }) => { + const filterEmptyResources = resources.filter(resource => !resource.resource_type); + const filteredResources = resources.filter(resource => resource.resource_type); + response = { + ...response, + total: response.total - filterEmptyResources.length + } return { ...response, - resources: resources.map(parseCatalogResource) + 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..23419bda26 --- /dev/null +++ b/geonode_mapstore_client/client/js/plugins/ExecutionTracker/index.jsx @@ -0,0 +1,143 @@ +/* + * 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'; +import ViewerLayout from '@js/components/ViewerLayout/ViewerLayout'; + +/** + * 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 foundProcess = processes.filter((p) => p?.resource?.pk === resourceData?.pk); + 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..be9225e142 --- /dev/null +++ b/geonode_mapstore_client/client/themes/geonode/less/_execution-tracker.less @@ -0,0 +1,37 @@ +#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; + .gn-main-execution-container { + background-color: rgba(0, 0, 0, 0.85); + position: relative; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + .gn-execution-tracker-content { + width: 100%; + height: 100%; + color: #eeeeee; + background-color: transparent; + 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": [ From b103a8cd492e42e30b9e32f4975ddbebb2a76dc9 Mon Sep 17 00:00:00 2001 From: Sovas Tiwari Date: Fri, 19 Dec 2025 16:27:39 +0545 Subject: [PATCH 2/2] Fix the eslint and improve the resource data checks --- .../js/observables/persistence/index.js | 6 +---- .../js/plugins/ExecutionTracker/index.jsx | 20 +++++++-------- .../geonode/less/_execution-tracker.less | 25 ++++--------------- 3 files changed, 15 insertions(+), 36 deletions(-) diff --git a/geonode_mapstore_client/client/js/observables/persistence/index.js b/geonode_mapstore_client/client/js/observables/persistence/index.js index 0dc4a84eda..c8a897f5fb 100644 --- a/geonode_mapstore_client/client/js/observables/persistence/index.js +++ b/geonode_mapstore_client/client/js/observables/persistence/index.js @@ -80,14 +80,10 @@ const persistence = { config, customFilters }).then(({ resources, ...response }) => { - const filterEmptyResources = resources.filter(resource => !resource.resource_type); const filteredResources = resources.filter(resource => resource.resource_type); - response = { - ...response, - total: response.total - filterEmptyResources.length - } return { ...response, + 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 index 23419bda26..1678b37ce2 100644 --- a/geonode_mapstore_client/client/js/plugins/ExecutionTracker/index.jsx +++ b/geonode_mapstore_client/client/js/plugins/ExecutionTracker/index.jsx @@ -21,7 +21,6 @@ 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'; -import ViewerLayout from '@js/components/ViewerLayout/ViewerLayout'; /** * Plugin that monitors async executions embedded in resources and @@ -88,8 +87,11 @@ function ExecutionTracker({ if (isEmpty(resourceData)) { return null; } - - const foundProcess = processes.filter((p) => p?.resource?.pk === resourceData?.pk); + const resourcePk = resourceData?.pk ?? resourceData?.id; + if (!resourcePk) { + return null; + } + const foundProcess = processes.filter((p) => p?.resource?.pk === resourcePk); if (!foundProcess?.length) { return null; } @@ -106,14 +108,10 @@ function ExecutionTracker({ return msgId ? (
- -
- - - - -
-
+ + + +
) : null; } diff --git a/geonode_mapstore_client/client/themes/geonode/less/_execution-tracker.less b/geonode_mapstore_client/client/themes/geonode/less/_execution-tracker.less index be9225e142..47f9b3e579 100644 --- a/geonode_mapstore_client/client/themes/geonode/less/_execution-tracker.less +++ b/geonode_mapstore_client/client/themes/geonode/less/_execution-tracker.less @@ -14,24 +14,9 @@ height: 100%; top: 0; left: 0; - .gn-main-execution-container { - background-color: rgba(0, 0, 0, 0.85); - position: relative; - width: 100%; - height: 100%; - margin: 0; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - .gn-execution-tracker-content { - width: 100%; - height: 100%; - color: #eeeeee; - background-color: transparent; - display: flex; - align-items: center; - justify-content: center; - } - } + background-color: rgba(0, 0, 0, 0.85); + color: #eeeeee; + display: flex; + align-items: center; + justify-content: center; } \ No newline at end of file