You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@age.apache.org by ak...@apache.org on 2023/01/27 21:20:29 UTC

[age-viewer] branch main updated: Query Generator Feature (#102)

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

ako 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 7680c93  Query Generator Feature (#102)
7680c93 is described below

commit 7680c9383944cd0ba725ae2e214ff6cbbf7f354f
Author: marodins <67...@users.noreply.github.com>
AuthorDate: Fri Jan 27 13:20:23 2023 -0800

    Query Generator Feature (#102)
    
    * implement data structure
    
    * parse matrix
    
    * implement frontend matrix parser
    
    * create component modules
    
    * retrieve query keyword list and implement drawer
    
    * update component styles for consistency
    
    * parsing input and refactor data structure
    
    * query builder components and interaction with editor implemented
    
    * clean up log statements
    
    * fix frame minimization bug and remove unnecessary module
    
    * comment out unused code segment
---
 backend/misc/graph_kw.csv                          | 15 ++++
 backend/src/app.js                                 |  2 +
 backend/src/routes/miscellaneous.js                |  9 +++
 backend/src/services/queryList.js                  | 38 +++++++++
 .../components/contents/presentations/Contents.jsx |  2 +-
 .../components/contents/presentations/Editor.jsx   | 12 +--
 .../components/query_builder/BuilderContainer.jsx  | 94 ++++++++++++++++++++++
 .../components/query_builder/BuilderContainer.scss | 16 ++++
 .../components/query_builder/BuilderSelection.jsx  | 38 +++++++++
 .../template/presentations/DefaultTemplate.jsx     | 41 ++++++++--
 .../template/presentations/DefaultTemplate.scss    | 13 +++
 .../src/features/query_builder/KeyWordFinder.js    | 45 +++++++++++
 frontend/src/static/style.css                      |  7 +-
 13 files changed, 316 insertions(+), 16 deletions(-)

diff --git a/backend/misc/graph_kw.csv b/backend/misc/graph_kw.csv
new file mode 100644
index 0000000..79e35d2
--- /dev/null
+++ b/backend/misc/graph_kw.csv
@@ -0,0 +1,15 @@
+,MATCH,WITH,DELETE,CREATE,RETURN,ORDER BY,SKIP,LIMIT,SET,REMOVE,MERGE,AS,WHERE,DETACH
+MATCH,0,1,1,0,1,0,0,0,0,1,0,0,0,0
+WITH,0,0,0,0,0,1,0,0,0,0,0,1,1,0
+DELETE,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+CREATE,0,0,0,0,1,0,0,0,0,0,0,0,0,0
+RETURN,0,0,0,0,1,0,0,0,0,0,0,0,0,0
+ORDER BY,0,0,0,0,1,0,1,1,0,0,0,0,0,0
+SKIP,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+LIMIT,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+SET,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+REMOVE,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+MERGE,0,0,0,0,1,0,0,0,0,0,0,0,0,0
+AS,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+WHERE,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+DETACH,0,0,1,0,0,0,0,0,0,0,0,0,0,0
diff --git a/backend/src/app.js b/backend/src/app.js
index a6bbfad..8141c91 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -27,6 +27,7 @@ const {stream} = require('./config/winston');
 const cypherRouter = require('./routes/cypherRouter');
 const databaseRouter = require('./routes/databaseRouter');
 const sessionRouter = require('./routes/sessionRouter');
+const miscellaneousRouter = require('./routes/miscellaneous');
 const app = express();
 
 app.use(cors({
@@ -56,6 +57,7 @@ app.use(express.urlencoded({extended: false}));
 app.use(cookieParser());
 
 app.use('/api/v1/*', sessionRouter);
+app.use('/api/v1/miscellaneous', miscellaneousRouter);
 app.use('/api/v1/cypher', cypherRouter);
 app.use('/api/v1/db', databaseRouter);
 
diff --git a/backend/src/routes/miscellaneous.js b/backend/src/routes/miscellaneous.js
new file mode 100644
index 0000000..69f4ad7
--- /dev/null
+++ b/backend/src/routes/miscellaneous.js
@@ -0,0 +1,9 @@
+const express = require('express');
+const { wrap } = require('../common/Routes');
+const getQueryList = require('../services/queryList');
+const router = express.Router();
+
+
+router.get('/', wrap(getQueryList));
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/src/services/queryList.js b/backend/src/services/queryList.js
new file mode 100644
index 0000000..8e4d2aa
--- /dev/null
+++ b/backend/src/services/queryList.js
@@ -0,0 +1,38 @@
+const fs = require('fs').promises;
+const papa = require('papaparse');
+const path = require('path');
+
+
+const readCSV = (file, resolve, reject)=>{
+    return papa.parse(file, {
+        skipEmptyLines:true,
+        transform:(val, col)=>{
+            if (col !== 0) return val;
+
+        },
+        complete:(results)=>{
+            resolve(results);
+        },
+        error:(err)=>{
+            reject(err);
+        },
+    });
+}
+const getQueryList = async (req, res, next)=>{
+    const p = path.join(__dirname, "../../misc/graph_kw.csv");
+    const file = await fs.readFile(p, {
+        encoding: 'utf-8'
+    });
+
+    const results = await new Promise((resolve, reject)=>{
+        readCSV(file, resolve, reject);
+    });
+
+    const kwResults = {
+        kw:results.data[0].splice(1),
+        relationships:results.data.slice(1)
+    }
+    res.status(200).json(kwResults).end();
+
+}
+module.exports = getQueryList;
\ No newline at end of file
diff --git a/frontend/src/components/contents/presentations/Contents.jsx b/frontend/src/components/contents/presentations/Contents.jsx
index b28a271..7d08c74 100644
--- a/frontend/src/components/contents/presentations/Contents.jsx
+++ b/frontend/src/components/contents/presentations/Contents.jsx
@@ -43,7 +43,7 @@ const Contents = ({
 
   return (
     <div className={`${styles.Content} ${isActive ? styles.Expanded : ''}`}>
-      <div style={{ padding: '0rem 1rem 1rem 1rem' }}>
+      <div>
         <FramesContainer />
       </div>
     </div>
diff --git a/frontend/src/components/contents/presentations/Editor.jsx b/frontend/src/components/contents/presentations/Editor.jsx
index 9a6082a..c3f0b86 100644
--- a/frontend/src/components/contents/presentations/Editor.jsx
+++ b/frontend/src/components/contents/presentations/Editor.jsx
@@ -192,13 +192,15 @@ const Editor = ({
                 type="button"
                 onClick={() => {
                   toggleMenu('home');
+                  /*
                   if (!isActive) {
-                    document.getElementById('wrapper').classList.remove('wrapper');
-                    document.getElementById('wrapper').classList.add('wrapper-extension-padding');
+                    document.getElementById('wrapper')?.classList?.remove('wrapper');
+                    document.getElementById('wrapper')?.classList?.add('wrapper-extension-padding');
                   } else {
-                    document.getElementById('wrapper').classList.remove('wrapper-extension-padding');
-                    document.getElementById('wrapper').classList.add('wrapper');
-                  }
+                    document.getElementById('wrapper')?
+                    .classList?.remove('wrapper-extension-padding');
+                    document.getElementById('wrapper')?.classList?.add('wrapper');
+                  } */
                 }}
                 title={(isActive) ? 'Hide' : 'Show'}
               >
diff --git a/frontend/src/components/query_builder/BuilderContainer.jsx b/frontend/src/components/query_builder/BuilderContainer.jsx
new file mode 100644
index 0000000..3a95e66
--- /dev/null
+++ b/frontend/src/components/query_builder/BuilderContainer.jsx
@@ -0,0 +1,94 @@
+import {
+  Button, Drawer, Select, Space,
+} from 'antd';
+import React, { useState, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import PropTypes from 'prop-types';
+import CodeMirror from '../editor/containers/CodeMirrorWapperContainer';
+import BuilderSelection from './BuilderSelection';
+import KeyWordFinder from '../../features/query_builder/KeyWordFinder';
+
+import { setCommand } from '../../features/editor/EditorSlice';
+import './BuilderContainer.scss';
+
+const BuilderContainer = ({ open, setOpen, finder }) => {
+  const [query, setQuery] = useState('');
+  const [currentWord, setCurrentWord] = useState('');
+  const [selectedGraph, setSelectedGraph] = useState('');
+  const [availableGraphs, setAvailableGraphs] = useState([]);
+  const metadata = useSelector((state) => state.metadata);
+  const dispatch = useDispatch();
+  useEffect(() => {
+    setAvailableGraphs(Object.keys(metadata.graphs));
+  }, [metadata]);
+
+  const getCurrentWord = (q) => {
+    const words = q.split(/[ ,\n]/);
+    const isWord = words.findLast((element) => finder.hasWord(element));
+    const word = isWord || '';
+    setCurrentWord(word);
+  };
+
+  const handleSetQuery = (word) => {
+    const fullQuery = query !== '' ? `${query.trim()}\n${word}` : word;
+
+    setQuery(fullQuery);
+    getCurrentWord(fullQuery);
+  };
+  const handleSelectGraph = (s) => {
+    setSelectedGraph(s);
+  };
+
+  const handleSubmit = () => {
+    const finalQuery = `SELECT * FROM cypher('${selectedGraph}', $$ ${query} $$) as (V agtype)`;
+    dispatch((setCommand(finalQuery)));
+    setOpen(false);
+  };
+  return (
+    <Drawer
+      title="Query Generator"
+      open={open}
+      onClose={() => setOpen(!open)}
+      placement="left"
+    >
+      <Select
+        id="graph-selection"
+        onChange={handleSelectGraph}
+        placeholder="Select Graph"
+        value={selectedGraph}
+      >
+        {
+          availableGraphs.map((s) => (
+            <Select.Option
+              value={s}
+            />
+          ))
+          }
+      </Select>
+
+      <Space />
+      <div className="code-mirror-builder">
+        <CodeMirror onChange={handleSetQuery} value={query} />
+      </div>
+
+      <Space />
+      <div className="selection-builder">
+        <BuilderSelection
+          finder={finder}
+          setQuery={handleSetQuery}
+          currentWord={currentWord}
+        />
+      </div>
+      <div id="submit-builder">
+        <Button size="sm" onClick={handleSubmit}>Submit</Button>
+      </div>
+
+    </Drawer>
+  );
+};
+BuilderContainer.propTypes = {
+  open: PropTypes.bool.isRequired,
+  setOpen: PropTypes.func.isRequired,
+  finder: PropTypes.shape(KeyWordFinder).isRequired,
+};
+export default BuilderContainer;
diff --git a/frontend/src/components/query_builder/BuilderContainer.scss b/frontend/src/components/query_builder/BuilderContainer.scss
new file mode 100644
index 0000000..3b4207b
--- /dev/null
+++ b/frontend/src/components/query_builder/BuilderContainer.scss
@@ -0,0 +1,16 @@
+.code-mirror-builder{
+    display: flex;
+    padding: 5px 5px 5px 5px;
+}
+
+.selection-builder{
+    display: block;
+
+}
+
+#submit-builder{
+    width: 100%;
+    display: flex;
+    justify-content: right;
+    padding-top: 10px;
+}
\ No newline at end of file
diff --git a/frontend/src/components/query_builder/BuilderSelection.jsx b/frontend/src/components/query_builder/BuilderSelection.jsx
new file mode 100644
index 0000000..99274a7
--- /dev/null
+++ b/frontend/src/components/query_builder/BuilderSelection.jsx
@@ -0,0 +1,38 @@
+import PropTypes from 'prop-types';
+import { ListGroup, Button } from 'react-bootstrap';
+import React from 'react';
+import uuid from 'react-uuid';
+import KeyWordFinder from '../../features/query_builder/KeyWordFinder';
+
+const BuilderSelection = ({ finder, setQuery, currentWord }) => {
+  const handleClick = (e) => {
+    const selectedVal = e.target.getAttribute('data-val');
+    setQuery(selectedVal);
+  };
+  return (
+    <ListGroup>
+      {
+    finder?.getConnectedNames(currentWord).map(
+      (element) => (
+        <ListGroup.Item key={uuid()}>
+          <Button
+            size="small"
+            onClick={handleClick}
+            data-val={element}
+          >
+            {element}
+          </Button>
+        </ListGroup.Item>
+      ),
+    )
+    }
+    </ListGroup>
+  );
+};
+
+BuilderSelection.propTypes = {
+  finder: PropTypes.shape(KeyWordFinder).isRequired,
+  setQuery: PropTypes.func.isRequired,
+  currentWord: PropTypes.string.isRequired,
+};
+export default BuilderSelection;
diff --git a/frontend/src/components/template/presentations/DefaultTemplate.jsx b/frontend/src/components/template/presentations/DefaultTemplate.jsx
index 323e1c9..9ce3a84 100644
--- a/frontend/src/components/template/presentations/DefaultTemplate.jsx
+++ b/frontend/src/components/template/presentations/DefaultTemplate.jsx
@@ -20,11 +20,17 @@
 import React, { useEffect, useState } from 'react';
 import PropTypes from 'prop-types';
 import { useDispatch } from 'react-redux';
+import { Row, Button } from 'react-bootstrap';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faBars } from '@fortawesome/free-solid-svg-icons';
 import EditorContainer from '../../contents/containers/Editor';
 import Sidebar from '../../sidebar/containers/Sidebar';
 import Contents from '../../contents/containers/Contents';
 import Modal from '../../modal/containers/Modal';
 import { loadFromCookie, saveToCookie } from '../../../features/cookie/CookieUtil';
+import BuilderContainer from '../../query_builder/BuilderContainer';
+import './DefaultTemplate.scss';
+import KeyWordFinder from '../../../features/query_builder/KeyWordFinder';
 
 const DefaultTemplate = ({
   theme,
@@ -36,6 +42,7 @@ const DefaultTemplate = ({
   isOpen,
 }) => {
   const dispatch = useDispatch();
+  const [open, setOpen] = useState(false);
   const [stateValues] = useState({
     theme,
     maxNumOfFrames,
@@ -43,6 +50,17 @@ const DefaultTemplate = ({
     maxDataOfGraph,
     maxDataOfTable,
   });
+  const [finder, setFinder] = useState(null);
+
+  useEffect(async () => {
+    const req = {
+      method: 'GET',
+    };
+    const res = await fetch('/api/v1/miscellaneous', req);
+    const results = await res.json();
+    const kwFinder = KeyWordFinder.fromMatrix(results);
+    setFinder(kwFinder);
+  }, []);
 
   useEffect(() => {
     let isChanged = false;
@@ -93,13 +111,22 @@ const DefaultTemplate = ({
         checked={theme === 'dark'}
         readOnly
       />
-      <div className="editor-divison">
-        <EditorContainer />
-        <Sidebar />
-      </div>
-      <div className="wrapper-extension-padding" id="wrapper">
-        <Contents />
-      </div>
+      <Row className="content-row">
+        <div>
+          <Button onClick={() => setOpen(true)}>
+            <FontAwesomeIcon icon={faBars} />
+          </Button>
+          <BuilderContainer open={open} setOpen={setOpen} finder={finder} />
+        </div>
+        <div className="editor-division wrapper-extension-padding">
+
+          <EditorContainer />
+          <Sidebar />
+          <Contents />
+
+        </div>
+
+      </Row>
 
     </div>
   );
diff --git a/frontend/src/components/template/presentations/DefaultTemplate.scss b/frontend/src/components/template/presentations/DefaultTemplate.scss
new file mode 100644
index 0000000..e50b6c7
--- /dev/null
+++ b/frontend/src/components/template/presentations/DefaultTemplate.scss
@@ -0,0 +1,13 @@
+.editor-division{
+    display: flex;
+}
+.content-row{
+    flex-wrap: nowrap;
+    display: flex;
+    justify-items: space-evenly;
+    margin:2px 2px 2px 2px;
+}
+.row{
+    flex-wrap: nowrap;
+    overflow-y: scroll;
+}
\ No newline at end of file
diff --git a/frontend/src/features/query_builder/KeyWordFinder.js b/frontend/src/features/query_builder/KeyWordFinder.js
new file mode 100644
index 0000000..6bf0980
--- /dev/null
+++ b/frontend/src/features/query_builder/KeyWordFinder.js
@@ -0,0 +1,45 @@
+class KeyWordFinder {
+  constructor() {
+    this.keywordMap = new Map();
+    this.allKeywords = new Set();
+  }
+
+  getConnectedNames(kw) {
+    const key = kw.toUpperCase();
+    if (!this.allKeywords.has(key)) {
+      return KeyWordFinder.INITIAL;
+    }
+    const relationships = this.keywordMap[key];
+    const keywordList = Object.keys(this.keywordMap);
+    const relatedKeys = [];
+    relationships.forEach((element, index) => {
+      if (element !== '0') {
+        relatedKeys.push(keywordList[index]);
+      }
+    });
+    return relatedKeys;
+  }
+
+  hasWord(word) {
+    const upperWord = word.toUpperCase();
+    return this.allKeywords.has(upperWord);
+  }
+
+  static get INITIAL() {
+    return ['MATCH', 'CREATE', 'MERGE'];
+  }
+
+  static fromMatrix(data) {
+    const { kw, relationships } = data;
+    const finder = new KeyWordFinder();
+    // kw is list of keywordList and relationships is matrix
+    kw.forEach((element, index) => {
+      if (element === '') return;
+      finder.keywordMap[element] = relationships[index].slice(1);
+      finder.allKeywords.add(element);
+    });
+    return finder;
+  }
+}
+
+export default KeyWordFinder;
diff --git a/frontend/src/static/style.css b/frontend/src/static/style.css
index d00f15d..2dd133d 100644
--- a/frontend/src/static/style.css
+++ b/frontend/src/static/style.css
@@ -89,7 +89,7 @@ body {
     width: 100%;
     align-items: stretch;
     position: relative;
-    max-height: calc(100% - 300px);
+    max-height: 100%;
     overflow-y: scroll ;
 }
 /* ---------------------------------------------------
@@ -124,7 +124,7 @@ body {
     SIDEBAR STYLE
 ----------------------------------------------------- */
 #navbar {
-    width: 70px;
+    width: 100%sideba;
     background-color: var(--navbar-color);
 }
 
@@ -380,10 +380,11 @@ a[data-toggle="collapse"] {
     }
 
     .sidebar-home {
-        display: block;
+        display: flex;
     }
     
     .sidebar-body {
+        display: flex;
         width: 100%;
     }