Source: plugins/ExecutionTracker/index.jsx

/*
 * 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 ? (
        <div className="gn-execution-tracker">
            <FlexBox centerChildren gap="sm" className="ms-text _font-size-lg _strong">
                <Spinner />
                <Message msgId={msgId} />
            </FlexBox>
        </div>
    ) : null;
}

const ExecutionTrackerPlugin = connect(
    createSelector(
        [userSelector, getResourceData, getCurrentProcesses],
        (user, resourceData, processes) => ({
            user,
            resourceData,
            processes
        })
    ),
    {
        onStartAsyncProcess: startAsyncProcess
    }
)(ExecutionTracker);

export default createPlugin('ExecutionTracker', {
    component: ExecutionTrackerPlugin
});