Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ const persistence = {
config,
customFilters
}).then(({ resources, ...response }) => {
const filteredResources = resources.filter(resource => resource.resource_type);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like that we don't change client side the resources. We should show all resources returned, instead let's add a new case to support null resource types. Please remove filteredResources and use resources instead

return {
...response,
resources: resources.map(parseCatalogResource)
total: response.total - (resources.length - filteredResources.length),
resources: filteredResources.map((resource) => parseCatalogResource(resource, monitoredState.user))
};
});
});
Expand Down
141 changes: 141 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,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());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this file we are tracking both resources and resourceData but this plugin is only used in dataset pages where only resourceData is available.
Why do we need resources here?

Please remove resources if not needed, instead if needed could you explain why?

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 ? (
<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(
[
(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
});
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 @@ -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';
Expand Down Expand Up @@ -78,6 +79,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'].includes(funcName)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we support also delete now that all dirty state resources are visible (are there other processes we should support?)

&& statusUrl && user && user === username
).map((output) => {
return {
Expand Down
10 changes: 5 additions & 5 deletions geonode_mapstore_client/client/js/utils/ResourceUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -878,15 +878,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 @@ -911,7 +911,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 @@ -28,6 +28,8 @@
@import '~font-awesome/css/font-awesome.min.css';
@import '~ol/ol.css';

@import '_execution-tracker.less';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the other files are imported in alphabetical order above, please add this to the correct position


:root {
font-size: 16px;
}
Expand Down
12 changes: 12 additions & 0 deletions geonode_mapstore_client/static/mapstore/configs/localConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,12 @@
},
{
"name": "MetadataViewer"
},
{
"name": "ExecutionTracker",
"cfg": {
"containerPosition": "header"
}
}
],
"dataset_edit_data_viewer": [
Expand Down Expand Up @@ -3485,6 +3491,12 @@
},
{
"name": "Favorites"
},
{
"name": "ExecutionTracker",
"cfg": {
"containerPosition": "header"
}
}
],
"viewer": [
Expand Down