Skip to content

Commit

Permalink
Sync plugin for dashboard and geostory (#702)
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidQuartz authored Jan 18, 2022
1 parent ba3475a commit afc5074
Show file tree
Hide file tree
Showing 16 changed files with 563 additions and 69 deletions.
18 changes: 18 additions & 0 deletions geonode_mapstore_client/client/js/actions/gnsync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2021, 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.
*/

/**
* Sync geostory components with their live resources on geonode
*/
export const SYNC_RESOURCES = 'GEONODE:SYNC_RESOURCES';

export function syncResources() {
return {
type: SYNC_RESOURCES
};
}
87 changes: 87 additions & 0 deletions geonode_mapstore_client/client/js/epics/__tests__/sync-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2022, 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 expect from 'expect';
import MockAdapter from 'axios-mock-adapter';
import axios from '@mapstore/framework/libs/ajax';
import { testEpic } from '@mapstore/framework/epics/__tests__/epicTestUtils';
import { gnSyncComponentsWithResources } from '../gnsync';
import { syncResources } from '@js/actions/gnsync';
import {
SAVE_SUCCESS, SAVING_RESOURCE
} from '@js/actions/gnsave';
import { EDIT_RESOURCE } from '@mapstore/framework/actions/geostory';
import {
SHOW_NOTIFICATION
} from '@mapstore/framework/actions/notifications';

let mockAxios;

describe('gnsave epics', () => {
beforeEach(done => {
global.__DEVTOOLS__ = true;
mockAxios = new MockAdapter(axios);
setTimeout(done);
});
afterEach(done => {
delete global.__DEVTOOLS__;
mockAxios.restore();
setTimeout(done);
});

const pk = 1;

const geostoryState = {
geostory: {currentStory: {resources: [{data: {id: pk, sourceId: 'geonode', title: 'test'}, type: 'video', id: pk}]}},
gnresource: {type: 'geostory'}
};


it('should sync resources for geostory', (done) => {
const NUM_ACTIONS = 4;
const resource = {
document: {
pk,
title: 'Test title',
thumbnail: 'Test',
src: 'Test src',
description: 'A test',
credits: null,
resource_type: 'video'
}
};

mockAxios.onGet().reply((config) => {
// debug config
if (config.url.match(`/api/v2/documents/${pk}/`)) {
return [200, resource];
}
if (config.url.match(`/api/v2/maps/${pk}/`)) {
return [200, resource];
}
return [200, resource];
});
testEpic(
gnSyncComponentsWithResources,
NUM_ACTIONS,
syncResources(),
(actions) => {
try {
expect(actions.map(({ type }) => type))
.toEqual([
SAVING_RESOURCE, EDIT_RESOURCE, SAVE_SUCCESS, SHOW_NOTIFICATION
]);
} catch (e) {
done(e);
}
done();
},
geostoryState
);
});
});
164 changes: 164 additions & 0 deletions geonode_mapstore_client/client/js/epics/gnsync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright 2021, 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 { Observable } from 'rxjs';
import axios from '@mapstore/framework/libs/ajax';
import { merge } from 'lodash';
import { SYNC_RESOURCES } from '@js/actions/gnsync';
import {
savingResource, saveSuccess
} from '@js/actions/gnsave';
import { getViewedResourceType, getGeonodeResourceDataFromGeostory, getGeonodeResourceFromDashboard } from '@js/selectors/resource';
import { getMapByPk, getDocumentByPk } from '@js/api/geonode/v2';
import { editResource } from '@mapstore/framework/actions/geostory';
import {
show as showNotification,
error as errorNotification
} from '@mapstore/framework/actions/notifications';
import { parseMapConfig, parseDocumentConfig } from '@js/utils/ResourceUtils';
import { dashboardResource, originalDataSelector } from '@mapstore/framework/selectors/dashboard';
import { dashboardLoaded } from '@mapstore/framework/actions/dashboard';

const getRelevantResourceParams = (resourceType, state) => {
let resources = [];
switch (resourceType) {
case 'geostory': {
resources = getGeonodeResourceDataFromGeostory(state);
return resources;
}
case 'dashboard': {
resources = getGeonodeResourceFromDashboard(state);
return resources;
}
default:
return resources;
}
};

const setResourceApi = {
map: getMapByPk,
image: getDocumentByPk,
video: getDocumentByPk
};

/**
* Get resource type and data for state update in sync process
* @param {String} appType geostory or dashboard
* @param {Object} resourceData Resource Object
* @param {Optional: Array} successArr Array of success responses only used in case of dashboard
* @returns {Object}
*/
const getSyncInfo = (appType, resourceData, successArr = []) => {
let type = '';
let updatedData = {};


if (appType === 'geostory') {
type = resourceData.subtype || resourceData.resource_type;
updatedData = type !== 'map' ? parseDocumentConfig(resourceData, resourceData) : parseMapConfig(resourceData);

} else if (appType === 'dashboard') {
const updatedWidgets = resourceData.widgets?.map((widget) => {
const currentWidget = successArr.find(res => !!(res.data.pk === widget.map?.extraParams?.pk));
if (currentWidget) {
return { ...widget, map: { ...widget.map, ...currentWidget.data.data.map } };
}

return widget;
});
updatedData = merge(resourceData, { widgets: updatedWidgets });
}

return { type, data: updatedData };
};

/**
* Get notification title, leve, and message for showNotification
* @param {Number} errors length of errors array
* @param {Number} successes length of success arra
* @returns {Object}
*/
const getNotificationInfo = (errors, successes) => {
let verdict = 'Success';
if (errors > 0 && successes > 0) verdict = 'Warning';
else if (errors === 0 && successes > 0) verdict = 'Success';
else if (errors > 0 && successes === 0) verdict = 'Error';

return {level: verdict.toLowerCase(), title: `gnviewer.sync${verdict}Title`, message: `gnviewer.sync${verdict}Message`};
};

/**
* Sync reources in current geostory or dashboard with their respective sources
* @param {*} action$ the actions
* @param {Object} store
* @returns {Observable}
*/
export const gnSyncComponentsWithResources = (action$, store) => action$.ofType(SYNC_RESOURCES)
.switchMap(() => {
const state = store.getState();
const resourceType = getViewedResourceType(state);
const resources = getRelevantResourceParams(resourceType, state);

return Observable.defer(() =>
axios.all(resources.map((resource) => (resourceType === 'geostory' ?
setResourceApi[resource.type](resource.id)
: getMapByPk(resource?.map?.extraParams?.pk)).then(data => ({ data, status: 'success', title: data.title }))
.catch(() => ({ data: resource, status: 'error', title: resource?.data?.title || resource?.map?.extraParams?.pk || resource?.data?.name }))
)))
.switchMap(updatedResources => {

const errorsResponses = updatedResources.filter(({ status }) => status === 'error');
const successResponses = updatedResources.filter(({ status }) => status === 'success');

const getUpdateActions = () => {
if (successResponses.length === 0) {
return [];
}
if (resourceType === 'geostory') {
return successResponses.map(({ data }) => {
const { type, data: updatedData } = getSyncInfo('geostory', data);
return editResource(data.pk, type, updatedData);
});
}
if (resourceType === 'dashboard') {
const originalData = originalDataSelector(state);
const { data: newResourceData } = getSyncInfo('dashboard', originalData, successResponses);
return [dashboardLoaded(dashboardResource(state), newResourceData)];
}
return [];
};

const updateActions = getUpdateActions();

// notification action into
const {level, title, message} = getNotificationInfo(errorsResponses.length, successResponses.length);

return Observable.of(
...updateActions,
saveSuccess(),
showNotification({
title,
message,
values: {
successTitles: successResponses.map((response) => response.title)?.join(', '),
errorTitles: errorsResponses.map((resource) => resource.title)?.join(', ')
}
}, level)
);
}).catch((error) => {
return Observable.of(
saveSuccess(),
errorNotification({ title: "gnviewer.syncErrorTitle", message: error?.data?.detail || error?.originalError?.message || error?.message || "gnviewer.syncErrorDefault" })
);
}).startWith(savingResource());
});


export default {
gnSyncComponentsWithResources
};
90 changes: 22 additions & 68 deletions geonode_mapstore_client/client/js/observables/media/geonode.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,13 @@ import {
getDocumentsByDocType,
getMapByPk
} from '@js/api/geonode/v2';
import { excludeGoogleBackground, extractTileMatrixFromSources } from '@mapstore/framework/utils/LayersUtils';
import { convertFromLegacy, normalizeConfig } from '@mapstore/framework/utils/ConfigUtils';

function parseMapConfig(mapResponse, resource) {
const { data, pk: id } = mapResponse;
const config = data;
const mapState = !config.version
? convertFromLegacy(config)
: normalizeConfig(config.map);

const layers = excludeGoogleBackground(mapState.layers.map(layer => {
if (layer.group === 'background' && (layer.type === 'ol' || layer.type === 'OpenLayers.Layer')) {
layer.type = 'empty';
}
return layer;
}));

const map = {
...(mapState && mapState.map || {}),
id,
sourceId: resource?.data?.sourceId,
groups: mapState && mapState.groups || [],
layers: mapState?.map?.sources
? layers.map(layer => {
const tileMatrix = extractTileMatrixFromSources(mapState.map.sources, layer);
return { ...layer, ...tileMatrix };
})
: layers
};

return {
...map,
id,
owner: mapResponse?.owner?.username,
canCopy: true,
canDelete: true,
canEdit: true,
name: resource?.data?.title || mapResponse?.title,
description: resource?.data?.description || mapResponse?.abstract,
thumbnail: resource?.data?.thumbnail || mapResponse?.thumbnail_url,
type: 'map'
};
}
import { parseMapConfig, parseDocumentConfig } from '@js/utils/ResourceUtils';

/**
* Get promise of Image dimensions
* @param {string} src geostory image source (href)
* @returns {Promise}
*/
function getImageDimensions(src) {
return new Promise(resolve => {
const img = new Image();
Expand Down Expand Up @@ -85,19 +48,14 @@ const loadMediaList = {
})
.then((response) => {
const totalCount = response.totalCount || 0;
const resources = response.resources.map((resource) => ({
id: resource.pk,
type: 'image',
data: {
thumbnail: resource.thumbnail_url,
src: resource.href,
title: resource.title,
description: resource.raw_abstract,
alt: resource.alternate,
credits: resource.attribution,
sourceId
}
}));
const resources = response.resources.map((resource) => {
const newResource = { ...resource, sourceId };
return {
id: resource.pk,
type: 'image',
data: parseDocumentConfig(newResource)
};
});
const selectedResource = resources.find((resource) => resource.id === selectedId);
if (selectedResource) {
// get resource data when it's selected
Expand Down Expand Up @@ -135,18 +93,14 @@ const loadMediaList = {
})
.then((response) => {
const totalCount = response.totalCount || 0;
const resources = response.resources.map((resource) => ({
id: resource.pk,
type: 'video',
data: {
thumbnail: resource.thumbnail_url,
src: resource.href,
title: resource.title,
description: resource.raw_abstract,
credits: resource.attribution,
sourceId
}
}));
const resources = response.resources.map((resource) => {
const newResource = { ...resource, sourceId };
return {
id: resource.pk,
type: 'video',
data: parseDocumentConfig(newResource)
};
});

return {
resources,
Expand Down
Loading

0 comments on commit afc5074

Please sign in to comment.