From 6c47c57a7668ca353bed5f8af317ae73a5239cef Mon Sep 17 00:00:00 2001
From: esikkala <esko.ikkala@aalto.fi>
Date: Wed, 10 Jun 2020 19:21:54 +0300
Subject: [PATCH] Add network actions from AcademySampo

---
 src/client/actions/index.js                   | 27 +++++++-
 .../components/facet_results/Network.js       | 67 ++++++++++++++-----
 src/client/containers/SemanticPortal.js       | 14 +++-
 src/client/reducers/sampo/perspective1.js     |  1 +
 src/client/reducers/sampo/perspective2.js     |  1 +
 src/client/reducers/sampo/perspective3.js     |  1 +
 src/server/index.js                           | 18 +++++
 src/server/openapi.yaml                       | 32 ++++++---
 src/server/sparql/NetworkApi.js               | 13 ++--
 9 files changed, 140 insertions(+), 34 deletions(-)

diff --git a/src/client/actions/index.js b/src/client/actions/index.js
index 0a917b5a..5530b2b4 100644
--- a/src/client/actions/index.js
+++ b/src/client/actions/index.js
@@ -16,8 +16,11 @@ export const FETCH_BY_URI = 'FETCH_BY_URI'
 export const FETCH_BY_URI_FAILED = 'FETCH_BY_URI_FAILED'
 export const FETCH_SIMILAR_DOCUMENTS_BY_ID = 'FETCH_SIMILAR_DOCUMENTS_BY_ID'
 export const FETCH_SIMILAR_DOCUMENTS_BY_ID_FAILED = 'FETCH_SIMILAR_DOCUMENTS_BY_ID_FAILED'
+export const FETCH_NETWORK_BY_ID = 'FETCH_NETWORK_BY_ID'
+export const FETCH_NETWORK_BY_ID_FAILED = 'FETCH_NETWORK_BY_ID_FAILED'
 export const UPDATE_INSTANCE = 'UPDATE_INSTANCE'
 export const UPDATE_INSTANCE_RELATED_DATA = 'UPDATE_INSTANCE_RELATED_DATA'
+export const UPDATE_INSTANCE_NETWORK_DATA = 'UPDATE_INSTANCE_NETWORK_DATA'
 export const FETCH_FACET = 'FETCH_FACET'
 export const FETCH_FACET_CONSTRAIN_SELF = 'FETCH_FACET_CONSTRAIN_SELF'
 export const FETCH_FACET_FAILED = 'FETCH_FACET_FAILED'
@@ -61,10 +64,12 @@ export const fetchPaginatedResultsFailed = (resultClass, error, message) => ({
   error,
   message
 })
-export const fetchResults = ({ resultClass, facetClass }) => ({
+export const fetchResults = ({ resultClass, facetClass, limit = null, optimize = null }) => ({
   type: FETCH_RESULTS,
   resultClass,
-  facetClass
+  facetClass,
+  limit,
+  optimize
 })
 export const fetchResultCount = ({ resultClass, facetClass }) => ({
   type: FETCH_RESULT_COUNT,
@@ -148,6 +153,19 @@ export const fetchSimilarDocumentsById = ({ resultClass, id, modelName, resultSi
   modelName,
   resultSize
 })
+export const fetchNetworkById = ({ resultClass, id, limit = null, optimize = null }) => ({
+  type: FETCH_NETWORK_BY_ID,
+  resultClass,
+  id,
+  limit,
+  optimize
+})
+export const fetchNetworkByIdFailed = ({ resultClass, id, error, message }) => ({
+  type: FETCH_NETWORK_BY_ID_FAILED,
+  resultClass,
+  error,
+  message
+})
 export const fetchSimilarDocumentsByIdFailed = (resultClass, id, error, message) => ({
   type: FETCH_SIMILAR_DOCUMENTS_BY_ID_FAILED,
   resultClass,
@@ -166,6 +184,11 @@ export const updateInstanceRelatedData = ({ resultClass, data }) => ({
   resultClass,
   data
 })
+export const updateInstanceNetworkData = ({ resultClass, data }) => ({
+  type: UPDATE_INSTANCE_NETWORK_DATA,
+  resultClass,
+  data
+})
 export const fetchFacet = ({ facetClass, facetID }) => ({
   type: FETCH_FACET,
   facetClass,
diff --git a/src/client/components/facet_results/Network.js b/src/client/components/facet_results/Network.js
index 9ecf0973..1fe17c9f 100644
--- a/src/client/components/facet_results/Network.js
+++ b/src/client/components/facet_results/Network.js
@@ -1,13 +1,14 @@
 import React from 'react'
 import PropTypes from 'prop-types'
 import { withStyles } from '@material-ui/core/styles'
+import history from '../../History'
 import cytoscape from 'cytoscape'
 
 const styles = theme => ({
   root: {
     height: 400,
     [theme.breakpoints.up('md')]: {
-      height: 'calc(100% - 72px)'
+      height: 'calc(100% - 21px)'
     }
   },
   cyContainer: {
@@ -29,7 +30,7 @@ const layout = {
   edgeElasticity: 100,
   nestingFactor: 5,
   gravity: 80,
-  numIter: 1000,
+  numIter: 1347,
   initialTemp: 200,
   coolingFactor: 0.95,
   minTemp: 1.0
@@ -42,10 +43,21 @@ class Network extends React.Component {
   }
 
   componentDidMount = () => {
-    this.props.fetchResults({
-      resultClass: this.props.resultClass,
-      facetClass: this.props.facetClass
-    })
+    if (this.props.pageType === 'instancePage') {
+      this.props.fetchNetworkById({
+        resultClass: this.props.resultClass,
+        id: this.props.id,
+        limit: this.props.limit,
+        optimize: this.props.optimize
+      })
+    } else {
+      this.props.fetchResults({
+        resultClass: this.props.resultClass,
+        facetClass: this.props.facetClass,
+        limit: this.props.limit,
+        optimize: this.props.optimize
+      })
+    }
     this.cy = cytoscape({
       container: this.cyRef.current,
 
@@ -53,37 +65,53 @@ class Network extends React.Component {
         {
           selector: 'node',
           style: {
-            'background-color': ele => ele.data('class') === 'http://erlangen-crm.org/efrbroo/F4_Manifestation_Singleton'
-              ? '#666' : '#000',
-            label: 'data(prefLabel)'
+            shape: 'ellipse',
+            'font-size': '12',
+            'background-color': ele => ele.data('color') || '#666',
+            label: ' data(prefLabel)',
+            height: ele => (ele.data('size') || 16 / (ele.data('distance') + 1) || '16px'),
+            width: ele => (ele.data('size') || 16 / (ele.data('distance') + 1) || '16px')
           }
         },
         {
           selector: 'edge',
           style: {
-            // 'width': 'data(weight)',
-            'line-color': '#999',
+            width: ele => ele.data('weight') || 1,
+            'line-color': ele => ele.data('color') || '#BBB',
             'curve-style': 'bezier',
-            content: 'data(prefLabel)',
+            content: ' data(prefLabel) ',
             'target-arrow-shape': 'triangle',
             'target-arrow-color': '#999',
             color: '#555',
-            'font-size': '9',
+            'font-size': '6',
             'text-valign': 'top',
             'text-halign': 'center',
             'edge-text-rotation': 'autorotate',
             'text-background-opacity': 1,
-            'text-background-color': '#FFF',
+            'text-background-color': 'white',
             'text-background-shape': 'roundrectangle'
           }
         }
       ]
     })
+
+    this.cy.on('tap', 'node', function () {
+      try {
+        if (this.data('href')) {
+          // console.log(this.data('href'))
+          history.push(this.data('href'))
+        }
+      } catch (e) { // fall back on url change
+        console.log('Fail', e)
+        console.log(this.data())
+      }
+    })
   }
 
   componentDidUpdate = prevProps => {
     if (prevProps.resultUpdateID !== this.props.resultUpdateID) {
       // console.log(this.props.results.elements);
+      this.cy.elements().remove()
       this.cy.add(this.props.results.elements)
       this.cy.layout(layout).run()
     }
@@ -108,11 +136,14 @@ class Network extends React.Component {
 Network.propTypes = {
   classes: PropTypes.object.isRequired,
   results: PropTypes.object,
-  fetchResults: PropTypes.func.isRequired,
+  fetchResults: PropTypes.func,
+  fetchNetworkById: PropTypes.func,
   resultClass: PropTypes.string.isRequired,
-  facetClass: PropTypes.string.isRequired,
-  facetUpdateID: PropTypes.number.isRequired,
-  resultUpdateID: PropTypes.number.isRequired
+  facetClass: PropTypes.string,
+  facetUpdateID: PropTypes.number,
+  resultUpdateID: PropTypes.number.isRequired,
+  limit: PropTypes.number.isRequired,
+  optimize: PropTypes.number.isRequired
 }
 
 export default withStyles(styles)(Network)
diff --git a/src/client/containers/SemanticPortal.js b/src/client/containers/SemanticPortal.js
index 39d6bc62..c232f4d9 100644
--- a/src/client/containers/SemanticPortal.js
+++ b/src/client/containers/SemanticPortal.js
@@ -42,6 +42,7 @@ import {
   fetchFullTextResults,
   clearResults,
   fetchByURI,
+  fetchNetworkById,
   fetchFacet,
   fetchFacetConstrainSelf,
   clearFacet,
@@ -417,10 +418,13 @@ const SemanticPortal = props => {
                                   <InstanceHomePage
                                     rootUrl={rootUrlWithLang}
                                     fetchByURI={props.fetchByURI}
+                                    fetchNetworkById={props.fetchNetworkById}
                                     resultClass={perspective.id}
+                                    resultUpdateID={props[perspective.id].resultUpdateID}
                                     properties={props[perspective.id].properties}
                                     tabs={perspective.instancePageTabs}
                                     data={props[perspective.id].instance}
+                                    networkData={props[perspective.id].instanceNetworkData}
                                     sparqlQuery={props[perspective.id].instanceSparqlQuery}
                                     isLoading={props[perspective.id].fetching}
                                     routeProps={routeProps}
@@ -441,7 +445,7 @@ const SemanticPortal = props => {
             {perspectiveConfigOnlyInfoPages.map(perspective =>
               <Switch key={perspective.id}>
                 <Redirect
-                  from={`/${perspective.id}/page/:id`}
+                  from={`${rootUrl}/${perspective.id}/page/:id`}
                   to={`${rootUrlWithLang}/${perspective.id}/page/:id`}
                 />
                 <Route
@@ -466,10 +470,13 @@ const SemanticPortal = props => {
                             <InstanceHomePage
                               rootUrl={rootUrlWithLang}
                               fetchByURI={props.fetchByURI}
+                              fetchNetworkById={props.fetchNetworkById}
                               resultClass={perspective.id}
+                              resultUpdateID={props[perspective.id].resultUpdateID}
                               properties={props[perspective.id].properties}
                               tabs={perspective.instancePageTabs}
                               data={props[perspective.id].instance}
+                              networkData={props[perspective.id].instanceNetworkData}
                               sparqlQuery={props[perspective.id].instanceSparqlQuery}
                               isLoading={props[perspective.id].fetching}
                               routeProps={routeProps}
@@ -597,6 +604,7 @@ const mapDispatchToProps = ({
   fetchFacetConstrainSelf,
   clearFacet,
   fetchGeoJSONLayers,
+  fetchNetworkById,
   fetchGeoJSONLayersBackend,
   clearGeoJSONLayers,
   sortResults,
@@ -686,6 +694,10 @@ SemanticPortal.propTypes = {
    * Redux action for fetching information about a single entity.
    */
   fetchByURI: PropTypes.func.isRequired,
+  /**
+   * Redux action for fetching network of a single entity.
+   */
+  fetchNetworkById: PropTypes.func.isRequired,
   /**
    * Redux action for loading external GeoJSON layers.
    */
diff --git a/src/client/reducers/sampo/perspective1.js b/src/client/reducers/sampo/perspective1.js
index 15fdbd42..fedb1d7b 100644
--- a/src/client/reducers/sampo/perspective1.js
+++ b/src/client/reducers/sampo/perspective1.js
@@ -36,6 +36,7 @@ export const INITIAL_STATE = {
   paginatedResults: [],
   paginatedResultsSparqlQuery: null,
   instance: null,
+  instanceNetworkData: null,
   instanceSparqlQuery: null,
   resultCount: 0,
   page: -1,
diff --git a/src/client/reducers/sampo/perspective2.js b/src/client/reducers/sampo/perspective2.js
index 1159e8f2..b1ab409a 100644
--- a/src/client/reducers/sampo/perspective2.js
+++ b/src/client/reducers/sampo/perspective2.js
@@ -35,6 +35,7 @@ export const INITIAL_STATE = {
   paginatedResults: [],
   paginatedResultsSparqlQuery: null,
   instance: null,
+  instanceNetworkData: null,
   instanceSparqlQuery: null,
   resultCount: 0,
   page: -1,
diff --git a/src/client/reducers/sampo/perspective3.js b/src/client/reducers/sampo/perspective3.js
index b303797c..a96bf18b 100644
--- a/src/client/reducers/sampo/perspective3.js
+++ b/src/client/reducers/sampo/perspective3.js
@@ -35,6 +35,7 @@ export const INITIAL_STATE = {
   paginatedResults: [],
   paginatedResultsSparqlQuery: null,
   instance: null,
+  instanceNetworkData: null,
   instanceSparqlQuery: null,
   resultCount: 0,
   page: -1,
diff --git a/src/server/index.js b/src/server/index.js
index 4fbe8f2d..08285c92 100644
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -168,6 +168,24 @@ new OpenApiValidator({
       }
     })
 
+    app.get(`${apiPath}/:resultClass/network/:id`, async (req, res, next) => {
+      const { params, query } = req
+      try {
+        const data = await getByURI({
+          backendSearchConfig,
+          resultClass: params.resultClass,
+          uri: params.id,
+          limit: query.limit,
+          optimize: query.optimize,
+          constraints: null,
+          resultFormat: 'json'
+        })
+        res.json(data)
+      } catch (error) {
+        next(error)
+      }
+    })
+
     app.post(`${apiPath}/faceted-search/:facetClass/facet/:id`, async (req, res, next) => {
       const { params, body } = req
       try {
diff --git a/src/server/openapi.yaml b/src/server/openapi.yaml
index e4107a5a..2dac21ea 100644
--- a/src/server/openapi.yaml
+++ b/src/server/openapi.yaml
@@ -118,15 +118,6 @@ paths:
             application/json:
               schema: 
                 type: object
-                properties: 
-                  data: 
-                    type: array
-                    items: 
-                      type: object
-                    description: Results as an array of objects
-                  sparqlQuery:
-                    type: string
-                    description: The SPARQL query that was used for the results
     get:
         summary: Return all search results as a CSV file
         responses:
@@ -292,6 +283,29 @@ paths:
                   sparqlQuery:
                     type: string
                     description: The SPARQL query that was used for retrieving the metadata
+  /{resultClass}/network/{id}:
+    get: 
+      summary: Return a network of a single resource
+      parameters:
+        - in: path
+          name: resultClass
+          schema: 
+            type: string
+          required: true
+          description: The class of the resource
+        - in: path
+          name: id
+          schema: 
+            type: string
+          required: true
+          description: The URI of the resource
+      responses:
+        '200':
+          description: Network data
+          content:
+            application/json:
+              schema: 
+                type: object
   /full-text-search:
     get:
       summary: Full text search
diff --git a/src/server/sparql/NetworkApi.js b/src/server/sparql/NetworkApi.js
index 9273bcfe..dc73f2b5 100644
--- a/src/server/sparql/NetworkApi.js
+++ b/src/server/sparql/NetworkApi.js
@@ -4,17 +4,22 @@ export const runNetworkQuery = async ({
   endpoint,
   prefixes,
   links,
-  nodes
+  limit,
+  nodes,
+  id,
+  optimize
 }) => {
   const payload = {
     endpoint,
     prefixes,
     links,
     nodes,
-    limit: 500
-    // id: 'http://ldf.fi/mmm/actor/bodley_person_51697938'
+    limit,
+    id,
+    optimize,
+    customHttpHeaders: { Authorization: `Basic ${process.env.SPARQL_ENDPOINT_BASIC_AUTH}` }
   }
-  const url = 'http://127.0.0.1:5000/query'
+  const url = 'https://sparql-network.demo.seco.cs.aalto.fi/query' // 'http://127.0.0.1:5000/query'
   const config = {
     headers: {
       'Content-Type': 'application/json'
-- 
GitLab