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": [