Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion geonode_mapstore_client/client/js/apps/gn-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};

Expand Down Expand Up @@ -73,7 +74,8 @@ document.addEventListener('DOMContentLoaded', function() {
const appEpics = cleanEpics({
...configEpics,
...gnresourceEpics,
...resourceServiceEpics
...resourceServiceEpics,
gnListenToResourcesPendingExecution
});

storeEpicsNamesToExclude(appEpics);
Expand Down
37 changes: 36 additions & 1 deletion geonode_mapstore_client/client/js/epics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
};
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const persistence = {
}).then(({ resources, ...response }) => {
return {
...response,
resources: resources.map(parseCatalogResource)
resources: resources.map((resource) => parseCatalogResource(resource, monitoredState.user))
};
});
});
Expand Down
30 changes: 0 additions & 30 deletions geonode_mapstore_client/client/js/plugins/ActionNavbar/buttons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -196,30 +193,3 @@ export const AddWidgetActionButton = connect(
</Button>
);
});

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 (
<FlexBox centerChildrenVertically gap="xs" className={className}>
<Spinner />
<Message msgId="gnviewer.cloning" />
</FlexBox>
);
}

if (clonedResourceUrl) {
return (
<a href={clonedResourceUrl} className={className}>
<Message msgId="gnviewer.navigateToClonedResource" />
</a>
);
}

return null;
});
135 changes: 135 additions & 0 deletions geonode_mapstore_client/client/js/plugins/ExecutionTracker/index.jsx
Original file line number Diff line number Diff line change
@@ -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 ? (
<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
});
6 changes: 0 additions & 6 deletions geonode_mapstore_client/client/js/plugins/SaveAs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions geonode_mapstore_client/client/js/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -80,6 +81,7 @@ const toModulePlugin = (...args) => {
export const plugins = {
TOCPlugin,
OperationPlugin,
ExecutionTrackerPlugin,
MetadataEditorPlugin,
MetadataViewerPlugin,
ResourcesGridPlugin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 14 additions & 5 deletions geonode_mapstore_client/client/js/utils/ResourceUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -859,15 +868,15 @@ export const getResourceAdditionalProperties = (_resource = {}) => {
};
};

export const parseCatalogResource = (resource) => {
export const parseCatalogResource = (resource, user) => {
const {
formatDetailUrl,
icon,
formatEmbedUrl,
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);
Expand All @@ -892,7 +901,7 @@ export const parseCatalogResource = (resource) => {
metadataDetailUrl,
typeName: name
},
status: getResourceStatuses(resource)
status: getResourceStatuses(resource, user)
}
};
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading