You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@age.apache.org by hb...@apache.org on 2022/11/01 23:53:55 UTC

[age-viewer] branch main updated: Selecting a graph feature and removed graph path input (#46)

This is an automated email from the ASF dual-hosted git repository.

hbshin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/age-viewer.git


The following commit(s) were added to refs/heads/main by this push:
     new 8073244  Selecting a graph feature and removed graph path input (#46)
8073244 is described below

commit 807324426cbf1163d4219c126a482846ffad15a4
Author: marodins <67...@users.noreply.github.com>
AuthorDate: Tue Nov 1 16:53:50 2022 -0700

    Selecting a graph feature and removed graph path input (#46)
    
    * removed graph input
    
    * sql query to get all graph names in database
    
    * prepare class GraphRepository to handle multiple graphs/graph names
    
    * implemented methods to retreive metadata for multiple graphs of AGE flavor
    
    * refactor metadata retrieval and remove flavor check
    
    * added dropdown and implemented reducer for database slice
    
    * implement graph change method and update state mapping
    
    * select graph feature implemented
    
    * update query
    
    * CodeReview SidebarComponents.jsx
    
    Update to SelectBox
    
    Co-authored-by: Hanbyeol Shin /  David Shin / 신한별 <76...@users.noreply.github.com>
---
 backend/sql/get_graph_names/AGE.sql                |  1 +
 backend/src/models/GraphRepository.js              | 26 +++++---
 backend/src/services/databaseService.js            | 39 ++++++++---
 .../components/contents/presentations/Editor.jsx   |  1 +
 .../frame/presentations/ServerConnectFrame.jsx     |  7 +-
 .../components/sidebar/containers/SidebarHome.js   | 27 ++++----
 .../sidebar/presentations/SidebarComponents.jsx    | 36 +++++++++-
 .../sidebar/presentations/SidebarHome.jsx          | 23 ++++++-
 frontend/src/features/database/DatabaseSlice.js    |  8 ++-
 frontend/src/features/database/MetadataSlice.js    | 76 +++++++++-------------
 10 files changed, 160 insertions(+), 84 deletions(-)

diff --git a/backend/sql/get_graph_names/AGE.sql b/backend/sql/get_graph_names/AGE.sql
new file mode 100644
index 0000000..1ce9ff4
--- /dev/null
+++ b/backend/sql/get_graph_names/AGE.sql
@@ -0,0 +1 @@
+SELECT * FROM ag_catalog.ag_graph;
diff --git a/backend/src/models/GraphRepository.js b/backend/src/models/GraphRepository.js
index a5eaa2e..bd75b4c 100644
--- a/backend/src/models/GraphRepository.js
+++ b/backend/src/models/GraphRepository.js
@@ -16,17 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 import Flavors from '../config/Flavors';
 import PgConfig from '../config/Pg'
 
 import pg from 'pg';
 import types from 'pg-types';
 import {setAGETypes} from '../tools/AGEParser';
+import { getQuery } from '../tools/SQLFlavorManager';
 
 
 class GraphRepository {
-    constructor({host, port, database, graph, user, password, flavor} = {}) {
+    constructor({host, port, database, graph, user, password, flavor, graphs=[]} = {}) {
         if (!flavor) {
             throw new Error('Flavor is required.');
         }
@@ -34,6 +34,7 @@ class GraphRepository {
         this._host = host;
         this._port = port;
         this._database = database;
+        this._graphs = graphs;
         this._graph = graph;
         this._user = user;
         this._password = password;
@@ -44,7 +45,6 @@ class GraphRepository {
                                    host,
                                    port,
                                    database,
-                                   graph,
                                    user,
                                    password,
                                    flavor
@@ -89,6 +89,13 @@ class GraphRepository {
         return result;
     }
 
+    async initGraphNames(){
+        const { rows } = await this.execute(getQuery(this.flavor, 'get_graph_names'));
+        this._graphs = rows.map((item)=>item.name);
+        // set current graph to first name
+        this.setCurrentGraph(this._graphs[0]);
+    }
+    
     /**
      * Get connectionInfo
      */
@@ -97,11 +104,9 @@ class GraphRepository {
             this._pool = GraphRepository.newConnectionPool(this.getPoolConnectionInfo());
         }
         const client = await this._pool.connect();
-        if (this.flavor === 'AGE') {
-            await setAGETypes(client, types);
-        } else {
-            await client.query(`set graph_path = ${this._graph}`);
-        }
+
+        await setAGETypes(client, types);
+
         return client;
     }
 
@@ -149,10 +154,15 @@ class GraphRepository {
             database: this._database,
             user: this._user,
             password: this._password,
+            graphs: this._graphs,
             graph: this._graph,
             flavor: this.flavor,
         };
     }
+
+    setCurrentGraph(name){
+        this._graph = name;
+    }
 }
 
 module.exports = GraphRepository;
diff --git a/backend/src/services/databaseService.js b/backend/src/services/databaseService.js
index 5dbbc7d..48cc43d 100644
--- a/backend/src/services/databaseService.js
+++ b/backend/src/services/databaseService.js
@@ -27,17 +27,40 @@ class DatabaseService {
         this._graphRepository = null;
     }
 
-    async getMetaData() {
+    async getMetaData(graphName) {
+        await this._graphRepository.initGraphNames();
+        const {graphs} = this._graphRepository.getConnectionInfo();
+        if(graphName){
+            if(graphs.includes(graphName)){
+                return await this.getMetaDataSingle(graphName);
+            }else{
+                throw new Error('graph does not exist');
+            }
+            
+        }else{
+            return await this.getMetaDataMultiple(graphs);
+        }
+
+    }
+
+    async getMetaDataMultiple(graphs){
+        const metadata = {};
+        await Promise.all(graphs.map(async(gname)=>{
+            metadata[gname] = await this.getMetaDataSingle(gname);
+        }))
+        return metadata;
+    }
+
+    async getMetaDataSingle(curGraph){
         let metadata = {};
-        
+        const {database} = this.getConnectionInfo();
         try {
-            let connectionInfo = this.getConnectionInfo();
-            let {nodes, edges} = await this.readMetaData();
+            let {nodes, edges} = await this.readMetaData(curGraph);
             metadata.nodes = nodes;
             metadata.edges = edges;
             metadata.propertyKeys = await this.getPropertyKeys();
-            metadata.graph = connectionInfo.graph;
-            metadata.database = connectionInfo.database;
+            metadata.graph = curGraph;
+            metadata.database = database;
             metadata.role = await this.getRole();
         } catch (error) {
             throw error;
@@ -72,9 +95,9 @@ class DatabaseService {
         return queryResult.rows;
     }
     
-    async readMetaData(){
+    async readMetaData(graphName){
         let gr = this._graphRepository;
-        let queryResult = await gr.execute(util.format(getQuery(gr.flavor, 'meta_data'), this.getConnectionInfo().graph));
+        let queryResult = await gr.execute(util.format(getQuery(gr.flavor, 'meta_data'), graphName));
         return this.parseMeta(queryResult[1].rows);
     }
     /* 
diff --git a/frontend/src/components/contents/presentations/Editor.jsx b/frontend/src/components/contents/presentations/Editor.jsx
index 4c66ac1..dafff88 100644
--- a/frontend/src/components/contents/presentations/Editor.jsx
+++ b/frontend/src/components/contents/presentations/Editor.jsx
@@ -55,6 +55,7 @@ const Editor = ({
   };
 
   const onClick = () => {
+    console.log('in editor presentation command is ', command);
     const refKey = uuid();
     if (command.toUpperCase().startsWith(':PLAY')) {
       dispatch(() => addFrame(command, 'Contents', refKey));
diff --git a/frontend/src/components/frame/presentations/ServerConnectFrame.jsx b/frontend/src/components/frame/presentations/ServerConnectFrame.jsx
index 457d439..8e5434d 100644
--- a/frontend/src/components/frame/presentations/ServerConnectFrame.jsx
+++ b/frontend/src/components/frame/presentations/ServerConnectFrame.jsx
@@ -75,14 +75,14 @@ const ServerConnectFrame = ({
       <Row>
         <Col span={6}>
           <h3>Connect to Database</h3>
-          <p>Database access might require and authenticated connection.</p>
+          <p>Database access might require an authenticated connection.</p>
         </Col>
         <Col span={18}>
           <div className={styles.FrameWrapper}>
             <Form
               initialValues={FormInitialValue}
               layout="vertical"
-              onFinish={(values) => connectToDatabase(values)}
+              onFinish={connectToDatabase}
             >
               <Form.Item name="flavor" label="Database Type" rules={[{ required: true }]}>
                 <Select
@@ -101,9 +101,6 @@ const ServerConnectFrame = ({
               <Form.Item name="database" label="Database Name" rules={[{ required: true }]}>
                 <Input placeholder="postgres" />
               </Form.Item>
-              <Form.Item name="graph" label="Graph Path" rules={[{ required: true }]}>
-                <Input placeholder="postgres" />
-              </Form.Item>
               <Form.Item name="user" label="User Name" rules={[{ required: true }]}>
                 <Input placeholder="postgres" />
               </Form.Item>
diff --git a/frontend/src/components/sidebar/containers/SidebarHome.js b/frontend/src/components/sidebar/containers/SidebarHome.js
index 860495b..3bf91bf 100644
--- a/frontend/src/components/sidebar/containers/SidebarHome.js
+++ b/frontend/src/components/sidebar/containers/SidebarHome.js
@@ -21,20 +21,25 @@ import { connect } from 'react-redux';
 import SidebarHome from '../presentations/SidebarHome';
 import { setCommand } from '../../../features/editor/EditorSlice';
 import { addFrame, trimFrame } from '../../../features/frame/FrameSlice';
-import { getMetaData } from '../../../features/database/MetadataSlice';
+import { changeGraph } from '../../../features/database/DatabaseSlice';
+import { getMetaData, changeCurrentGraph } from '../../../features/database/MetadataSlice';
 
-const mapStateToProps = (state) => ({
-  edges: state.metadata.edges,
-  nodes: state.metadata.nodes,
-  propertyKeys: state.metadata.propertyKeys,
-  dbname: state.metadata.dbname,
-  graph: state.metadata.graph,
-  role: state.metadata.role,
-  command: state.editor.command,
-});
+const mapStateToProps = (state) => {
+  const currentGraphData = state.metadata.graphs[state.metadata.currentGraph] || '';
+  return {
+    graphs: Object.entries(state.metadata.graphs).map(([k, v]) => [k, v.id]),
+    edges: currentGraphData.edges,
+    nodes: currentGraphData.nodes,
+    propertyKeys: currentGraphData.propertyKeys,
+    dbname: state.metadata.dbname,
+    status: state.metadata.status,
+    role: currentGraphData.role,
+    command: state.editor.command,
+  };
+};
 
 const mapDispatchToProps = {
-  setCommand, addFrame, trimFrame, getMetaData,
+  setCommand, addFrame, trimFrame, getMetaData, changeCurrentGraph, changeGraph,
 };
 
 export default connect(mapStateToProps, mapDispatchToProps)(SidebarHome);
diff --git a/frontend/src/components/sidebar/presentations/SidebarComponents.jsx b/frontend/src/components/sidebar/presentations/SidebarComponents.jsx
index fcc34a0..644d7fd 100644
--- a/frontend/src/components/sidebar/presentations/SidebarComponents.jsx
+++ b/frontend/src/components/sidebar/presentations/SidebarComponents.jsx
@@ -18,6 +18,7 @@
  */
 
 import React from 'react';
+import { Select } from 'antd';
 import PropTypes from 'prop-types';
 
 const StyleTextRight = {
@@ -75,4 +76,37 @@ SubLabelLeftWithLink.propTypes = {
   label: PropTypes.string.isRequired,
 };
 
-export { SubLabelRight, SubLabelLeft, SubLabelLeftWithLink };
+const GraphSelectDropdown = ({ graphs, changeCurrentGraph, changeGraphDB }) => {
+  const selectStyle = {
+    marginTop: '1rem',
+  };
+  const handleGraphClick = (e) => {
+    const graphName = graphs.find((graph) => graph[1] === e)[0];
+    changeCurrentGraph({ id: e });
+    changeGraphDB({ graphName });
+  };
+
+  const options = (
+    graphs.map(([gname, graphId]) => (<option value={graphId}>{gname}</option>))
+  );
+  return (
+    <div>
+      <Select onChange={handleGraphClick} placeholder="Select Graph" style={selectStyle}>
+        {options}
+      </Select>
+      <b>
+        Current Graph
+      </b>
+    </div>
+  );
+};
+
+GraphSelectDropdown.propTypes = {
+  graphs: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)).isRequired,
+  changeCurrentGraph: PropTypes.func.isRequired,
+  changeGraphDB: PropTypes.func.isRequired,
+};
+
+export {
+  SubLabelRight, SubLabelLeft, SubLabelLeftWithLink, GraphSelectDropdown,
+};
diff --git a/frontend/src/components/sidebar/presentations/SidebarHome.jsx b/frontend/src/components/sidebar/presentations/SidebarHome.jsx
index 80b35bc..5acd6bf 100644
--- a/frontend/src/components/sidebar/presentations/SidebarHome.jsx
+++ b/frontend/src/components/sidebar/presentations/SidebarHome.jsx
@@ -25,7 +25,9 @@ import uuid from 'react-uuid';
 import { connect, useDispatch } from 'react-redux';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faRedo, faTimes } from '@fortawesome/free-solid-svg-icons';
-import { VerticalLine, SubLabelLeft, SubLabelRight } from './SidebarComponents';
+import {
+  VerticalLine, SubLabelLeft, SubLabelRight, GraphSelectDropdown,
+} from './SidebarComponents';
 
 const genLabelQuery = (eleType, labelName, database) => {
   function age() {
@@ -292,12 +294,15 @@ DBMSText.propTypes = {
 const SidebarHome = ({
   edges,
   nodes,
+  graphs,
   propertyKeys,
   setCommand,
   command,
   trimFrame,
   addFrame,
   getMetaData,
+  changeCurrentGraph,
+  changeGraph,
 }) => {
   const dispatch = useDispatch();
   const { confirm } = Modal;
@@ -378,6 +383,19 @@ const SidebarHome = ({
           </button>
           <br />
           <b>Close Session</b>
+          <div style={{
+            border: '1px solid #C4C4C4',
+            opacity: '1',
+            width: '80%',
+            height: '0',
+            margin: '3px auto',
+          }}
+          />
+          <GraphSelectDropdown
+            graphs={graphs}
+            changeCurrentGraph={changeCurrentGraph}
+            changeGraphDB={changeGraph}
+          />
         </div>
       </div>
     </div>
@@ -402,6 +420,9 @@ SidebarHome.propTypes = {
   trimFrame: PropTypes.func.isRequired,
   addFrame: PropTypes.func.isRequired,
   getMetaData: PropTypes.func.isRequired,
+  changeCurrentGraph: PropTypes.func.isRequired,
+  graphs: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)).isRequired,
+  changeGraph: PropTypes.func.isRequired,
 };
 
 export default SidebarHome;
diff --git a/frontend/src/features/database/DatabaseSlice.js b/frontend/src/features/database/DatabaseSlice.js
index 04c6efe..7955100 100644
--- a/frontend/src/features/database/DatabaseSlice.js
+++ b/frontend/src/features/database/DatabaseSlice.js
@@ -78,6 +78,10 @@ const DatabaseSlice = createSlice({
     status: 'init',
   },
   reducers: {
+    changeGraph: (state, action) => ({
+      ...state,
+      graph: action.payload.graphName,
+    }),
   },
   extraReducers: {
     [connectToDatabase.fulfilled]: (state, action) => ({
@@ -132,8 +136,6 @@ const DatabaseSlice = createSlice({
     }),
   },
 });
+export const { changeGraph } = DatabaseSlice.actions;
 
-/*
-export const { } = DatabaseSlice.actions
-*/
 export default DatabaseSlice.reducer;
diff --git a/frontend/src/features/database/MetadataSlice.js b/frontend/src/features/database/MetadataSlice.js
index 0e37d0a..9158c24 100644
--- a/frontend/src/features/database/MetadataSlice.js
+++ b/frontend/src/features/database/MetadataSlice.js
@@ -18,6 +18,7 @@
  */
 
 import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
+import uuid from 'react-uuid';
 
 export const getMetaData = createAsyncThunk(
   'database/getMetaData',
@@ -25,20 +26,21 @@ export const getMetaData = createAsyncThunk(
     try {
       const response = await fetch('/api/v1/db/meta');
       if (response.ok) {
-        let allCountEdge = 0;
-        let allCountNode = 0;
         const ret = await response.json();
+        Object.keys(ret).forEach((gname) => {
+          let allCountEdge = 0;
+          let allCountNode = 0;
+          ret[gname].nodes.forEach((item) => {
+            allCountNode += item.cnt;
+          });
 
-        ret.nodes.forEach((item) => {
-          allCountNode += item.cnt;
+          ret[gname].edges.forEach((item) => {
+            allCountEdge += item.cnt;
+          });
+          ret[gname].nodes.unshift({ label: '*', cnt: allCountNode });
+          ret[gname].edges.unshift({ label: '*', cnt: allCountEdge });
+          ret[gname].id = uuid();
         });
-
-        ret.edges.forEach((item) => {
-          allCountEdge += item.cnt;
-        });
-
-        ret.nodes.unshift({ label: '*', cnt: allCountNode });
-        ret.edges.unshift({ label: '*', cnt: allCountEdge });
         return ret;
       }
       throw response;
@@ -76,55 +78,35 @@ export const getMetaChartData = createAsyncThunk(
 const MetadataSlice = createSlice({
   name: 'metadata',
   initialState: {
-    edges: [],
-    nodes: [],
-    propertyKeys: [],
+    graphs: {},
     status: 'init',
     dbname: '',
-    graph: '',
-    role: {
-      user_name: '',
-      role_name: '',
-    },
-    rows: [],
+    currentGraph: '',
   },
   reducers: {
-    resetMetaData: () => ({
-      edges: [],
-      nodes: [],
-      propertyKeys: [],
-      status: 'init',
-      dbname: '',
-      graph: '',
-      role: {
-        user_name: '',
-        role_name: '',
-      },
-
+    resetMetaData: (state) => (state.initialState),
+    changeCurrentGraph: (state, action) => ({
+      ...state,
+      currentGraph: Object.entries(state.graphs)
+        .find(([, data]) => data.id === action.payload.id)[0],
     }),
   },
   extraReducers: {
     [getMetaData.fulfilled]: (state, action) => {
       if (action.payload) {
-        return Object.assign(state, {
-          edges: action.payload.edges,
-          nodes: action.payload.nodes,
-          propertyKeys: action.payload.propertyKeys,
+        return {
+          ...state,
+          graphs: action.payload,
           status: 'connected',
           dbname: action.payload.database,
-          graph: action.payload.graph,
-          role: action.payload.role,
-        });
+          currentGraph: state.currentGraph !== '' ? state.currentGraph : Object.keys(action.payload)[0],
+        };
       }
-      return Object.assign(state, {
-        edges: [],
-        nodes: [],
-        propertyKeys: [],
+      return {
+        ...state,
         status: 'disconnected',
         dbname: action.payload.database,
-        graph: action.payload.graph,
-        role: action.payload.role,
-      });
+      };
     },
     [getMetaChartData.fulfilled]: (state, action) => {
       if (action.payload) {
@@ -135,6 +117,6 @@ const MetadataSlice = createSlice({
   },
 });
 
-export const { resetMetaData } = MetadataSlice.actions;
+export const { resetMetaData, changeCurrentGraph } = MetadataSlice.actions;
 
 export default MetadataSlice.reducer;