From 9a3d859a6e2ba2955756eb26cfe4c1a979d95df5 Mon Sep 17 00:00:00 2001 From: joon-at-sri Date: Fri, 2 Aug 2024 14:55:21 -0700 Subject: [PATCH 01/10] wip --- .../QueryBuilder/QueryBuilderComponent.tsx | 116 ++++++++++++++++++ .../Details/QueryBuilder/SelectSelector | 0 .../Details/QueryBuilder/WhereSelector.tsx | 68 ++++++++++ src/reducers/gremlinReducer.ts | 2 +- 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/components/Details/QueryBuilder/QueryBuilderComponent.tsx create mode 100644 src/components/Details/QueryBuilder/SelectSelector create mode 100644 src/components/Details/QueryBuilder/WhereSelector.tsx diff --git a/src/components/Details/QueryBuilder/QueryBuilderComponent.tsx b/src/components/Details/QueryBuilder/QueryBuilderComponent.tsx new file mode 100644 index 0000000..0a0e3aa --- /dev/null +++ b/src/components/Details/QueryBuilder/QueryBuilderComponent.tsx @@ -0,0 +1,116 @@ +import React, { SyntheticEvent, useEffect, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { Box, Button, LinearProgress, Paper, createFilterOptions } from '@mui/material'; + +import { Edge, Node } from 'vis-network'; +import _ from 'lodash'; +import axios from 'axios'; +import { QUERY_ENDPOINT, COMMON_GREMLIN_ERROR } from '../../../constants'; +import { MaterialSelector } from './WhereSelector'; +import { onFetchQuery } from '../../../logics/actionHelper'; +import { applyLayout } from '../../../logics/graph'; +import { selectGraph, clearGraph, setMaterials, setComponents, setSuppliers } from '../../../reducers/graphReducer'; +import { selectGremlin } from '../../../reducers/gremlinReducer'; +import { selectOptions, setLayout } from '../../../reducers/optionReducer'; + +interface HeaderComponentProps { + panelWidth: number +} + +export const HeaderComponent = (props: HeaderComponentProps) => { + const { nodeLabels, nodeLimit, graphOptions } = useSelector(selectOptions); + const { components, suppliers, materials, selectorNodes } = useSelector(selectGraph); + const [error, setError] = useState(null); + const dispatch = useDispatch(); + const { host, port } = useSelector(selectGremlin); + + const handleLoad = () => { + dispatch(clearGraph()); + applyLayout("hierarchical"); + dispatch(setLayout("hierarchical")); + let queryToSend = ''; + let str = ''; + setError(null); + if (suppliers.length > 0) { + str = suppliers.map((gr) => `'${gr}'`).join(','); + queryToSend = `g.V().has("Entity", "name", within(${str})).emit().repeat(out())`; + sendRequest(queryToSend); + } + if (components.length > 0) { + str = components.map((gr) => `'${gr}'`).join(','); + queryToSend = `g.V().has("Component", "name", within(${str})).emit().repeat(in())`; + sendRequest(queryToSend); + } + if (materials.length > 0) { + str = materials.map((gr) => `'${gr}'`).join(','); + queryToSend = `g.V().has("Material", "name", within(${str})).emit().repeat(in())`; + sendRequest(queryToSend); + } + }; + + const handleClear = () => { + dispatch(clearGraph()); + dispatch(setMaterials([])); + dispatch(setComponents([])); + dispatch(setSuppliers([])); + } + + const sendRequest = (query: string) => { + axios + .post( + QUERY_ENDPOINT, + { host, port, query, nodeLimit }, + { headers: { 'Content-Type': 'application/json' } } + ) + .then((response) => { + onFetchQuery(response, query, nodeLabels, dispatch); + }) + .catch((error) => { + console.warn(error) + setError(COMMON_GREMLIN_ERROR); + }); + } + + + return ( + + {/* + + + + + + + + + +
+ + +
{error}
*/} +
+ ); +}; \ No newline at end of file diff --git a/src/components/Details/QueryBuilder/SelectSelector b/src/components/Details/QueryBuilder/SelectSelector new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Details/QueryBuilder/WhereSelector.tsx b/src/components/Details/QueryBuilder/WhereSelector.tsx new file mode 100644 index 0000000..c4da8ed --- /dev/null +++ b/src/components/Details/QueryBuilder/WhereSelector.tsx @@ -0,0 +1,68 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + FormControl, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, +} from '@mui/material'; +import { useDispatch, useSelector } from 'react-redux'; +import { selectGraph, setMaterials } from '../../../reducers/graphReducer'; + + +export function MaterialSelector() { + const dispatch = useDispatch(); + const { selectorNodes, materials } = useSelector(selectGraph); + const names = selectorNodes.filter(node => node.type === 'Material').map(node => node.properties.name); + + const [selectedMaterialNames, setSelectedMaterialNames] = React.useState(materials); + + const handleChange = (event: SelectChangeEvent) => { + + const { + target: { value }, + } = event; + setSelectedMaterialNames( + typeof value === 'string' ? value.split(',') : value, + ); + dispatch(setMaterials(typeof value === 'string' ? value.split(',') : value)); + }; + + return ( + <> + + Select Material + + + + ); +} \ No newline at end of file diff --git a/src/reducers/gremlinReducer.ts b/src/reducers/gremlinReducer.ts index cbd9157..7ece5e9 100644 --- a/src/reducers/gremlinReducer.ts +++ b/src/reducers/gremlinReducer.ts @@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { RootState } from '../app/store'; const initialState = { - host: 'gremlin-server', + host: 'localhost', port: '8182', query: 'g.V()', error: null From 5a265055ae6f7b5199c7d1f153eb4b55b483ddfa Mon Sep 17 00:00:00 2001 From: joon-at-sri Date: Tue, 6 Aug 2024 11:01:57 -0700 Subject: [PATCH 02/10] list implementation of logical expressions --- .../QueryBuilder/QueryBuilderComponent.tsx | 116 ----- .../Details/QueryBuilder/SelectSelector | 0 .../Details/QueryBuilder/WhereSelector.tsx | 68 --- .../Details/QueryBuilderComponent.tsx | 472 ++++++++++++++++++ src/components/Details/SidebarComponent.tsx | 10 +- 5 files changed, 481 insertions(+), 185 deletions(-) delete mode 100644 src/components/Details/QueryBuilder/QueryBuilderComponent.tsx delete mode 100644 src/components/Details/QueryBuilder/SelectSelector delete mode 100644 src/components/Details/QueryBuilder/WhereSelector.tsx create mode 100644 src/components/Details/QueryBuilderComponent.tsx diff --git a/src/components/Details/QueryBuilder/QueryBuilderComponent.tsx b/src/components/Details/QueryBuilder/QueryBuilderComponent.tsx deleted file mode 100644 index 0a0e3aa..0000000 --- a/src/components/Details/QueryBuilder/QueryBuilderComponent.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { SyntheticEvent, useEffect, useState } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import { Box, Button, LinearProgress, Paper, createFilterOptions } from '@mui/material'; - -import { Edge, Node } from 'vis-network'; -import _ from 'lodash'; -import axios from 'axios'; -import { QUERY_ENDPOINT, COMMON_GREMLIN_ERROR } from '../../../constants'; -import { MaterialSelector } from './WhereSelector'; -import { onFetchQuery } from '../../../logics/actionHelper'; -import { applyLayout } from '../../../logics/graph'; -import { selectGraph, clearGraph, setMaterials, setComponents, setSuppliers } from '../../../reducers/graphReducer'; -import { selectGremlin } from '../../../reducers/gremlinReducer'; -import { selectOptions, setLayout } from '../../../reducers/optionReducer'; - -interface HeaderComponentProps { - panelWidth: number -} - -export const HeaderComponent = (props: HeaderComponentProps) => { - const { nodeLabels, nodeLimit, graphOptions } = useSelector(selectOptions); - const { components, suppliers, materials, selectorNodes } = useSelector(selectGraph); - const [error, setError] = useState(null); - const dispatch = useDispatch(); - const { host, port } = useSelector(selectGremlin); - - const handleLoad = () => { - dispatch(clearGraph()); - applyLayout("hierarchical"); - dispatch(setLayout("hierarchical")); - let queryToSend = ''; - let str = ''; - setError(null); - if (suppliers.length > 0) { - str = suppliers.map((gr) => `'${gr}'`).join(','); - queryToSend = `g.V().has("Entity", "name", within(${str})).emit().repeat(out())`; - sendRequest(queryToSend); - } - if (components.length > 0) { - str = components.map((gr) => `'${gr}'`).join(','); - queryToSend = `g.V().has("Component", "name", within(${str})).emit().repeat(in())`; - sendRequest(queryToSend); - } - if (materials.length > 0) { - str = materials.map((gr) => `'${gr}'`).join(','); - queryToSend = `g.V().has("Material", "name", within(${str})).emit().repeat(in())`; - sendRequest(queryToSend); - } - }; - - const handleClear = () => { - dispatch(clearGraph()); - dispatch(setMaterials([])); - dispatch(setComponents([])); - dispatch(setSuppliers([])); - } - - const sendRequest = (query: string) => { - axios - .post( - QUERY_ENDPOINT, - { host, port, query, nodeLimit }, - { headers: { 'Content-Type': 'application/json' } } - ) - .then((response) => { - onFetchQuery(response, query, nodeLabels, dispatch); - }) - .catch((error) => { - console.warn(error) - setError(COMMON_GREMLIN_ERROR); - }); - } - - - return ( - - {/* - - - - - - - - - -
- - -
{error}
*/} -
- ); -}; \ No newline at end of file diff --git a/src/components/Details/QueryBuilder/SelectSelector b/src/components/Details/QueryBuilder/SelectSelector deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/Details/QueryBuilder/WhereSelector.tsx b/src/components/Details/QueryBuilder/WhereSelector.tsx deleted file mode 100644 index c4da8ed..0000000 --- a/src/components/Details/QueryBuilder/WhereSelector.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - Button, - FormControl, - InputLabel, - MenuItem, - Select, - SelectChangeEvent, -} from '@mui/material'; -import { useDispatch, useSelector } from 'react-redux'; -import { selectGraph, setMaterials } from '../../../reducers/graphReducer'; - - -export function MaterialSelector() { - const dispatch = useDispatch(); - const { selectorNodes, materials } = useSelector(selectGraph); - const names = selectorNodes.filter(node => node.type === 'Material').map(node => node.properties.name); - - const [selectedMaterialNames, setSelectedMaterialNames] = React.useState(materials); - - const handleChange = (event: SelectChangeEvent) => { - - const { - target: { value }, - } = event; - setSelectedMaterialNames( - typeof value === 'string' ? value.split(',') : value, - ); - dispatch(setMaterials(typeof value === 'string' ? value.split(',') : value)); - }; - - return ( - <> - - Select Material - - - - ); -} \ No newline at end of file diff --git a/src/components/Details/QueryBuilderComponent.tsx b/src/components/Details/QueryBuilderComponent.tsx new file mode 100644 index 0000000..61035ac --- /dev/null +++ b/src/components/Details/QueryBuilderComponent.tsx @@ -0,0 +1,472 @@ +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { Autocomplete, Box, Button, FormControl, Grid, InputLabel, MenuItem, Paper, Select, SelectChangeEvent, TextField } from '@mui/material'; +import { COMMON_GREMLIN_ERROR, QUERY_ENDPOINT } from '../../constants'; +import { onFetchQuery } from '../../logics/actionHelper'; +import { selectOptions, setLayout } from '../../reducers/optionReducer'; +import _, { findLast } from 'lodash'; +import { clearGraph, selectGraph } from '../../reducers/graphReducer'; +import { selectGremlin } from '../../reducers/gremlinReducer'; +import axios from 'axios'; +import { applyLayout } from '../../logics/graph'; +import { type } from 'os'; +import { selectDialog } from '../../reducers/dialogReducer'; +import { DIALOG_TYPES } from '../ModalDialog/ModalDialogComponent'; + +interface Term { + [key: string]: string // Replace with your actual term structure +} + +type QueryNode = { + [key: string]: QueryNode[]; // A key-value map where values are Queries +} | Term; // Or it can be just a Term object + + +const exampleQuery: QueryNode[] = [ + { term1: 'value1' }, + { + 'OR': [ + { + 'OR': [ + { term2: 'value2' }, + { term3: 'value3' } + ] + }, + { term4: 'value4' } + ] + }, + { term5: 'value5' } +]; + +const orQuery: QueryNode[] = [ + {'OR' : [ + {term1: 'value1'}, + {term2: 'value2'} + ]} +] + +// const deeperQuery: QueryNode[] = [ +// {'OR' : [ +// {term1: 'value1'}, +// {'OR': [{term2: 'value2'}, {term4: 'value4'}], [{term3: 'value3'}, {term5: 'value5'}]} +// ]} +// ] + +const findLastORNode = (query: QueryNode[]): QueryNode | null => { + let lastORNode: QueryNode | null = null; + + const traverse = (nodes: QueryNode[]) => { + for (const node of nodes) { + if (typeof node === 'object' && !Array.isArray(node)) { + const keys = Object.keys(node); + if (keys.includes('OR')) { + lastORNode = node; // Update lastORNode if 'OR' key is found + return; + } + + // If the value of 'OR' is an array, traverse it + if (lastORNode && Array.isArray(lastORNode['OR'])) { + traverse(lastORNode['OR']); + } + } + } + }; + + traverse(query); + return lastORNode; +}; + + +export const QueryBuilder = () => { + + const processQuery = (query: QueryNode[]): string => { + const processNode = (node: QueryNode): string => { + if (Array.isArray(node)) { + // node is a Query array + return node.map(processNode).join(' AND '); + } + + const keys = Object.keys(node); + if (keys.length === 0) { + return ''; + } + + const key = keys[0]; + const value = node[key]; + + if (key === 'OR' && Array.isArray(value)) { + return `(${value.map(processNode).join(' OR ')})`; + } + return Object.entries(node).map(([k, v]) => `${k}:${v}`).join(' '); + + }; + return query.map(node => processNode(node)).join('AND'); + + }; + + const insertTerm = (query: QueryNode[], term: Term, operator: 'AND' | 'OR' = 'AND'): QueryNode[] => { + if (query.length === 0) { + return [term]; + } + + if (operator === 'AND') { + return [...query, term]; + } + + // For OR operator, wrap the current query and the new term into an OR clause' + const orNode = findLastORNode(query) + if (!orNode) { + return [{ 'OR': [...query, term] }]; + } + else { + console.log(JSON.stringify(orNode)); + return [orNode]; + } + + }; + + + console.log(JSON.stringify(processQuery(exampleQuery))); + + + const [selectedProperty, setSelectedProperty] = useState(null); + const [value, setValue] = useState(''); + + const handleAddTerm = () => { + // Add logic to handle adding a new term + }; + + const handleAddNode = (type: 'AND' | 'OR') => { + // Add logic to handle adding a new node + }; + + const handleChange = (index: number, newTerm: Term) => { + // Handle term change logic + }; + + return (
+ +
) +}; + + +// import React, { useState } from 'react'; +// import { useSelector, useDispatch } from 'react-redux'; +// import { Autocomplete, Box, Button, FormControl, Grid, InputLabel, MenuItem, Paper, Select, SelectChangeEvent, TextField } from '@mui/material'; +// import { COMMON_GREMLIN_ERROR, QUERY_ENDPOINT } from '../../constants'; +// import { onFetchQuery } from '../../logics/actionHelper'; +// import { selectOptions, setLayout } from '../../reducers/optionReducer'; +// import _ from 'lodash'; +// import { clearGraph, selectGraph } from '../../reducers/graphReducer'; +// import { selectGremlin } from '../../reducers/gremlinReducer'; +// import axios from 'axios'; +// import { applyLayout } from '../../logics/graph'; +// import { type } from 'os'; +// import { selectDialog } from '../../reducers/dialogReducer'; +// import { DIALOG_TYPES } from '../ModalDialog/ModalDialogComponent'; + +// type FormField = { +// propertyName: string; +// propertyValue: string; +// propertyClause: string; +// }; + +// const CLAUSES = { +// GREATER_THAN: '>', +// LESS_THAN: '<', +// GREATER_THAN_EQ : '>=', +// LESS_THAN_EQ: '<=', +// EQUAL: '==' +// }; + +// export const QueryBuilder = () => { +// const { nodeLabels, nodeLimit } = useSelector(selectOptions); +// const { selectorNodes } = useSelector(selectGraph); +// const {suggestions} = useSelector(selectDialog); +// const [error, setError] = useState(null); +// const dispatch = useDispatch(); +// const { host, port } = useSelector(selectGremlin); +// const [formFields, setFormFields] = useState([]); +// const types = ["Component", "Entity", "Material"]; +// const [selectedType, setSelectedType] = React.useState(""); +// const [whereOptions, setWhereOptions] = React.useState([]); +// console.log(suggestions); + +// const [selectedWhereOptions, setSelectedWhereOptions] = React.useState([]); + +// const materialNames = selectorNodes.filter(node => node.type === 'Material').map(node => node.properties.name); +// const [selectedMaterialNames, setSelectedMaterialNames] = React.useState([]); + +// const handleFormChange = (event: React.ChangeEvent, index: number) => { +// const { name, value } = event.target; +// setFormFields(prevFormFields => +// prevFormFields.map((formField, i) => +// i === index ? { ...formField, [name]: value } : formField +// ) +// ); +// }; + + +// const addFields = () => { +// let object = { +// propertyName: '', +// propertyValue: '', +// propertyClause: '' +// }; +// setFormFields([...formFields, object]); +// }; + +// const removeFields = (index: number) => { +// let data = [...formFields]; +// data.splice(index, 1); +// setFormFields(data); +// }; + + +// const handleSelectChange = (event: SelectChangeEvent) => { +// const { +// target: { value }, +// } = event; +// setSelectedType(value); +// setWhereOptions(suggestions[DIALOG_TYPES.NODE]?.labels[value] ?? []); +// }; + +// const handleWhereChange = (event: SelectChangeEvent) => { +// const { +// target: { value }, +// } = event; +// setSelectedWhereOptions( +// typeof value === 'string' ? value.split(',') : value, +// ); +// }; + +// const handleMaterialChange = (event: SelectChangeEvent) => { + +// const { +// target: { value }, +// } = event; +// setSelectedMaterialNames( +// typeof value === 'string' ? value.split(',') : value, +// ); +// }; + +// const handleLoad = () => { +// dispatch(clearGraph()); +// applyLayout("hierarchical"); +// dispatch(setLayout("hierarchical")); +// let queryToSend = ''; +// let str = ''; +// setError(null); + + +// if (selectedMaterialNames.length > 0) { +// str = selectedMaterialNames.map((gr) => `'${gr}'`).join(','); +// queryToSend = `g.V().has("Material", "name", within(${str})).emit().repeat(in())`; +// sendRequest(queryToSend); +// } +// }; + +// const handleClear = () => { +// dispatch(clearGraph()); +// } + +// const sendRequest = (query: string) => { +// axios +// .post( +// QUERY_ENDPOINT, +// { host, port, query, nodeLimit }, +// { headers: { 'Content-Type': 'application/json' } } +// ) +// .then((response) => { +// onFetchQuery(response, query, nodeLabels, dispatch); +// }) +// .catch((error) => { +// console.warn(error) +// setError(COMMON_GREMLIN_ERROR); +// }); +// } +// const [selectedProperty, setSelectedProperty] = useState(null); +// const [value, setValue] = useState(''); + +// const handlePropertyChange = (event: React.ChangeEvent) => { +// setSelectedProperty(event.target.value); +// setValue(''); // Clear value when property changes +// }; + +// const handleValueChange = (event: React.ChangeEvent) => { +// setValue(event.target.value); +// }; + + +// return ( +// +// +// +// SELECT +// +// +// +// +// +// WHERE +// +// +// +// +// +// Select Material +// +// +// + +//
+// +// +//
{error}
+ + +// +// {formFields.map((form, index) => ( +// +// +// handleFormChange(event, index)} +// fullWidth +// variant="standard" +// /> +// +// +// handleFormChange(event, index)} +// fullWidth +// variant="standard" +// /> +// +// +// +// +// +// ))} +// +// +// +// +//
+// ); +// }; diff --git a/src/components/Details/SidebarComponent.tsx b/src/components/Details/SidebarComponent.tsx index c77d1ef..1046978 100644 --- a/src/components/Details/SidebarComponent.tsx +++ b/src/components/Details/SidebarComponent.tsx @@ -6,12 +6,14 @@ import Query from "./QueryComponent"; import { Settings } from "./SettingsComponent"; import { DetailsComponent } from "./DetailsComponent"; import CollapsibleTable from "./TableComponent"; +import {QueryBuilder} from "./QueryBuilderComponent"; import SavedQueries from "./SavedQueriesComponent" import PlayCircleFilledIcon from '@mui/icons-material/PlayCircleFilled'; import TocIcon from '@mui/icons-material/Toc'; import SettingsIcon from '@mui/icons-material/Settings'; import GradeIcon from '@mui/icons-material/Grade'; import DatasetIcon from '@mui/icons-material/Dataset'; +import BuildCircleIcon from '@mui/icons-material/BuildCircle'; type QueryHistoryProps = { list: Array; @@ -105,8 +107,11 @@ export const SidebarComponent = (props: SidebarComponentProps) => { } value={3} /> + + } value={4} /> + - } value={4} /> + } value={5} /> @@ -128,6 +133,9 @@ export const SidebarComponent = (props: SidebarComponentProps) => { + + + From e27d1665f9f15c9041bd0f8b378add9235447aec Mon Sep 17 00:00:00 2001 From: joon-at-sri Date: Thu, 8 Aug 2024 14:14:38 -0700 Subject: [PATCH 03/10] WIP --- .../Details/QueryBuilderComponent.tsx | 762 ++++++++---------- src/components/Details/QueryComponent.tsx | 5 +- 2 files changed, 324 insertions(+), 443 deletions(-) diff --git a/src/components/Details/QueryBuilderComponent.tsx b/src/components/Details/QueryBuilderComponent.tsx index 61035ac..e6700ad 100644 --- a/src/components/Details/QueryBuilderComponent.tsx +++ b/src/components/Details/QueryBuilderComponent.tsx @@ -1,472 +1,350 @@ import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { Autocomplete, Box, Button, FormControl, Grid, InputLabel, MenuItem, Paper, Select, SelectChangeEvent, TextField } from '@mui/material'; +import { Autocomplete, Box, Button, FormControl, Grid, IconButton, InputLabel, MenuItem, Paper, Select, SelectChangeEvent, Stack, TextField } from '@mui/material'; import { COMMON_GREMLIN_ERROR, QUERY_ENDPOINT } from '../../constants'; import { onFetchQuery } from '../../logics/actionHelper'; import { selectOptions, setLayout } from '../../reducers/optionReducer'; -import _, { findLast } from 'lodash'; +import _ from 'lodash'; import { clearGraph, selectGraph } from '../../reducers/graphReducer'; -import { selectGremlin } from '../../reducers/gremlinReducer'; +import { selectGremlin, setError } from '../../reducers/gremlinReducer'; import axios from 'axios'; import { applyLayout } from '../../logics/graph'; import { type } from 'os'; import { selectDialog } from '../../reducers/dialogReducer'; import { DIALOG_TYPES } from '../ModalDialog/ModalDialogComponent'; +import Typography from '@mui/material/Typography'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import { EditText } from 'react-edit-text'; +import ClearIcon from '@mui/icons-material/Clear'; +import { highlightNodesAndEdges } from '../../logics/graphImpl/visImpl'; +import { GRAPH_IMPL } from '../../constants'; + +type FormField = { + propertyName: string; + whereClause: string; + propertyValue: string; + operator: string; +}; -interface Term { - [key: string]: string // Replace with your actual term structure -} - -type QueryNode = { - [key: string]: QueryNode[]; // A key-value map where values are Queries -} | Term; // Or it can be just a Term object - - -const exampleQuery: QueryNode[] = [ - { term1: 'value1' }, - { - 'OR': [ - { - 'OR': [ - { term2: 'value2' }, - { term3: 'value3' } - ] - }, - { term4: 'value4' } - ] - }, - { term5: 'value5' } -]; - -const orQuery: QueryNode[] = [ - {'OR' : [ - {term1: 'value1'}, - {term2: 'value2'} - ]} -] - -// const deeperQuery: QueryNode[] = [ -// {'OR' : [ -// {term1: 'value1'}, -// {'OR': [{term2: 'value2'}, {term4: 'value4'}], [{term3: 'value3'}, {term5: 'value5'}]} -// ]} -// ] - -const findLastORNode = (query: QueryNode[]): QueryNode | null => { - let lastORNode: QueryNode | null = null; - - const traverse = (nodes: QueryNode[]) => { - for (const node of nodes) { - if (typeof node === 'object' && !Array.isArray(node)) { - const keys = Object.keys(node); - if (keys.includes('OR')) { - lastORNode = node; // Update lastORNode if 'OR' key is found - return; - } - - // If the value of 'OR' is an array, traverse it - if (lastORNode && Array.isArray(lastORNode['OR'])) { - traverse(lastORNode['OR']); - } - } - } - }; - - traverse(query); - return lastORNode; +const OPERATORS = { + NONE: 'none', + AND: 'And', + OR: 'Or' +}; +const CLAUSES = { + GREATER_THAN: '>', + LESS_THAN: '<', + GREATER_THAN_EQ: '>=', + LESS_THAN_EQ: '<=', + EQUAL: '==' }; export const QueryBuilder = () => { - - const processQuery = (query: QueryNode[]): string => { - const processNode = (node: QueryNode): string => { - if (Array.isArray(node)) { - // node is a Query array - return node.map(processNode).join(' AND '); - } - - const keys = Object.keys(node); - if (keys.length === 0) { - return ''; - } - - const key = keys[0]; - const value = node[key]; - - if (key === 'OR' && Array.isArray(value)) { - return `(${value.map(processNode).join(' OR ')})`; - } - return Object.entries(node).map(([k, v]) => `${k}:${v}`).join(' '); - + const { nodeLabels, nodeLimit } = useSelector(selectOptions); + const { selectorNodes } = useSelector(selectGraph); + const { suggestions } = useSelector(selectDialog); + const dispatch = useDispatch(); + const { host, port } = useSelector(selectGremlin); + const [formFields, setFormFields] = useState([{ propertyName: '', whereClause: '', propertyValue: '', operator: OPERATORS.NONE }]); + const types = ["Component", "Entity", "Material"]; + const [selectedType, setSelectedType] = React.useState(""); + const [whereOptions, setWhereOptions] = React.useState([]); + + + const addFields = (operator: string) => { + let object = { + propertyName: '', + whereClause: '', + propertyValue: '', + operator: operator, }; - return query.map(node => processNode(node)).join('AND'); - + setFormFields([...formFields, object]); }; - const insertTerm = (query: QueryNode[], term: Term, operator: 'AND' | 'OR' = 'AND'): QueryNode[] => { - if (query.length === 0) { - return [term]; - } - - if (operator === 'AND') { - return [...query, term]; - } - - // For OR operator, wrap the current query and the new term into an OR clause' - const orNode = findLastORNode(query) - if (!orNode) { - return [{ 'OR': [...query, term] }]; - } - else { - console.log(JSON.stringify(orNode)); - return [orNode]; - } - + const removeFields = (index: number) => { + let data = [...formFields]; + data.splice(index, 1); + setFormFields(data); }; - console.log(JSON.stringify(processQuery(exampleQuery))); - - - const [selectedProperty, setSelectedProperty] = useState(null); - const [value, setValue] = useState(''); - - const handleAddTerm = () => { - // Add logic to handle adding a new term + const handleSelectChange = (event: SelectChangeEvent) => { + const { value } = event.target + setSelectedType(value); + setWhereOptions(suggestions[DIALOG_TYPES.NODE]?.labels[value] ?? []); }; - const handleAddNode = (type: 'AND' | 'OR') => { - // Add logic to handle adding a new node + const handleWhereChange = (event: SelectChangeEvent, index: number) => { + const { name, value } = event.target; + setFormFields(prevFormFields => + prevFormFields.map((formField, i) => + i === index ? { ...formField, propertyName: value } : formField + ) + ); + }; + const handleClauseChange = (event: SelectChangeEvent, index: number) => { + const { name, value } = event.target; + setFormFields(prevFormFields => + prevFormFields.map((formField, i) => + i === index ? { ...formField, whereClause: value } : formField + ) + ); }; - const handleChange = (index: number, newTerm: Term) => { - // Handle term change logic + const handleValueChange = (event: React.ChangeEvent, index: number) => { + const { name, value } = event.target; + setFormFields(prevFormFields => + prevFormFields.map((formField, i) => + i === index ? { ...formField, propertyValue: value } : formField + ) + ); }; - return (
+ const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + let query = `g.V().hasLabel('${selectedType}')`; + console.log(formFields) + + formFields.forEach((form) => { + let expression; + + switch (form.whereClause) { + case CLAUSES.EQUAL: + expression = `'${form.propertyValue}'`; + break; + case CLAUSES.GREATER_THAN: + expression = `P.gt(${form.propertyValue})`; + break; + case CLAUSES.GREATER_THAN_EQ: + expression = `P.gte(${form.propertyValue})`; + break; + case CLAUSES.LESS_THAN: + expression = `P.lt(${form.propertyValue})`; + break; + case CLAUSES.LESS_THAN_EQ: + expression = `P.lte(${form.propertyValue})`; + break + } + expression = `('${form.propertyName}', ${expression})`; + + switch (form.operator) { + case OPERATORS.NONE: + query += `.has${expression}`; + break; + case OPERATORS.AND: + query += `.and(__.has${expression})`; + break; + case OPERATORS.OR: + query += `.or(__.has${expression})`; + break; + } -
) + console.log(query); + + }) + + dispatch(clearGraph()); + dispatch(setError(null)); + if (GRAPH_IMPL === 'vis') { + highlightNodesAndEdges(null, null); + } + axios + .post( + QUERY_ENDPOINT, + { host, port, query, nodeLimit }, + { headers: { 'Content-Type': 'application/json' } } + ) + .then((response) => { + onFetchQuery(response, query, nodeLabels, dispatch); + }) + .catch((error) => { + console.warn(error) + dispatch(setError(COMMON_GREMLIN_ERROR)); + }); + } + + + const whereRow = (form: FormField, index: number) => { + return ( + + {formFields[index].operator !== OPERATORS.NONE && ( + + {formFields[index].operator} + + )} + + + + WHERE + + + + + + + CLAUSE + + + + + handleValueChange(event, index)} + fullWidth + sx={{ width: "100%", border: '1px solid #ccc', + borderRadius: '4px', + paddingBottom: '8px', margin: '0px', paddingLeft: '10px',boxSizing: 'border-box'}} + variant="standard" + + InputLabelProps={{ + sx: { + marginLeft: '8px' + } + }} + /> + + removeFields(index)} color="secondary"> + + + + + ) + } + + return ( + + + + SELECT + + + +
+ + } + aria-controls="panel2-content" + id="panel2-header" + > + WHERE + + + + {formFields.map((form, index) => ( + + {whereRow(form, index)} + + ))} + + + + + + + + +
+
+ ); }; - - -// import React, { useState } from 'react'; -// import { useSelector, useDispatch } from 'react-redux'; -// import { Autocomplete, Box, Button, FormControl, Grid, InputLabel, MenuItem, Paper, Select, SelectChangeEvent, TextField } from '@mui/material'; -// import { COMMON_GREMLIN_ERROR, QUERY_ENDPOINT } from '../../constants'; -// import { onFetchQuery } from '../../logics/actionHelper'; -// import { selectOptions, setLayout } from '../../reducers/optionReducer'; -// import _ from 'lodash'; -// import { clearGraph, selectGraph } from '../../reducers/graphReducer'; -// import { selectGremlin } from '../../reducers/gremlinReducer'; -// import axios from 'axios'; -// import { applyLayout } from '../../logics/graph'; -// import { type } from 'os'; -// import { selectDialog } from '../../reducers/dialogReducer'; -// import { DIALOG_TYPES } from '../ModalDialog/ModalDialogComponent'; - -// type FormField = { -// propertyName: string; -// propertyValue: string; -// propertyClause: string; -// }; - -// const CLAUSES = { -// GREATER_THAN: '>', -// LESS_THAN: '<', -// GREATER_THAN_EQ : '>=', -// LESS_THAN_EQ: '<=', -// EQUAL: '==' -// }; - -// export const QueryBuilder = () => { -// const { nodeLabels, nodeLimit } = useSelector(selectOptions); -// const { selectorNodes } = useSelector(selectGraph); -// const {suggestions} = useSelector(selectDialog); -// const [error, setError] = useState(null); -// const dispatch = useDispatch(); -// const { host, port } = useSelector(selectGremlin); -// const [formFields, setFormFields] = useState([]); -// const types = ["Component", "Entity", "Material"]; -// const [selectedType, setSelectedType] = React.useState(""); -// const [whereOptions, setWhereOptions] = React.useState([]); -// console.log(suggestions); - -// const [selectedWhereOptions, setSelectedWhereOptions] = React.useState([]); - -// const materialNames = selectorNodes.filter(node => node.type === 'Material').map(node => node.properties.name); -// const [selectedMaterialNames, setSelectedMaterialNames] = React.useState([]); - -// const handleFormChange = (event: React.ChangeEvent, index: number) => { -// const { name, value } = event.target; -// setFormFields(prevFormFields => -// prevFormFields.map((formField, i) => -// i === index ? { ...formField, [name]: value } : formField -// ) -// ); -// }; - - -// const addFields = () => { -// let object = { -// propertyName: '', -// propertyValue: '', -// propertyClause: '' -// }; -// setFormFields([...formFields, object]); -// }; - -// const removeFields = (index: number) => { -// let data = [...formFields]; -// data.splice(index, 1); -// setFormFields(data); -// }; - - -// const handleSelectChange = (event: SelectChangeEvent) => { -// const { -// target: { value }, -// } = event; -// setSelectedType(value); -// setWhereOptions(suggestions[DIALOG_TYPES.NODE]?.labels[value] ?? []); -// }; - -// const handleWhereChange = (event: SelectChangeEvent) => { -// const { -// target: { value }, -// } = event; -// setSelectedWhereOptions( -// typeof value === 'string' ? value.split(',') : value, -// ); -// }; - -// const handleMaterialChange = (event: SelectChangeEvent) => { - -// const { -// target: { value }, -// } = event; -// setSelectedMaterialNames( -// typeof value === 'string' ? value.split(',') : value, -// ); -// }; - -// const handleLoad = () => { -// dispatch(clearGraph()); -// applyLayout("hierarchical"); -// dispatch(setLayout("hierarchical")); -// let queryToSend = ''; -// let str = ''; -// setError(null); - - -// if (selectedMaterialNames.length > 0) { -// str = selectedMaterialNames.map((gr) => `'${gr}'`).join(','); -// queryToSend = `g.V().has("Material", "name", within(${str})).emit().repeat(in())`; -// sendRequest(queryToSend); -// } -// }; - -// const handleClear = () => { -// dispatch(clearGraph()); -// } - -// const sendRequest = (query: string) => { -// axios -// .post( -// QUERY_ENDPOINT, -// { host, port, query, nodeLimit }, -// { headers: { 'Content-Type': 'application/json' } } -// ) -// .then((response) => { -// onFetchQuery(response, query, nodeLabels, dispatch); -// }) -// .catch((error) => { -// console.warn(error) -// setError(COMMON_GREMLIN_ERROR); -// }); -// } -// const [selectedProperty, setSelectedProperty] = useState(null); -// const [value, setValue] = useState(''); - -// const handlePropertyChange = (event: React.ChangeEvent) => { -// setSelectedProperty(event.target.value); -// setValue(''); // Clear value when property changes -// }; - -// const handleValueChange = (event: React.ChangeEvent) => { -// setValue(event.target.value); -// }; - - -// return ( -// -// -// -// SELECT -// -// -// -// -// -// WHERE -// -// -// -// -// -// Select Material -// -// -// - -//
-// -// -//
{error}
- - -// -// {formFields.map((form, index) => ( -// -// -// handleFormChange(event, index)} -// fullWidth -// variant="standard" -// /> -// -// -// handleFormChange(event, index)} -// fullWidth -// variant="standard" -// /> -// -// -// -// -// -// ))} -// -// -// -// -//
-// ); -// }; diff --git a/src/components/Details/QueryComponent.tsx b/src/components/Details/QueryComponent.tsx index a26e10a..7631ac9 100644 --- a/src/components/Details/QueryComponent.tsx +++ b/src/components/Details/QueryComponent.tsx @@ -9,6 +9,7 @@ import { COMMON_GREMLIN_ERROR, QUERY_ENDPOINT } from "../../constants"; import { onFetchQuery } from "../../logics/actionHelper"; import { RootState } from "../../app/store"; import { highlightNodesAndEdges } from "../../logics/graphImpl/visImpl"; +import { GRAPH_IMPL } from "../../constants"; const Query = ({ }) => { const dispatch = useDispatch() @@ -27,7 +28,9 @@ const Query = ({ }) => { function sendQuery() { dispatch(setError(null)); - highlightNodesAndEdges(null, null); + if (GRAPH_IMPL === 'vis') { + highlightNodesAndEdges(null, null); + } axios .post( QUERY_ENDPOINT, From efd1037bd46ff855f41cfa55a58676900bce9610 Mon Sep 17 00:00:00 2001 From: joon-at-sri Date: Thu, 8 Aug 2024 14:23:37 -0700 Subject: [PATCH 04/10] change host to gremlin-server --- src/reducers/gremlinReducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reducers/gremlinReducer.ts b/src/reducers/gremlinReducer.ts index 7ece5e9..cbd9157 100644 --- a/src/reducers/gremlinReducer.ts +++ b/src/reducers/gremlinReducer.ts @@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { RootState } from '../app/store'; const initialState = { - host: 'localhost', + host: 'gremlin-server', port: '8182', query: 'g.V()', error: null From 400f88065af3d4bc1005a6d14a314e9fed260b48 Mon Sep 17 00:00:00 2001 From: joon-at-sri Date: Fri, 9 Aug 2024 13:15:25 -0700 Subject: [PATCH 05/10] WIP OR --- src/app/store.ts | 3 +- .../Details/QueryBuilderComponent.tsx | 242 +++++++++++------- src/components/Header/HeaderComponent.tsx | 69 +++-- src/reducers/gremlinReducer.ts | 2 +- src/reducers/queryBuilderReducer.ts | 94 +++++++ 5 files changed, 270 insertions(+), 140 deletions(-) create mode 100644 src/reducers/queryBuilderReducer.ts diff --git a/src/app/store.ts b/src/app/store.ts index 2fb7491..6b5a850 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -3,13 +3,14 @@ import gremlinReducer from '../reducers/gremlinReducer'; import graphReducer from '../reducers/graphReducer'; import optionReducer from '../reducers/optionReducer'; import dialogReducer from '../reducers/dialogReducer'; +import queryBuilderReducer from '../reducers/queryBuilderReducer'; import { useDispatch } from "react-redux"; // const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; export const setupStore = (preloadedState: any = {}) => configureStore({ - reducer: { gremlin: gremlinReducer, graph: graphReducer, options: optionReducer, dialog: dialogReducer }, + reducer: { gremlin: gremlinReducer, graph: graphReducer, options: optionReducer, dialog: dialogReducer, queryBuilder: queryBuilderReducer }, preloadedState // composeEnhancers(applyMiddleware(createLogger())) }); diff --git a/src/components/Details/QueryBuilderComponent.tsx b/src/components/Details/QueryBuilderComponent.tsx index e6700ad..6a52a50 100644 --- a/src/components/Details/QueryBuilderComponent.tsx +++ b/src/components/Details/QueryBuilderComponent.tsx @@ -22,20 +22,24 @@ import { EditText } from 'react-edit-text'; import ClearIcon from '@mui/icons-material/Clear'; import { highlightNodesAndEdges } from '../../logics/graphImpl/visImpl'; import { GRAPH_IMPL } from '../../constants'; +import { addWhereField, removeWhereField, selectQueryBuilder, setClause, setPropertyName, setPropertyValue, setSelectedType } from '../../reducers/queryBuilderReducer'; -type FormField = { +//g.V().hasLabel('Vertex').or(__.has('name', 'Jill'), __.has('name', 'Bob')) + + +export type WhereField = { propertyName: string; whereClause: string; propertyValue: string; operator: string; }; -const OPERATORS = { +export const OPERATORS = { NONE: 'none', AND: 'And', OR: 'Or' }; -const CLAUSES = { +export const CLAUSES = { GREATER_THAN: '>', LESS_THAN: '<', GREATER_THAN_EQ: '>=', @@ -46,71 +50,48 @@ const CLAUSES = { export const QueryBuilder = () => { const { nodeLabels, nodeLimit } = useSelector(selectOptions); - const { selectorNodes } = useSelector(selectGraph); const { suggestions } = useSelector(selectDialog); const dispatch = useDispatch(); const { host, port } = useSelector(selectGremlin); - const [formFields, setFormFields] = useState([{ propertyName: '', whereClause: '', propertyValue: '', operator: OPERATORS.NONE }]); const types = ["Component", "Entity", "Material"]; - const [selectedType, setSelectedType] = React.useState(""); - const [whereOptions, setWhereOptions] = React.useState([]); + const { selectedType, whereFields, whereOptions } = useSelector(selectQueryBuilder); const addFields = (operator: string) => { - let object = { - propertyName: '', - whereClause: '', - propertyValue: '', - operator: operator, - }; - setFormFields([...formFields, object]); + dispatch(addWhereField(operator)); }; const removeFields = (index: number) => { - let data = [...formFields]; - data.splice(index, 1); - setFormFields(data); + dispatch(removeWhereField(index)); }; const handleSelectChange = (event: SelectChangeEvent) => { const { value } = event.target - setSelectedType(value); - setWhereOptions(suggestions[DIALOG_TYPES.NODE]?.labels[value] ?? []); + const selectedType = value; + dispatch(setSelectedType({ selectedType, suggestions })) }; const handleWhereChange = (event: SelectChangeEvent, index: number) => { - const { name, value } = event.target; - setFormFields(prevFormFields => - prevFormFields.map((formField, i) => - i === index ? { ...formField, propertyName: value } : formField - ) - ); + const { value } = event.target; + dispatch(setPropertyName({ value, index })); }; const handleClauseChange = (event: SelectChangeEvent, index: number) => { - const { name, value } = event.target; - setFormFields(prevFormFields => - prevFormFields.map((formField, i) => - i === index ? { ...formField, whereClause: value } : formField - ) - ); + const { value } = event.target; + dispatch(setClause({ value, index })); }; const handleValueChange = (event: React.ChangeEvent, index: number) => { - const { name, value } = event.target; - setFormFields(prevFormFields => - prevFormFields.map((formField, i) => - i === index ? { ...formField, propertyValue: value } : formField - ) - ); + const { value } = event.target; + dispatch(setPropertyValue({ value, index })) }; const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); let query = `g.V().hasLabel('${selectedType}')`; - console.log(formFields) + console.log(whereFields) - formFields.forEach((form) => { + whereFields.forEach((form) => { let expression; switch (form.whereClause) { @@ -144,47 +125,111 @@ export const QueryBuilder = () => { break; } - console.log(query); }) - + console.log(buildNestedQuery(whereFields)); dispatch(clearGraph()); dispatch(setError(null)); + console.log(query) if (GRAPH_IMPL === 'vis') { highlightNodesAndEdges(null, null); - } + } axios - .post( - QUERY_ENDPOINT, - { host, port, query, nodeLimit }, - { headers: { 'Content-Type': 'application/json' } } - ) - .then((response) => { - onFetchQuery(response, query, nodeLabels, dispatch); - }) - .catch((error) => { - console.warn(error) - dispatch(setError(COMMON_GREMLIN_ERROR)); - }); + .post( + QUERY_ENDPOINT, + { host, port, query, nodeLimit }, + { headers: { 'Content-Type': 'application/json' } } + ) + .then((response) => { + onFetchQuery(response, query, nodeLabels, dispatch); + }) + .catch((error) => { + console.warn(error) + dispatch(setError(COMMON_GREMLIN_ERROR)); + }); + } + + function buildNestedQuery(whereFields: WhereField[]): string { + if (whereFields.length === 0) { + return "g.V()"; // Return a base traversal string if no conditions + } + + function convertClauseToPredicate(clause: string, value: any) { + switch (clause) { + case CLAUSES.GREATER_THAN: + return `P.gt(${JSON.stringify(value)})`; + case CLAUSES.GREATER_THAN_EQ: + return `P.gte(${JSON.stringify(value)})`; + case CLAUSES.LESS_THAN: + return `P.lt(${JSON.stringify(value)})`; + case CLAUSES.LESS_THAN_EQ: + return `P.lte(${JSON.stringify(value)})`; + case CLAUSES.EQUAL: + default: + return JSON.stringify(value); // For equality, the value is used directly + } + } + + function buildTraversal(fields: WhereField[]): string { + if (fields.length === 0) return ""; + + const firstField = fields[0]; + const restFields = fields.slice(1); + + const predicate = convertClauseToPredicate(firstField.whereClause, firstField.propertyValue); + const currentStep = `.has('${firstField.propertyName}', ${predicate})`; + + if (firstField.operator === OPERATORS.NONE) { + if (restFields.length === 0) { + return currentStep; + } else { + return `${currentStep}.and(${buildTraversal(restFields)})`; + } + } else if (firstField.operator === OPERATORS.AND) { + return `${currentStep}.and(${buildTraversal(restFields)})`; + } else if (firstField.operator === OPERATORS.OR) { + return `${currentStep}.or(${buildTraversal(restFields)})`; + } + return ""; + } + + return `g.V().hasLabel('Entity')${buildTraversal(whereFields)}`; } + /* + BC: if whereFields.length == 1 + const whereFields: WhereField[] = [ + { propertyName: 'country', whereClause: CLAUSES.EQUAL, propertyValue: 'Denmark', operator: OPERATORS.OR }, + { propertyName: 'risk', whereClause: CLAUSES.EQUAL, propertyValue: 'high', operator: OPERATORS.AND }, + { propertyName: 'population', whereClause: CLAUSES.GREATER_THAN, propertyValue: 5000000, operator: OPERATORS.AND }, + { propertyName: 'stability', whereClause: CLAUSES.EQUAL, propertyValue: 'low', operator: OPERATORS.NONE }, +]; + +looks like: g.V().hasLabel("Entity").and(AND(population > 50000, 1{(stability == low)}) + +base: + + */ - const whereRow = (form: FormField, index: number) => { + + const whereRow = (form: WhereField, index: number) => { + console.log(index); + console.log(whereFields[index]) return ( - {formFields[index].operator !== OPERATORS.NONE && ( + {whereFields[index].operator !== OPERATORS.NONE && ( - {formFields[index].operator} + {whereFields[index].operator} )} - + WHERE handleClauseChange(event, index)} - sx={{ height: '100%'}} + sx={{ height: '100%' }} MenuProps={{ anchorOrigin: { vertical: 'bottom', @@ -248,20 +293,21 @@ export const QueryBuilder = () => { - + handleValueChange(event, index)} fullWidth - sx={{ width: "100%", border: '1px solid #ccc', - borderRadius: '4px', - paddingBottom: '8px', margin: '0px', paddingLeft: '10px',boxSizing: 'border-box'}} + sx={{ + width: "100%", border: '1px solid #ccc', + borderRadius: '4px', + paddingBottom: '8px', margin: '0px', paddingLeft: '10px', boxSizing: 'border-box' + }} variant="standard" - + InputLabelProps={{ sx: { marginLeft: '8px' @@ -314,36 +360,36 @@ export const QueryBuilder = () => { -
- - } - aria-controls="panel2-content" - id="panel2-header" - > - WHERE - - - - {formFields.map((form, index) => ( - - {whereRow(form, index)} - - ))} - - - - + + + - - - - + +
); diff --git a/src/components/Header/HeaderComponent.tsx b/src/components/Header/HeaderComponent.tsx index 22d0d3b..120e862 100644 --- a/src/components/Header/HeaderComponent.tsx +++ b/src/components/Header/HeaderComponent.tsx @@ -11,7 +11,7 @@ import style from './HeaderComponent.module.css'; import { Edge, Node } from 'vis-network'; import _ from 'lodash'; import { selectGraph, setSuppliers } from '../../reducers/graphReducer'; -import { selectGremlin, setQuery, } from '../../reducers/gremlinReducer'; +import { selectGremlin, setError, setQuery, } from '../../reducers/gremlinReducer'; import axios from 'axios'; interface HeaderComponentProps { @@ -21,9 +21,8 @@ interface HeaderComponentProps { export const HeaderComponent = (props: HeaderComponentProps) => { const { nodeLabels, nodeLimit } = useSelector(selectOptions); const { components, suppliers, materials } = useSelector(selectGraph); - const [error, setError] = useState(null); const dispatch = useDispatch(); - const { host, port } = useSelector(selectGremlin); + const { host, port, error } = useSelector(selectGremlin); useEffect(() => { onChange(); @@ -32,7 +31,7 @@ export const HeaderComponent = (props: HeaderComponentProps) => { const onChange = () => { let queryToSend = ''; let str = ''; - setError(null); + dispatch(setError(null)); if (suppliers.length > 0) { str = suppliers.map((gr) => `'${gr}'`).join(','); queryToSend = `g.V().has("Entity", "name", within(${str})).emit().repeat(out())`; @@ -62,47 +61,37 @@ export const HeaderComponent = (props: HeaderComponentProps) => { }) .catch((error) => { console.warn(error) - setError(COMMON_GREMLIN_ERROR); + dispatch(setError(COMMON_GREMLIN_ERROR)); }); } return ( - - - - - - - - - - - -
-
{error}
+ + + + + + + + + + + + + {error &&
+ {error} +
}
- // - // - // - // - // - // - // - // - // - // - //
{error}
- //
+ ); }; diff --git a/src/reducers/gremlinReducer.ts b/src/reducers/gremlinReducer.ts index cbd9157..7ece5e9 100644 --- a/src/reducers/gremlinReducer.ts +++ b/src/reducers/gremlinReducer.ts @@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { RootState } from '../app/store'; const initialState = { - host: 'gremlin-server', + host: 'localhost', port: '8182', query: 'g.V()', error: null diff --git a/src/reducers/queryBuilderReducer.ts b/src/reducers/queryBuilderReducer.ts new file mode 100644 index 0000000..a7b4103 --- /dev/null +++ b/src/reducers/queryBuilderReducer.ts @@ -0,0 +1,94 @@ +// import vis from 'vis-network'; +import { createSlice } from '@reduxjs/toolkit'; +import { RootState } from '../app/store'; +import _ from 'lodash'; +import { defaultNodeLabel, EdgeData, NodeData } from "../logics/utils"; +import { Workspace } from '../components/Details/SettingsComponent'; +import { CLAUSES, OPERATORS, WhereField } from '../components/Details/QueryBuilderComponent'; +import { DIALOG_TYPES } from '../components/ModalDialog/ModalDialogComponent'; + +type QueryBuilderState = { + selectedType: string, + whereFields: WhereField[], + whereOptions: string[] +}; + +const initialState: QueryBuilderState = { + selectedType: "", + whereFields: [{ propertyName: '', whereClause: CLAUSES.EQUAL, propertyValue: '', operator: OPERATORS.NONE }], + whereOptions: [] + +}; + +const slice = createSlice({ + name: 'queryBuilder', + initialState, + reducers: { + setSelectedType: (state, action) => { + const {selectedType, suggestions} = action.payload + state.selectedType = selectedType + state.whereOptions = suggestions[DIALOG_TYPES.NODE]?.labels[selectedType] ?? []; + }, + setPropertyName: (state, action) => { + const { value, index } = action.payload + state.whereFields = state.whereFields.map((whereField, i) => + i === index ? { ...whereField, propertyName: value } : whereField + ); + }, + setClause: (state, action) => { + const { value, index } = action.payload + state.whereFields = state.whereFields.map((whereField, i) => + i === index ? { ...whereField, whereClause: value } : whereField + ); + }, + setPropertyValue: (state, action) => { + const { value, index } = action.payload + state.whereFields = state.whereFields.map((whereField, i) => + i === index ? { ...whereField, propertyValue: value } : whereField + ); + }, + addWhereField: (state, action) => { + const operator = action.payload + let object = { + propertyName: '', + whereClause: CLAUSES.EQUAL, + propertyValue: '', + operator: operator, + }; + state.whereFields = [...state.whereFields, object]; + }, + removeWhereField: (state, action) => { + const index = action.payload; + let data = [...state.whereFields]; + if (index == 0 && data.length > 1) { + data[1] = {...data[1], operator: OPERATORS.NONE} + } + data.splice(index, 1); + state.whereFields = data; + }, + setWhereOptions: (state, action) => { + const index = action.payload; + let data = [...state.whereFields]; + data.splice(index, 1); + state.whereFields = data; + }, + + + + + + }, +}); + +export const { + setSelectedType, + setPropertyName, + setClause, + setPropertyValue, + addWhereField, + removeWhereField +} = slice.actions; + +export const selectQueryBuilder = (state: RootState) => state.queryBuilder; + +export default slice.reducer; From 0896fa7e0af9daece4b31dcd687f61c884ed0ea1 Mon Sep 17 00:00:00 2001 From: joon-at-sri Date: Fri, 9 Aug 2024 14:24:32 -0700 Subject: [PATCH 06/10] nested somewhat but reverse direction --- .../Details/QueryBuilderComponent.tsx | 107 +++++++++++++----- src/reducers/queryBuilderReducer.ts | 7 +- 2 files changed, 83 insertions(+), 31 deletions(-) diff --git a/src/components/Details/QueryBuilderComponent.tsx b/src/components/Details/QueryBuilderComponent.tsx index 6a52a50..d0af89c 100644 --- a/src/components/Details/QueryBuilderComponent.tsx +++ b/src/components/Details/QueryBuilderComponent.tsx @@ -127,10 +127,11 @@ export const QueryBuilder = () => { }) - console.log(buildNestedQuery(whereFields)); dispatch(clearGraph()); dispatch(setError(null)); + query = buildNestedQuery(whereFields) console.log(query) + if (GRAPH_IMPL === 'vis') { highlightNodesAndEdges(null, null); } @@ -170,30 +171,29 @@ export const QueryBuilder = () => { } } - function buildTraversal(fields: WhereField[]): string { - if (fields.length === 0) return ""; + function buildTraversal(fields: WhereField[], isFirst: boolean): string { + const firstField = fields[0]; const restFields = fields.slice(1); const predicate = convertClauseToPredicate(firstField.whereClause, firstField.propertyValue); - const currentStep = `.has('${firstField.propertyName}', ${predicate})`; - - if (firstField.operator === OPERATORS.NONE) { - if (restFields.length === 0) { - return currentStep; - } else { - return `${currentStep}.and(${buildTraversal(restFields)})`; - } - } else if (firstField.operator === OPERATORS.AND) { - return `${currentStep}.and(${buildTraversal(restFields)})`; - } else if (firstField.operator === OPERATORS.OR) { - return `${currentStep}.or(${buildTraversal(restFields)})`; + const currentStep = `__.has('${firstField.propertyName}', ${predicate})`; + + const prefix = isFirst == true? '' : '__'; + + if (restFields.length === 0 && firstField.operator === OPERATORS.NONE) { + return currentStep; + } + else if (firstField.operator === OPERATORS.AND) { + return `${prefix}.and(${currentStep}, ${buildTraversal(restFields, false)})`; + } + else if (firstField.operator === OPERATORS.OR) { + return `${prefix}.or(${currentStep}, ${buildTraversal(restFields, false)})`; } return ""; } - - return `g.V().hasLabel('Entity')${buildTraversal(whereFields)}`; + return `g.V().hasLabel('${selectedType}')${buildTraversal(whereFields, true)}`; } /* @@ -203,25 +203,67 @@ export const QueryBuilder = () => { { propertyName: 'risk', whereClause: CLAUSES.EQUAL, propertyValue: 'high', operator: OPERATORS.AND }, { propertyName: 'population', whereClause: CLAUSES.GREATER_THAN, propertyValue: 5000000, operator: OPERATORS.AND }, { propertyName: 'stability', whereClause: CLAUSES.EQUAL, propertyValue: 'low', operator: OPERATORS.NONE }, -]; - -looks like: g.V().hasLabel("Entity").and(AND(population > 50000, 1{(stability == low)}) - -base: - + ]; + + + { propertyName: 'stability', whereClause: CLAUSES.EQUAL, propertyValue: 'low', operator: OPERATORS.NONE }, + { propertyName: 'population', whereClause: CLAUSES.GREATER_THAN, propertyValue: 5000000, operator: OPERATORS.AND }, + { propertyName: 'risk', whereClause: CLAUSES.EQUAL, propertyValue: 'high', operator: OPERATORS.AND }, + { propertyName: 'country', whereClause: CLAUSES.EQUAL, propertyValue: 'Denmark', operator: OPERATORS.OR }, + + + return OR(country, + + process OR(country, risk) + return AND(OR(country, risk), + + return AND(AND(OR(country, risk), population), + + return AND(AND(AND)) + + + looks like: g.V().hasLabel(Entity).AND(stability, .AND(population, OR(risk, country))) + + base: + + + + + g.V().hasLabel('Entity').and( + __.or( + __.and( + __.has('country', 'Denmark'), + __.has('risk', 'high') + ), + __.and( + __.has('country', 'Norway'), + __.has('population', P.gt(5000000)) + ) + ), + __.has('stability', 'low') +) + + + +g.V().hasLabel('Entity').and( + __.or( + __.and( + __.has('country', 'Denmark'), + __.has('risk', 'high') + ), + __.and( + __.has('country', 'Norway'), + __.has('population', P.gt(5000000)) + ) + ), + __.has('stability', 'low') +) */ const whereRow = (form: WhereField, index: number) => { - console.log(index); - console.log(whereFields[index]) return ( - {whereFields[index].operator !== OPERATORS.NONE && ( - - {whereFields[index].operator} - - )} @@ -319,6 +361,11 @@ base: + {whereFields[index].operator !== OPERATORS.NONE && ( + + {whereFields[index].operator} + + )} ) } diff --git a/src/reducers/queryBuilderReducer.ts b/src/reducers/queryBuilderReducer.ts index a7b4103..b20351c 100644 --- a/src/reducers/queryBuilderReducer.ts +++ b/src/reducers/queryBuilderReducer.ts @@ -49,11 +49,16 @@ const slice = createSlice({ }, addWhereField: (state, action) => { const operator = action.payload + const length = state.whereFields.length; + if (length > 0) { + state.whereFields[length - 1].operator = operator; + } + let object = { propertyName: '', whereClause: CLAUSES.EQUAL, propertyValue: '', - operator: operator, + operator: OPERATORS.NONE, }; state.whereFields = [...state.whereFields, object]; }, From 5ba6050f509133018a5d5337916e5d17c798c672 Mon Sep 17 00:00:00 2001 From: joon-at-sri Date: Fri, 9 Aug 2024 14:55:08 -0700 Subject: [PATCH 07/10] or working --- .../Details/QueryBuilderComponent.tsx | 147 +++--------------- src/reducers/queryBuilderReducer.ts | 30 ++-- 2 files changed, 31 insertions(+), 146 deletions(-) diff --git a/src/components/Details/QueryBuilderComponent.tsx b/src/components/Details/QueryBuilderComponent.tsx index d0af89c..a9a2e65 100644 --- a/src/components/Details/QueryBuilderComponent.tsx +++ b/src/components/Details/QueryBuilderComponent.tsx @@ -24,8 +24,6 @@ import { highlightNodesAndEdges } from '../../logics/graphImpl/visImpl'; import { GRAPH_IMPL } from '../../constants'; import { addWhereField, removeWhereField, selectQueryBuilder, setClause, setPropertyName, setPropertyValue, setSelectedType } from '../../reducers/queryBuilderReducer'; -//g.V().hasLabel('Vertex').or(__.has('name', 'Jill'), __.has('name', 'Bob')) - export type WhereField = { propertyName: string; @@ -88,49 +86,10 @@ export const QueryBuilder = () => { const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); - let query = `g.V().hasLabel('${selectedType}')`; - console.log(whereFields) - - whereFields.forEach((form) => { - let expression; - - switch (form.whereClause) { - case CLAUSES.EQUAL: - expression = `'${form.propertyValue}'`; - break; - case CLAUSES.GREATER_THAN: - expression = `P.gt(${form.propertyValue})`; - break; - case CLAUSES.GREATER_THAN_EQ: - expression = `P.gte(${form.propertyValue})`; - break; - case CLAUSES.LESS_THAN: - expression = `P.lt(${form.propertyValue})`; - break; - case CLAUSES.LESS_THAN_EQ: - expression = `P.lte(${form.propertyValue})`; - break - } - expression = `('${form.propertyName}', ${expression})`; - - switch (form.operator) { - case OPERATORS.NONE: - query += `.has${expression}`; - break; - case OPERATORS.AND: - query += `.and(__.has${expression})`; - break; - case OPERATORS.OR: - query += `.or(__.has${expression})`; - break; - } - - }) dispatch(clearGraph()); dispatch(setError(null)); - query = buildNestedQuery(whereFields) - console.log(query) + const query = buildNestedQuery(whereFields) if (GRAPH_IMPL === 'vis') { highlightNodesAndEdges(null, null); @@ -152,118 +111,61 @@ export const QueryBuilder = () => { function buildNestedQuery(whereFields: WhereField[]): string { if (whereFields.length === 0) { - return "g.V()"; // Return a base traversal string if no conditions + return "g.V()"; } - function convertClauseToPredicate(clause: string, value: any) { switch (clause) { case CLAUSES.GREATER_THAN: - return `P.gt(${JSON.stringify(value)})`; + return `P.gt(${value})`; case CLAUSES.GREATER_THAN_EQ: - return `P.gte(${JSON.stringify(value)})`; + return `P.gte(${value})`; case CLAUSES.LESS_THAN: - return `P.lt(${JSON.stringify(value)})`; + return `P.lt(${value})`; case CLAUSES.LESS_THAN_EQ: - return `P.lte(${JSON.stringify(value)})`; + return `P.lte(${value})`; case CLAUSES.EQUAL: default: - return JSON.stringify(value); // For equality, the value is used directly + return `'${value}'`; } } function buildTraversal(fields: WhereField[], isFirst: boolean): string { - - const firstField = fields[0]; const restFields = fields.slice(1); const predicate = convertClauseToPredicate(firstField.whereClause, firstField.propertyValue); const currentStep = `__.has('${firstField.propertyName}', ${predicate})`; - const prefix = isFirst == true? '' : '__'; + const prefix = isFirst == true ? '' : '__'; if (restFields.length === 0 && firstField.operator === OPERATORS.NONE) { - return currentStep; + if (isFirst) { + return `.has('${firstField.propertyName}', ${predicate})`; + } + else { + return currentStep; + } } else if (firstField.operator === OPERATORS.AND) { return `${prefix}.and(${currentStep}, ${buildTraversal(restFields, false)})`; - } + } else if (firstField.operator === OPERATORS.OR) { return `${prefix}.or(${currentStep}, ${buildTraversal(restFields, false)})`; } return ""; } - return `g.V().hasLabel('${selectedType}')${buildTraversal(whereFields, true)}`; + let reversedWhereFields = [...whereFields].reverse(); + return `g.V().hasLabel('${selectedType}')${buildTraversal(reversedWhereFields, true)}`; } - /* - BC: if whereFields.length == 1 - const whereFields: WhereField[] = [ - { propertyName: 'country', whereClause: CLAUSES.EQUAL, propertyValue: 'Denmark', operator: OPERATORS.OR }, - { propertyName: 'risk', whereClause: CLAUSES.EQUAL, propertyValue: 'high', operator: OPERATORS.AND }, - { propertyName: 'population', whereClause: CLAUSES.GREATER_THAN, propertyValue: 5000000, operator: OPERATORS.AND }, - { propertyName: 'stability', whereClause: CLAUSES.EQUAL, propertyValue: 'low', operator: OPERATORS.NONE }, - ]; - - - { propertyName: 'stability', whereClause: CLAUSES.EQUAL, propertyValue: 'low', operator: OPERATORS.NONE }, - { propertyName: 'population', whereClause: CLAUSES.GREATER_THAN, propertyValue: 5000000, operator: OPERATORS.AND }, - { propertyName: 'risk', whereClause: CLAUSES.EQUAL, propertyValue: 'high', operator: OPERATORS.AND }, - { propertyName: 'country', whereClause: CLAUSES.EQUAL, propertyValue: 'Denmark', operator: OPERATORS.OR }, - - - return OR(country, - - process OR(country, risk) - return AND(OR(country, risk), - - return AND(AND(OR(country, risk), population), - - return AND(AND(AND)) - - - looks like: g.V().hasLabel(Entity).AND(stability, .AND(population, OR(risk, country))) - - base: - - - - - g.V().hasLabel('Entity').and( - __.or( - __.and( - __.has('country', 'Denmark'), - __.has('risk', 'high') - ), - __.and( - __.has('country', 'Norway'), - __.has('population', P.gt(5000000)) - ) - ), - __.has('stability', 'low') -) - - - -g.V().hasLabel('Entity').and( - __.or( - __.and( - __.has('country', 'Denmark'), - __.has('risk', 'high') - ), - __.and( - __.has('country', 'Norway'), - __.has('population', P.gt(5000000)) - ) - ), - __.has('stability', 'low') -) - */ - - const whereRow = (form: WhereField, index: number) => { return ( + {whereFields[index].operator !== OPERATORS.NONE && ( + + {whereFields[index].operator} + + )} @@ -361,11 +263,6 @@ g.V().hasLabel('Entity').and( - {whereFields[index].operator !== OPERATORS.NONE && ( - - {whereFields[index].operator} - - )} ) } diff --git a/src/reducers/queryBuilderReducer.ts b/src/reducers/queryBuilderReducer.ts index b20351c..80a4bf3 100644 --- a/src/reducers/queryBuilderReducer.ts +++ b/src/reducers/queryBuilderReducer.ts @@ -2,8 +2,6 @@ import { createSlice } from '@reduxjs/toolkit'; import { RootState } from '../app/store'; import _ from 'lodash'; -import { defaultNodeLabel, EdgeData, NodeData } from "../logics/utils"; -import { Workspace } from '../components/Details/SettingsComponent'; import { CLAUSES, OPERATORS, WhereField } from '../components/Details/QueryBuilderComponent'; import { DIALOG_TYPES } from '../components/ModalDialog/ModalDialogComponent'; @@ -25,40 +23,35 @@ const slice = createSlice({ initialState, reducers: { setSelectedType: (state, action) => { - const {selectedType, suggestions} = action.payload + const { selectedType, suggestions } = action.payload state.selectedType = selectedType state.whereOptions = suggestions[DIALOG_TYPES.NODE]?.labels[selectedType] ?? []; }, setPropertyName: (state, action) => { const { value, index } = action.payload state.whereFields = state.whereFields.map((whereField, i) => - i === index ? { ...whereField, propertyName: value } : whereField - ); + i === index ? { ...whereField, propertyName: value } : whereField + ); }, setClause: (state, action) => { const { value, index } = action.payload state.whereFields = state.whereFields.map((whereField, i) => - i === index ? { ...whereField, whereClause: value } : whereField - ); + i === index ? { ...whereField, whereClause: value } : whereField + ); }, setPropertyValue: (state, action) => { const { value, index } = action.payload state.whereFields = state.whereFields.map((whereField, i) => - i === index ? { ...whereField, propertyValue: value } : whereField - ); + i === index ? { ...whereField, propertyValue: value } : whereField + ); }, addWhereField: (state, action) => { const operator = action.payload - const length = state.whereFields.length; - if (length > 0) { - state.whereFields[length - 1].operator = operator; - } - let object = { propertyName: '', whereClause: CLAUSES.EQUAL, propertyValue: '', - operator: OPERATORS.NONE, + operator: operator, }; state.whereFields = [...state.whereFields, object]; }, @@ -66,7 +59,7 @@ const slice = createSlice({ const index = action.payload; let data = [...state.whereFields]; if (index == 0 && data.length > 1) { - data[1] = {...data[1], operator: OPERATORS.NONE} + data[1] = { ...data[1], operator: OPERATORS.NONE } } data.splice(index, 1); state.whereFields = data; @@ -77,11 +70,6 @@ const slice = createSlice({ data.splice(index, 1); state.whereFields = data; }, - - - - - }, }); From c8c06fe72d3447858e2fc907e38dd3d9466a7a80 Mon Sep 17 00:00:00 2001 From: joon-at-sri Date: Fri, 9 Aug 2024 14:59:40 -0700 Subject: [PATCH 08/10] changed to gremlin-server --- src/reducers/gremlinReducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reducers/gremlinReducer.ts b/src/reducers/gremlinReducer.ts index 7ece5e9..cbd9157 100644 --- a/src/reducers/gremlinReducer.ts +++ b/src/reducers/gremlinReducer.ts @@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { RootState } from '../app/store'; const initialState = { - host: 'localhost', + host: 'gremlin-server', port: '8182', query: 'g.V()', error: null From e5cf2d90284105446c01bb31ff11b2e498be9b10 Mon Sep 17 00:00:00 2001 From: joon-at-sri Date: Fri, 9 Aug 2024 15:29:24 -0700 Subject: [PATCH 09/10] no condition and adding handling --- .../Details/QueryBuilderComponent.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/Details/QueryBuilderComponent.tsx b/src/components/Details/QueryBuilderComponent.tsx index a9a2e65..323f1b1 100644 --- a/src/components/Details/QueryBuilderComponent.tsx +++ b/src/components/Details/QueryBuilderComponent.tsx @@ -322,12 +322,20 @@ export const QueryBuilder = () => { ))} - - + {whereFields.length === 0 ? ( + + ) : ( + <> + + + + )}