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%;
}