Skip to content

Latest commit

 

History

History
200 lines (150 loc) · 17.5 KB

.README.md

File metadata and controls

200 lines (150 loc) · 17.5 KB

SEACRIFOG

This is a tool for exploring the inventory of carbon-related observation infrastructure. There are numerous metadata repositories describing, and linking to, datasets related to carbon measurement in some way or another. These datasets are rich, but not easily discovered by existing search tools such as Google Search.

The prototype (currently available at https://seacrifog.saeon.ac.za) is aimed at providing an interactive overview of the infrastructure that supports carbon measurements. Users can select/deselect various elements of the carbon observation infrastructure, which serves the dual purpose of providing detailed information on individual, selected components of the system, and also constraining search criteria that can be applied against various organizations’ metadata repositories across the world (providing these organizations make their repositories electronically searchable, which many do).

The prototype consists of a pair of software applications:

  • A long running HTTP server that provides a publicly available API for interacting with the data representing the carbon observation platform model, and that acts as an adapter for specifying metadata-searches constrained by some selection of the platform entities
  • A browser client (website) that provides a richly interactive UI for interacting with the API.

The browser client is tightly coupled with the API logic. The API, however, can stand as a useful publicly available service in it’s own right.

Tech stack

  • Database
    • PostGIS
  • API
    • Node.js (server-side JavaScript framework)
    • Express (web application framework)
    • GraphQL (express-graphql)
    • Node Postgres (database adapter)
  • Browser client
    • ESNext (Babel, Webpack pre-compilation and bundling)
    • React
    • Apollo Client (GraphQL provider)
    • React-MD (MIT licensed Material Design component library implementation)

Data model

alt text

API

The API provides HTTP endpoints, and a GraphQL interface. For the most part the HTTP endpoints are just stubs - they don't provide any real value at this point, but are instead a proof of concept that a GraphQL and RESTful API can share the data access layer completely (so it's fairly straightforward to provide both).

Using the API

The GraphQL API can be consumed via standard HTTP requests, with the request body a string representing a valid GraphQL query. A GraphQL IDE is available HERE. Below are some examples on how to fetch site-data from the API

# Fetch all sites that are located within the Africa region
curl -X POST https://api.seacrifog.saeon.ac.za/graphql -H "Content-Type: application/json" -d '{ "query": "{ sites { id name xyz } } "}'

# The above cURL command is implicitly the same as specifying the bounding box of "POLYGON((-26 -40,-26 38,64 38,64 -40,-26 -40))" . i.e. this cURL command should give the same results as the first one:
curl -X POST https://api.seacrifog.saeon.ac.za/graphql -H "Content-Type: application/json" -d '{ "query": "{ sites( extent: \"POLYGON((-26 -40,-26 38,64 38,64 -40,-26 -40))\") { id name xyz } } "}'

# To specify a different extent - i.e. the whole planet, a suitable extent can be specified ("POLYGON((-180 -90, -180 90, 180 90, 180 -90, -180 -90))"):
curl -X POST https://api.seacrifog.saeon.ac.za/graphql -H "Content-Type: application/json" -d '{ "query": "{ sites( extent: \"POLYGON((-180 -90, -180 90, 180 90, 180 -90, -180 -90))\") { id name xyz } } "}'

For these example, the contract of the API is such that the extent argument accepts text that is valid WKT. This is validated. It's difficult to validate the projection used. So the contract is that projection 4326 is the correct projection. WKT of a different projection will give strange results.

Integrations need to be specified by a user in two places. These are:

  1. Logic for polling network/site information from an endpoint - this is currently in the form of a JavaScript function that is executed on a scheduled interval. An example of the integration with ICOS is included in the source code. Currently the source code of the API needs to be adjusted to include further integrations - but this is a straightforward change to make in the future.
  2. Search logic needs to be specified per organization as a JavaScript function - executors. An example of the function contract is included in the source code. These functions are executed as child processes to the main Node.js process. Currently only JavaScript executors are supported, but it would be fairly straightforward to allow for interoperability between the API and executors in a variety of programming languages. To add a new executor, add an appropriate function to the source code and then redeploy the application.

Data access is directly via SQL using the Node Postgres PostgreSQL client, with a thin wrapper over the query functionality to handle connection pooling (hopefully) correctly. GraphQL APIs require request level batching optimization even from the very beginning - due to the logic of how GraphQL queries are resolved - this is implemented as is typically done via the DataLoader library. All future work on the data access layer needs to implement database queries via this pattern - there are many references WRT to how to use DataLoader in the context of this project.

Client

The client is an SPA (Single Page Application), such is typical of React.js client apps. Architecturally, the client is organized conceptually of 'pages', each page comprising one or more 'modules'. Observational infrastructure is organized according to entity 'class'. For each entity class there is a page that lists all entities of that type (a list/explorer page), and an overview page that allows for seeing and editing a single entity. For example, all the entities of type Variable can be found on the HTTP path /variables, listed and searchable in a table. A single variable can be viewed and edited on the /variable/:id path. There is an exception - the /sites route displays a map of sites, along with proof-of-concept visualization charts. Individual sites can be edited on the /networks/:id path (sites of a particular network can be edited). Below is a representation of the site map:

.
├── /sites
├── /networks
│   └── /networks/:id
├── /variables
│   └── /variables/:id
├── /protocols
│   └── /protocols/:id
└── /search-results

Modules

The concept of modules WRT the client refers to reusable react components. There is no definite difference between a component and a module in the context of SEACRIFOG. Essentially at some point a component is considered large enough to be a module, or sometimes modules export a number of related components. These are defined in client/src/modules.

The map is provided by OpenLayers 6, utilizing an API provided by a thin React.js wrapper library - @saeon/ol-react - authored by SAEON (at the time of writing there are no well-maintained OpenLayers 6 React.js wrapping libraries) and made available as MIT-licensed open source code. OpenLayers in the context of a JavaScript application is just a single object olMap. This object keeps it's own internal state and handles interactions internally. The @saeon/ol-react wrapper layer essentially provides the means of mapping React state to olMap internal state. This is achieved via using the ECMAScript Proxy objects API. Note that this is incompatible with Internet Explorer, and not possible to polyfill. This tool as it currently exists should, however, work on Internet Explorer 11 and upwards only because no advanced layer management is used. This will obviously not be the case with further development. In addition to the layer proxy, the Atlas module provides a means of selecting/deselecting map features, and also for specifying layers.

Throughout the client the @saeon/ol-react component is used directly. The Atlas module consists of map-related exports that are reused wherever maps are shown (these maps use the same layers, styles, configurable sources, etc.).

A simple component that wraps Apollo Client's useMutation hook.

A simple component that wraps Apollo Client's useQuery hook.

A collection of components that are the basis of the 'editor' pages (/networks/:id, /variables/:id, and /protocols/:id). The components include headers, input field formatters, etc.

Typically web forms are bound to some model - often referred to as 'form model binding'. This conceptually allows for representation of some table/object state as an appropriate input field. Similarly, this concept is utilized in SEACRIFOG. All the edit pages make use of UI logic to draw editable forms from JavaScript object (and provide a means of saving them to the database via GraphQL mutations).

A collection of components that are the basis of the list/explorer pages (/networks, /variables, and /protocols). The components include headers, buttons, user-feedback messages, etc.

A collection of components that are used to draw the SPA. These include a <Footer /> component that is used on most pages as well as navigation-related components that are typically only used once as 'parents' to other components used throughout the application. HTML path-based routing is handled by the react-router library.

State is managed in three ways across the application:

A single global state module is used to keep track of user interactions across the app (selecting/deselecting of items). As entities are toggled a background search is performed for all currently selected search criteria - the results are stored in client memory.

For the most part, components are used directly as provided by the React-MD library - already a significant amount of work in terms of crafting reusable components! However there are a few cases in this UI that 'grouped element trees' are reused in multiple places throughout the application. These include:

  • User-feedback messages (kept in a single place for consistency)
  • A controlled table that supports searching, sorting, and selecting rows (controlled meaning that state is handled by a parent component, so that selecting rows can update the global state)
  • A filterable list of items that can be selected/deselected - also controlled
  • The Side filter component used throughout the application. This component combines many instances of the DropdownSelect component, along with controlling callbacks to update the global state module. This component is used on most pages - it provides direct access to the the current global state in terms of what is being filtered
  • ChartStateManagement - Interactive charts are shown as a proof of concept. The API currently requires that management of state is done via context
  • A form component - simple to place anywhere in the component tree, and provides localized state management for all elements in the sub tree.

Pages

The concept of pages WRT the client refers to what is displayed at any particular URI. Pages comprise modules. These are defined in client/src/pages.

Static information mostly - partner logos are shown on this page.

The map is interactive in that it allows for assessing which variables are measured at which sites (or groups of sites) - this is achieved by clicking features on the atlas, that will both add selected sites to the metadata filter, and trigger charts (provided by eCharts) to display.

These routes display the list/explorer pages. Mostly the pages make use of the reusable components that comprise the ExplorerPage module.

These routes display editor pages for the various entities, utilizing reusable components that form the EditorPage module.

The search results page comprises a tabbed layout with the content of the tab a list of search results. This is easy to see via a visual representation of the element tree that is rendered on this page:

.
├── Toolbar header
└── Tab container
    ├── Tab content (org 1)
    │   └── Virtualized list (handles many records)
    │       ├── RecordViewer
    │       │   └── OrgRenderer
    │       ├── RecordViewer
    │       │   └── OrgRenderer
    │       └── (many more records) ...
    ├── Tab content (org 2)
    │   └── Virtualized list
    │       ├── RecordViewer
    │       │   └── OrgRenderer
    │       ├── RecordViewer
    │       │   └── OrgRenderer
    │       └── (many more records) ...
    └── ...

The OrgRenderer object is passed as properties to the RecordViewer component. A list of OrgRenderer objects is provided as configuration to the item renderer (<RecordViewer org={OrgRenderedObj} />). Loosely speaking this pattern is referred to as dependency injection.

Renderer objects comprise a variety of callbacks that are passed individual records. These callbacks need to be user-defined to return the correct information per field. The configuration file shows all the organizations that have been integrated into SEACRIFOG. Further work on SEACRIFOG could involve providing a means of editing the configuration object from a web UI - this would allow organizations to 'register' how their metadata records should be displayed. (Note that a similar registration process would need to be implemented on the API so that users could also define how any organization could be searched).

Deployment

For a simple setup (PostGIS, the API and the Client all served from a single server), deploy the Node.js API, PostGIS database, and React client directly from the root of this repository using the provided docker-compose.yml file. For an example of how to configure an automated deployment pipeline using GitHub Actions (with additional API and client configuration), refer to the GitHub Actions workflow configurations in this repository.

echo "POSTGRES_PASSWORD=PASSWORD" > ./.env
docker-compose up -d --force-recreate --build

Current deployment information (as of February 2020)

  • PostGIS: Served via a Docker container (mdillon/postgis Docker image)
  • API: Docker container (refer to the Dockerfile in the source code)
  • Browser client: Docker container (refer to the Dockerfile in the source code)
  • Server: Single CentOS 7 virtual machine (2 cores, 2GB RAM, 60GB)

DEVELOPER DOCUMENTATION

This repository contains two separate applications - a client and and API. Dependencies are NOT shared between these projects. Setup the project after cloning this repository via the following steps:

Install project wide dependencies

npm install

Install dependencies for the client and API (this is also mentioned below)

NOTE: these projects also need configuration - refer to the documentation below.

npm --prefix api/ install
npm --prefix client/ install

Start the API and client together

This is a helpful script that will start the API and client in the same terminal window. Alternatively you can start the API and client from the root of their respective directories.

npm start