Closed giohappy closed 3 years ago
@giohappy @tdipisa @afabiani
I would like to ask to @mbarto, @offtherailz and @MV88 some feedbacks on the whole app setup described in this comment because it include an sdk proposal for MapStore.
Below some notes about the design and setup of the SPA. I worked on this branch https://github.com/geosolutions-it/geonode-mapstore-client/tree/spa-setup
This branch contains a first setup for the SPA GeoNode MapStore client.
The folder structure is the same used by MapStore API with some additional files and directories:
n_m/mapstore-sdk/
: it's a folder that simulate a possible way to structure a npm package a mapstore sdk
n_m/mapstore-sdk/app/main.js
: it is a modified version of the MapStore product/main entry and it returns a function that initialize the application and it's possible to add some configuration to modify store or the App
n_m/mapstore-sdk/app/store.js
: it is a modified version of the StandardStore of MapStore and removes all default reducers and epics because they should be provided only on the app initialization or when a plugin is added (redux store)
n_m/mapstore-sdk/app/App.jsx
: it is a modified version of the StandardApp of MapStore, most of function related to extension as been removed
n_m/mapstore-sdk/app/Router.jsx
: it is the component that contains react router and error boundary
n_m/mapstore-sdk/plugins/
: it is the directory that exports some plugins of mapstore
n_m/mapstore-sdk/plugins/PluginsContainer.jsx
is the plugin container of MapStore. It's used inside a route component and it's needed to initialize a context (eg: a map viewer)
n_m/mapstore-sdk/plugins/hooks/useLazyPlugins
: it's an experimental hook used to load plugin asyncrounously. There are currently some issue while loading multiple plugin in a PluginContainer related in particular on the replace of redux store. This has been implemented following the extension load workflow
js/pages/
the pages directory contains all the application entries where an entry applies only to a django html template. Each page have an internal hash route. They app entries are:
home
is an app that represent the SPA of homepage and related overview/preview of resourcesbuilder
is an app that represent the MapStore map viewer (this same kind of app can be used and duplicated also for geostory, dashboard)js/routes/
the routes directory contains components that represent the actual route page (eg: Main, Builder).
plugins/
this directory contains two file:
index.js
have listed all the plugin available. There is an experiment using dynamic imports (evaluate if it's working in complex environments)The proposed routing of the SPA for this phase is the hash path. The hash path makes each html template containing a single application independent and it does not interfere with the current paths in django. We should use the hash path as much as possible to store information of current state or filters, this will help the browser navigation and it adds the possibility to directly share url.
Here an example of possible structure (*):
/
home html template
/#/
homepage no filter applied and visible all the cards/#/resource/:type/
homepage with cards filtered to a specific type ('all', 'maps', 'documents', 'layers', 'geostories', 'dashboards', ...)/#/resource/:type/?query-filters...
homepage with cards filtered to a specific type and additional query filter ('q', 'bbox', ... )/#/resource/:type/:id
homepage with detail preview (probably it will be only for map)/builder/
builder html template (**)
/#/view/:type/:id
view a resource in a viewer based on the type ('map', 'geostory', 'dashboard')/#/edit/:type/:id
edit a resource in a viewer based on the type ('map', 'geostory', 'dashboard')(*) This is a proposal, real paths need to be evaluated while developing.
(**) we could use directly the type
instead of the path /builder/
for the root html template and isolate each builder context (eg /map/#/view/:id
). Check if possible with all MapStore context: dashboard, geostory and map.
Note: the test branch uses /spa/
as root path for testing purpose
MapStore should provide via SDK pure react component (eg Map), utils and plugins. We needs also plugins because some functionality are stricly related to the redux state and epics. To support all the functionality of the plugins we need to wrap the whole application in a redux Provider and add support for epics and use the PluginsContainer as root of the plugin context. These are the investigated possible way we could include mapstore using a sdk inside a route page:
Following the requests for the SPA homepage we should provide at least following modules (this does not coult app component listed above):
Map mapstore-sdk/components/Map: we need it to create a simple map for filter by extension. This module has been mocked in the mapstore-sdk folder using components/map/BaseMap
and map/enhancers/mapType
of MapStore
A draw support for map unrelated to redux state. This could be useful to create spatial filter on the simple map. MapStore has a DrawSupport but it's connected directly to redux action. From a base component I would expect something like this (this component does not exist in MapStore):
<Map
{...props} >
<Draw
{...drawProps}
active={active}
geometryType={geometryType}
onChange={(...args) => console.log(args)}
/>
</Map>
PluginsContainer mapstore-sdk/plugins/PluginsContainer: we need this to create a plugin contaniner context. Mocked in the mapstore-sdk
mapstore-sdk/plugins/[PluginName]: Based on the Detail view of homepage we should select a list of plugins. Currently tested: Map, ZoomIn and ZoomOut. Mocked in the mapstore-sdk
Probably also all the utils, actions, epics and reducers related to plugins and component must be included in the SDK.
General issues encountered in current components of MapStore:
Current setup of mapstore expect that all used plugins are imported directly in the root of the app. Using this approach we load directly all the epics and reducers at the initialization of the app. This could increase the size of the bundle if we import all the plugin of mapsore in the app. We should evaluate the implementation of a way to load a complete plugin asynchronously including its own reducers and epics. In this way we can create an index js with all the available plugins of MapStore made of dynamic import(). While playing around with this repository I tried to apply the load workflow of extensions to the loading of plugin imported dynamically. So I ended up combining a loading workflow for lazy plugins in this react hook .
There are still some issues in this load workflow:
so current implementation has still inconsistent behaviours.
@mbarto What do you think about this proposal? Do you think we could implement something that could load plugins as discribed above?
If this approach can't be applied we could import plugins as usual.
https://github.com/geosolutions-it/geonode-mapstore-client/issues/188
// TODO
// TODO
// TODO
We need to discuss this section between backend and frontend
@allyoucanmap I think your proposal is going in the right direction. I agree with most of your thinking.
About lazy plugins loading, did you have a look at this example: https://github.com/geosolutions-it/MapStore2/tree/master/web/client/examples/lazyplugins ?
That one is where I started implementing the ability to dinamically load and activate a whole plugin on demand (reducers and epics included). Don't consider the product simplified implementation (everything loaded at startup) as the state of the art.
Extensions are not loaded as lazy plugins by the product, but the underlying SDK should be already able to do so (if you find any issue with that we can work together to make it fully functional for your use case).
Another point I would like to discuss in depth will all of you is how to slice properly the actual MapStore2 big monster into separated entities (and how many slices are needed to make everyone happy (@giohappy included of course)).
These are my candidates:
Obviously, I am not saying we need all 4 slices, but we at least need to know they should logically exist and how to better organize them in physical repositories / packages.
:wave: @giohappy, answer Florian
About lazy plugins loading, did you have a look at this example: https://github.com/geosolutions-it/MapStore2/tree/master/web/client/examples/lazyplugins ?
That one is where I started implementing the ability to dinamically load and activate a whole plugin on demand (reducers and epics included). Don't consider the product simplified implementation (everything loaded at startup) as the state of the art.
@mbarto
Yes, I took a look to that code and the one related to extensions while implementing the useLazyPlugins
hook helper. I focused the implementation trying to use an existing function augmentStore
(just noticed you used updateStore
instead) and import only the plugins declared in the config of the selected route (the solution provided in the lazy plugings example imports dynamically all the plugin index). Here what I tried to achieve:
const plugins = {
TestPlugin: () => import(/* webpackChunkName: 'plugins/test-plugin' */ '@js/plugins/Test'),
// ... others
};
if possible it would be nice that specific root reducers transformations will be handled by the plugin itself.
eg. the mapState manipulations should be included in the Map.jsx plugin and applied only if the map plugin is requested but currently they are in the StandardStore.jsx
// this code is currently in the StandardStore of MapStore
// https://github.com/geosolutions-it/MapStore2/blob/53e8f3905b26fb374d93799481abc09e47afc259/web/client/stores/StandardStore.js#L64-L74
let mapState = createHistory(LayersUtils.splitMapAndLayers(mapConfig(state, action)));
let newState = {
...allReducers(state, action),
map: mapState && mapState.map ? map(mapState.map, action) : null,
mapInitialConfig: mapState && mapState.mapInitialConfig || mapState && mapState.loadingError && {
loadingError: mapState.loadingError,
mapId: mapState.loadingError.mapId
} || null,
mapConfigRawData: mapState && mapState.mapConfigRawData || null,
layers: mapState ? layers(mapState.layers, action) : null
};
setState
inside the PluginContainer did not work as expected so I made an override.I tried to apply some fixes with overrides and function wrappers but the results were not consistent due to asynch updates. This experiment was mainly a transposition of the bahaviors of extensions to plugins.
Another point I would like to discuss in depth will all of you is how to slice properly the actual MapStore2 big monster into separated entities (and how many slices are needed to make everyone happy (@giohappy included of course)).
There was a discussion about the struture of SDK internally with @offtherailz and @MV88. These were the first proposal (summary):
I have not a preference on the structure of SDK because I think the two proposals are really similar but I agree that we should find an agreement on the structure.
@allyoucanmap I agree your work is a step further than the lazy-plugins example, I just wanted to check if you think the basic machinery needs any improvement to implement your ideas.
About the redurcers in StandardStore, currently only a very limited set is included there, but we can decide to move also those ones into plugins definitions.
The idea driving the inclusion was: map and layers state is needed in every MapStore application, but now this is not always true: we also want the ability to build pages without any map on it, so it makes sense to move forward and remove the standard reducers from StandardStore. Remember that plugins reducers list is a sort of dependency list, so we should list them not only in Map, but in all the plugins that use them (connect to their state via redux or hooks).
The following high level plan refers to the activities discussed inside "Ideas Round 2" for the new GeoNode Client.
The first steps for the new client includes:
@allyoucanmap and @afabiani please liaise and write down the list of steps to bootstrap the work. @afabiani I've merged the upstream master here. Let's create a new branch for the new client