Skip to content

Commit c018d64

Browse files
Fixes #2290 Improve the UX of the clone operation (#2311)
1 parent eba978e commit c018d64

File tree

12 files changed

+221
-45
lines changed

12 files changed

+221
-45
lines changed

geonode_mapstore_client/client/js/apps/gn-components.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import resourceservice from '@js/reducers/resourceservice';
3636
import notifications from '@mapstore/framework/reducers/notifications';
3737

3838
import '@js/observables/persistence';
39+
import { gnListenToResourcesPendingExecution } from '@js/epics';
3940

4041
const requires = {};
4142

@@ -73,7 +74,8 @@ document.addEventListener('DOMContentLoaded', function() {
7374
const appEpics = cleanEpics({
7475
...configEpics,
7576
...gnresourceEpics,
76-
...resourceServiceEpics
77+
...resourceServiceEpics,
78+
gnListenToResourcesPendingExecution
7779
});
7880

7981
storeEpicsNamesToExclude(appEpics);

geonode_mapstore_client/client/js/epics/index.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import { SELECT_NODE, updateNode, ADD_LAYER } from '@mapstore/framework/actions/
2121
import { setSelectedDatasetPermissions, setSelectedLayer, updateLayerDataset, setLayerDataset } from '@js/actions/gnresource';
2222
import { updateMapLayoutEpic as msUpdateMapLayoutEpic } from '@mapstore/framework/epics/maplayout';
2323
import isEmpty from 'lodash/isEmpty';
24+
import { userSelector } from "@mapstore/framework/selectors/security";
25+
import { getCurrentProcesses } from "@js/selectors/resourceservice";
26+
import { extractExecutionsFromResources } from "@js/utils/ResourceServiceUtils";
27+
import { UPDATE_RESOURCES } from "@mapstore/framework/plugins/ResourcesCatalog/actions/resources";
28+
import { startAsyncProcess } from "@js/actions/resourceservice";
2429

2530
// We need to include missing epics. The plugins that normally include this epic is not used.
2631

@@ -124,8 +129,38 @@ export const gnSetDatasetsPermissions = (actions$, { getState = () => {}} = {})
124129

125130
export const updateMapLayoutEpic = msUpdateMapLayoutEpic;
126131

132+
export const gnListenToResourcesPendingExecution = (actions$, { getState = () => {} } = {}) =>
133+
actions$.ofType(UPDATE_RESOURCES)
134+
.switchMap((action) => {
135+
const processes = getCurrentProcesses(getState());
136+
const username = userSelector(getState())?.info?.preferred_username;
137+
const resourcesToTrack = action.resources;
138+
if (!resourcesToTrack?.length || !username) {
139+
return Rx.Observable.empty();
140+
}
141+
const executions = extractExecutionsFromResources(resourcesToTrack, username) || [];
142+
if (!executions.length) {
143+
return Rx.Observable.empty();
144+
}
145+
const processesToStart = executions.map((process) => {
146+
const pk = process?.resource?.pk ?? process?.resource?.id;
147+
const processType = process?.processType;
148+
const statusUrl = process?.output?.status_url;
149+
if (!pk || !processType || !statusUrl) {
150+
return null;
151+
}
152+
const foundProcess = processes.find((p) => p?.resource?.pk === pk && p?.processType === processType);
153+
if (!foundProcess) {
154+
return startAsyncProcess({ ...process });
155+
}
156+
return null;
157+
}).filter((process) => process);
158+
return Rx.Observable.of(...processesToStart);
159+
});
160+
127161
export default {
128162
gnCheckSelectedDatasetPermissions,
129163
updateMapLayoutEpic,
130-
gnSetDatasetsPermissions
164+
gnSetDatasetsPermissions,
165+
gnListenToResourcesPendingExecution
131166
};

geonode_mapstore_client/client/js/observables/persistence/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const persistence = {
8282
}).then(({ resources, ...response }) => {
8383
return {
8484
...response,
85-
resources: resources.map(parseCatalogResource)
85+
resources: resources.map((resource) => parseCatalogResource(resource, monitoredState.user))
8686
};
8787
});
8888
});

geonode_mapstore_client/client/js/plugins/ActionNavbar/buttons.jsx

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ import { exportDataResultsControlEnabledSelector, checkingExportDataEntriesSelec
3131
import { currentLocaleSelector } from '@mapstore/framework/selectors/locale';
3232
import { checkExportDataEntries, removeExportDataResult } from '@mapstore/framework/actions/layerdownload';
3333
import ExportDataResultsComponent from '@mapstore/framework/components/data/download/ExportDataResultsComponent';
34-
import FlexBox from '@mapstore/framework/components/layout/FlexBox';
35-
import Spinner from '@mapstore/framework/components/layout/Spinner';
36-
import { getCurrentResourceCopyLoading, getCurrentResourceClonedUrl } from '@js/selectors/resourceservice';
3734

3835
// buttons override to use in ActionNavbar for plugin imported from mapstore
3936

@@ -196,30 +193,3 @@ export const AddWidgetActionButton = connect(
196193
</Button>
197194
);
198195
});
199-
200-
export const ResourceCloningIndicator = connect(
201-
(state) => ({
202-
isCopying: getCurrentResourceCopyLoading(state),
203-
clonedResourceUrl: getCurrentResourceClonedUrl(state)
204-
})
205-
)(({ isCopying, clonedResourceUrl }) => {
206-
const className = 'text-primary ms-text _font-size-sm _strong';
207-
if (isCopying) {
208-
return (
209-
<FlexBox centerChildrenVertically gap="xs" className={className}>
210-
<Spinner />
211-
<Message msgId="gnviewer.cloning" />
212-
</FlexBox>
213-
);
214-
}
215-
216-
if (clonedResourceUrl) {
217-
return (
218-
<a href={clonedResourceUrl} className={className}>
219-
<Message msgId="gnviewer.navigateToClonedResource" />
220-
</a>
221-
);
222-
}
223-
224-
return null;
225-
});
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2025, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import React, { useEffect, useMemo, useRef } from 'react';
10+
import { connect } from 'react-redux';
11+
import { createSelector } from 'reselect';
12+
import { createPlugin } from '@mapstore/framework/utils/PluginsUtils';
13+
import { userSelector } from '@mapstore/framework/selectors/security';
14+
15+
import { startAsyncProcess } from '@js/actions/resourceservice';
16+
import { extractExecutionsFromResources, ProcessTypes } from '@js/utils/ResourceServiceUtils';
17+
import { getResourceData } from '@js/selectors/resource';
18+
import isEmpty from 'lodash/isEmpty';
19+
import { getCurrentProcesses } from '@js/selectors/resourceservice';
20+
import FlexBox from '@mapstore/framework/components/layout/FlexBox';
21+
import Spinner from '@mapstore/framework/components/layout/Spinner';
22+
import Message from '@mapstore/framework/components/I18N/Message';
23+
24+
/**
25+
* Plugin that monitors async executions embedded in resources and
26+
* triggers the executions API using the existing resourceservice epics.
27+
*
28+
* It reads `resources[*].executions` checks for the executions, if found it
29+
* dispatches `startAsyncProcess({ resource, output, processType })` once per execution.
30+
*
31+
* @param {Object} user - The user object
32+
* @param {Function} onStartAsyncProcess - The function to start an async process
33+
* @param {Object} resourceData - The resource data (details page)
34+
* @param {Array} processes - The processes to track
35+
*/
36+
function ExecutionTracker({
37+
user,
38+
onStartAsyncProcess,
39+
resourceData,
40+
processes
41+
}) {
42+
const redirected = useRef(false);
43+
44+
useEffect(() => {
45+
const username = user?.info?.preferred_username;
46+
const resourcesToTrack = [resourceData];
47+
if (!resourcesToTrack?.length || !username) {
48+
return;
49+
}
50+
const executions = extractExecutionsFromResources(resourcesToTrack, username) || [];
51+
if (!executions.length) {
52+
return;
53+
}
54+
executions.forEach((process) => {
55+
const pk = process?.resource?.pk ?? process?.resource?.id;
56+
const processType = process?.processType;
57+
const statusUrl = process?.output?.status_url;
58+
if (!pk || !processType || !statusUrl) {
59+
return;
60+
}
61+
const foundProcess = processes.find((p) => p?.resource?.pk === pk && p?.processType === processType);
62+
if (!foundProcess) {
63+
onStartAsyncProcess(process);
64+
}
65+
});
66+
}, [user, onStartAsyncProcess, resourceData, processes]);
67+
68+
useEffect(() => {
69+
if (redirected.current) {
70+
return;
71+
}
72+
const resourcePk = resourceData?.pk ?? resourceData?.id;
73+
if (!resourcePk) {
74+
return;
75+
}
76+
const clonedResourceUrl = (processes || [])
77+
.find((p) => p?.resource?.pk === resourcePk && !!p?.clonedResourceUrl)
78+
?.clonedResourceUrl;
79+
80+
if (clonedResourceUrl && window?.location?.href !== clonedResourceUrl) {
81+
redirected.current = true;
82+
window.location.assign(clonedResourceUrl);
83+
}
84+
}, [processes, resourceData]);
85+
86+
const msgId = useMemo(() => {
87+
if (isEmpty(resourceData)) {
88+
return null;
89+
}
90+
const resourcePk = resourceData?.pk ?? resourceData?.id;
91+
if (!resourcePk) {
92+
return null;
93+
}
94+
const foundProcess = processes.filter((p) => p?.resource?.pk === resourcePk);
95+
if (!foundProcess?.length) {
96+
return null;
97+
}
98+
const copying = foundProcess.some((p) => [ProcessTypes.COPY_RESOURCE, 'copy', 'copy_geonode_resource'].includes(p?.processType));
99+
const deleting = foundProcess.some((p) => [ProcessTypes.DELETE_RESOURCE, 'delete'].includes(p?.processType));
100+
if (copying) {
101+
return 'gnviewer.cloning';
102+
}
103+
if (deleting) {
104+
return 'gnviewer.deleting';
105+
}
106+
return null;
107+
}, [resourceData, processes]);
108+
109+
return msgId ? (
110+
<div className="gn-execution-tracker">
111+
<FlexBox centerChildren gap="sm" className="ms-text _font-size-lg _strong">
112+
<Spinner />
113+
<Message msgId={msgId} />
114+
</FlexBox>
115+
</div>
116+
) : null;
117+
}
118+
119+
const ExecutionTrackerPlugin = connect(
120+
createSelector(
121+
[userSelector, getResourceData, getCurrentProcesses],
122+
(user, resourceData, processes) => ({
123+
user,
124+
resourceData,
125+
processes
126+
})
127+
),
128+
{
129+
onStartAsyncProcess: startAsyncProcess
130+
}
131+
)(ExecutionTracker);
132+
133+
export default createPlugin('ExecutionTracker', {
134+
component: ExecutionTrackerPlugin
135+
});

geonode_mapstore_client/client/js/plugins/SaveAs.jsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import { canCopyResource } from '@js/utils/ResourceUtils';
3636
import { processResources } from '@js/actions/gnresource';
3737
import { getCurrentResourceCopyLoading } from '@js/selectors/resourceservice';
3838
import withPrompt from '@js/plugins/save/withPrompt';
39-
import { ResourceCloningIndicator } from './ActionNavbar/buttons';
4039

4140
function SaveAs({
4241
resources,
@@ -218,11 +217,6 @@ export default createPlugin('SaveAs', {
218217
ActionNavbar: [{
219218
name: 'SaveAs',
220219
Component: ConnectedSaveAsButton
221-
}, {
222-
name: 'ResourceCloningIndicator',
223-
Component: ResourceCloningIndicator,
224-
target: 'right-menu',
225-
position: 1
226220
}],
227221
ResourcesGrid: {
228222
name: ProcessTypes.COPY_RESOURCE,

geonode_mapstore_client/client/js/plugins/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import BackgroundSelector from '@mapstore/framework/plugins/BackgroundSelector';
2525
import MetadataExplorer from '@mapstore/framework/plugins/MetadataExplorer';
2626

2727
import OperationPlugin from '@js/plugins/Operation';
28+
import ExecutionTrackerPlugin from '@js/plugins/ExecutionTracker';
2829
import MetadataEditorPlugin from '@js/plugins/MetadataEditor';
2930
import MetadataViewerPlugin from '@js/plugins/MetadataEditor/MetadataViewer';
3031
import FavoritesPlugin from '@js/plugins/Favorites';
@@ -80,6 +81,7 @@ const toModulePlugin = (...args) => {
8081
export const plugins = {
8182
TOCPlugin,
8283
OperationPlugin,
84+
ExecutionTrackerPlugin,
8385
MetadataEditorPlugin,
8486
MetadataViewerPlugin,
8587
ResourcesGridPlugin,

geonode_mapstore_client/client/js/utils/ResourceServiceUtils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const extractExecutionsFromResources = (resources, username) => {
5555
status_url: statusUrl,
5656
user
5757
}) =>
58-
funcName === 'copy'
58+
['copy', 'copy_geonode_resource', 'delete', ProcessTypes.DELETE_RESOURCE, ProcessTypes.COPY_RESOURCE].includes(funcName)
5959
&& statusUrl && user && user === username
6060
).map((output) => {
6161
return {

geonode_mapstore_client/client/js/utils/ResourceUtils.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,15 @@ export const isDocumentExternalSource = (resource) => {
376376
};
377377

378378
export const getResourceTypesInfo = () => ({
379+
'null': {
380+
icon: { glyph: 'dataset' },
381+
name: '',
382+
canPreviewed: () => false,
383+
formatEmbedUrl: () => undefined,
384+
formatDetailUrl: () => undefined,
385+
formatMetadataUrl: () => undefined,
386+
formatMetadataDetailUrl: () => undefined
387+
},
379388
[ResourceTypes.DATASET]: {
380389
icon: { glyph: 'dataset' },
381390
canPreviewed: (resource) => resourceHasPermission(resource, 'view_resourcebase'),
@@ -460,11 +469,11 @@ export const getResourceStatuses = (resource, userInfo) => {
460469
const isPublished = isApproved && resource?.is_published;
461470
const runningExecutions = executions.filter(({ func_name: funcName, status, user }) =>
462471
[ProcessStatus.RUNNING, ProcessStatus.READY].includes(status)
463-
&& ['delete', 'copy', ProcessTypes.DELETE_RESOURCE, ProcessTypes.COPY_RESOURCE].includes(funcName)
472+
&& ['delete', 'copy', 'copy_geonode_resource', ProcessTypes.DELETE_RESOURCE, ProcessTypes.COPY_RESOURCE].includes(funcName)
464473
&& (user === undefined || user === userInfo?.info?.preferred_username));
465474
const isProcessing = !!runningExecutions.length;
466475
const isDeleting = runningExecutions.some(({ func_name: funcName }) => ['delete', ProcessTypes.DELETE_RESOURCE].includes(funcName));
467-
const isCopying = runningExecutions.some(({ func_name: funcName }) => ['copy', ProcessTypes.COPY_RESOURCE].includes(funcName));
476+
const isCopying = runningExecutions.some(({ func_name: funcName }) => ['copy', 'copy_geonode_resource', ProcessTypes.COPY_RESOURCE].includes(funcName));
468477
return {
469478
isApproved,
470479
isPublished,
@@ -859,15 +868,15 @@ export const getResourceAdditionalProperties = (_resource = {}) => {
859868
};
860869
};
861870

862-
export const parseCatalogResource = (resource) => {
871+
export const parseCatalogResource = (resource, user) => {
863872
const {
864873
formatDetailUrl,
865874
icon,
866875
formatEmbedUrl,
867876
canPreviewed,
868877
hasPermission,
869878
name
870-
} = getResourceTypesInfo(resource)[resource.resource_type];
879+
} = getResourceTypesInfo(resource)[resource.resource_type] || {};
871880
const resourceCanPreviewed = resource?.pk && canPreviewed && canPreviewed(resource);
872881
const embedUrl = resourceCanPreviewed && formatEmbedUrl && resource?.embed_url && formatEmbedUrl(resource);
873882
const canView = resource?.pk && hasPermission && hasPermission(resource);
@@ -892,7 +901,7 @@ export const parseCatalogResource = (resource) => {
892901
metadataDetailUrl,
893902
typeName: name
894903
},
895-
status: getResourceStatuses(resource)
904+
status: getResourceStatuses(resource, user)
896905
}
897906
};
898907
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#ms-components-theme(@theme-vars) {
2+
.gn-main-execution-container {
3+
.background-color-var(@theme-vars[main-variant-bg]);
4+
.gn-execution-tracker-content {
5+
.background-color-var(@theme-vars[main-variant-bg]);
6+
}
7+
}
8+
}
9+
10+
.gn-execution-tracker {
11+
position: absolute;
12+
z-index: 5000;
13+
width: 100%;
14+
height: 100%;
15+
top: 0;
16+
left: 0;
17+
background-color: rgba(0, 0, 0, 0.85);
18+
color: #eeeeee;
19+
display: flex;
20+
align-items: center;
21+
justify-content: center;
22+
}

0 commit comments

Comments
 (0)