diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index eae4343098ce499729772044bf9565c46bb98046..c563ddb44d8157dcec0d42d04ddb8c4de469e554 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -2,6 +2,14 @@ @tailwind components; @tailwind utilities; +@layer base { + input[type='number']::-webkit-inner-spin-button, + input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } +} + * { font-family: 'Inter', ubuntu, sans-serif; box-sizing: border-box; diff --git a/libs/shared/lib/components/Popup.tsx b/libs/shared/lib/components/Popup.tsx index 8a1ed7cd0a53fc6c8c9de1df2a31663cd1290c98..13141bde35daacaae748891aca0c55ce085ba963 100644 --- a/libs/shared/lib/components/Popup.tsx +++ b/libs/shared/lib/components/Popup.tsx @@ -3,7 +3,7 @@ export const Popup = (props: { children: React.ReactNode; open: boolean; hAnchor <> {props.open && ( <div - className="absolute z-10 max-w-[20rem] bg-offwhite-100 flex flex-col gap-2 animate-openmenu" + className="absolute z-10 max-w-[20rem] bg-white flex flex-col gap-2 animate-openmenu p-0 m-0" style={props.hAnchor === 'left' ? { left: '5rem' } : { right: '5rem' }} > {props.children} diff --git a/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx b/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx index 22239739ba561a9441014981b76516ea0def2226..c7744b58e2a5f156cbd9c16d56ed4e773a451205 100644 --- a/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx +++ b/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx @@ -2,8 +2,9 @@ import React, { ReactNode, useEffect, useState } from 'react'; import Broker from '@graphpolaris/shared/lib/data-access/socket/broker'; import { useImmer } from 'use-immer'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import { useConfig } from '../store'; +import { useAppDispatch, useConfig } from '../store'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import { removeLastError, removeLastWarning } from '../store/configSlice'; type Message = { message: ReactNode; @@ -14,8 +15,9 @@ export const DashboardAlerts = (props: { timer?: number }) => { const [messages, setMessages] = useImmer<{ data: any; routingKey: string; message: Message; showing: boolean }[]>([]); const timer = props.timer || 1200; const config = useConfig(); + const dispatch = useAppDispatch(); - function processMessage(message: Message | undefined, data: any, routingKey: string) { + async function processMessage(message: Message | undefined, data: any, routingKey: string) { // Queue the message to be shown for props.time time, then fade it out matching the animation of animate-closemenu fade out time if (message) { setMessages((draft) => { @@ -34,6 +36,7 @@ export const DashboardAlerts = (props: { timer?: number }) => { draft.pop(); return draft; }); + return true; }, 300); // matches the animation of animate-closemenu }, timer); } @@ -70,9 +73,9 @@ export const DashboardAlerts = (props: { timer?: number }) => { }; }, []); - useEffect(() => { + async function processError() { if (config.errors.length > 0) { - processMessage( + await processMessage( { message: ( <> @@ -84,9 +87,36 @@ export const DashboardAlerts = (props: { timer?: number }) => { undefined, 'error' ); + dispatch(removeLastError()); } + } + + async function processWarning() { + if (config.warnings.length > 0) { + await processMessage( + { + message: ( + <> + <ErrorOutlineIcon /> {config.warnings[config.warnings.length - 1]} + </> + ), + className: 'alert-warning', + }, + undefined, + 'warning' + ); + dispatch(removeLastWarning()); + } + } + + useEffect(() => { + processError(); }, [config.errors]); + useEffect(() => { + processWarning(); + }, [config.warnings]); + return ( <> {messages && diff --git a/libs/shared/lib/data-access/store/configSlice.ts b/libs/shared/lib/data-access/store/configSlice.ts index eea8aaf9f98d98ddb2d3cb24c9b86b2b418ae176..2d6d2393ceb73ccfd257dbd339320e0896bde9e0 100644 --- a/libs/shared/lib/data-access/store/configSlice.ts +++ b/libs/shared/lib/data-access/store/configSlice.ts @@ -13,6 +13,7 @@ export const initialState: { elementsperDatabaseObject: Record<string, number>; autoSendQueries: boolean; errors: string[]; + warnings: string[]; } = { queryListOpen: false, queryStatusList: { queries: {}, queryIDsOrder: [] }, @@ -21,6 +22,7 @@ export const initialState: { elementsperDatabaseObject: {}, autoSendQueries: true, errors: [], + warnings: [], }; export const configSlice = createSlice({ @@ -33,14 +35,19 @@ export const configSlice = createSlice({ state.errors.push(action.payload); }, removeLastError: (state) => { - console.log('Removing last error'); - state.errors.shift(); }, + addWarning: (state, action: PayloadAction<string>) => { + console.warn('Warn Received!', action.payload); + state.warnings.push(action.payload); + }, + removeLastWarning: (state) => { + state.warnings.shift(); + }, }, }); -export const { addError, removeLastError } = configSlice.actions; +export const { addError, removeLastError, addWarning, removeLastWarning } = configSlice.actions; // Other code such as selectors can use the imported `RootState` type export const configState = (state: RootState) => state.config; diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts index 425a5725532434ed881a4af2f9220e81b0fe11bb..ef4208ec551b950cf307ffeefa50e361492b475c 100644 --- a/libs/shared/lib/data-access/store/querybuilderSlice.ts +++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts @@ -6,6 +6,7 @@ import { QueryMultiGraph, QueryMultiGraphology as QueryGraphology } from '../../ export type QueryBuilderSettings = { limit: number; + depth: { min: number; max: number }; }; // Define the initial state using that type @@ -16,6 +17,7 @@ export const initialState: { graphologySerialized: new QueryGraphology().export(), settings: { limit: 500, + depth: { min: 0, max: 1 }, }, // schemaLayout: 'Graphology_noverlap', }; diff --git a/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx index 5e514f6ea8aed3ab9a3c38c6eb52f9a188f88fe5..4b0cbe9a3f382e439b0b10d910ab34a47cd65daa 100644 --- a/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx +++ b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx @@ -76,7 +76,7 @@ export interface FilterStruct { export interface RelationStruct { ID?: string; label?: string; - depth?: QuerySearchDepthStruct; + depth: QuerySearchDepthStruct; direction: 'TO' | 'FROM'; node?: NodeStruct; } diff --git a/libs/shared/lib/querybuilder/panel/querySettingsDialog.tsx b/libs/shared/lib/querybuilder/panel/querySettingsDialog.tsx index 91a6fd99ee2711568719d9ea4bb3195137ad70b4..ccd4ffda4ac0e3f47209e558692dfd83c491b92a 100644 --- a/libs/shared/lib/querybuilder/panel/querySettingsDialog.tsx +++ b/libs/shared/lib/querybuilder/panel/querySettingsDialog.tsx @@ -4,6 +4,7 @@ import React from 'react'; import CloseIcon from '@mui/icons-material/Close'; import { useAppDispatch, useQuerybuilderSettings } from '../../data-access'; import { QueryBuilderSettings, setQuerybuilderSettings } from '../../data-access/store/querybuilderSlice'; +import { addWarning } from '../../data-access/store/configSlice'; export const QuerySettingsDialog = React.forwardRef<HTMLDivElement, DialogProps>((props, ref) => { const qb = useQuerybuilderSettings(); @@ -14,6 +15,19 @@ export const QuerySettingsDialog = React.forwardRef<HTMLDivElement, DialogProps> setState(qb); }, [qb, props.open]); + function submit() { + if (state.depth.min < 0) { + dispatch(addWarning('The minimum depth cannot be smaller than 0')); + } else if (state.depth.max > 99) { + dispatch(addWarning('The maximum depth cannot be larger than 99')); + } else if (state.depth.min >= state.depth.max) { + dispatch(addWarning('The minimum depth cannot be larger than the maximum depth')); + } else { + dispatch(setQuerybuilderSettings(state)); + props.onClose(); + } + } + return ( <> {props.open && ( @@ -23,8 +37,7 @@ export const QuerySettingsDialog = React.forwardRef<HTMLDivElement, DialogProps> className="card-body px-0 w-72 py-5" onSubmit={(e) => { e.preventDefault(); - dispatch(setQuerybuilderSettings(state)); - props.onClose(); + submit(); }} > <div className="card-title p-5 py-0 flex w-full"> @@ -36,18 +49,58 @@ export const QuerySettingsDialog = React.forwardRef<HTMLDivElement, DialogProps> <div className="divider m-0"></div> <div className="form-control px-5"> <label className="label"> - <span className="label-text">Limit Value</span> + <span className="label-text">Limit - Max number of results</span> </label> <input type="number" className="input input-sm input-bordered" placeholder="500" - name="limit" value={state.limit} onChange={(e) => setState({ ...state, limit: parseInt(e.target.value) })} /> </div> <div className="divider m-0"></div> + <div className="form-control px-5 flex flex-row gap-3"> + <div className=""> + <label className="label"> + <span className="label-text">Min Depth Default</span> + </label> + <input + type="number" + className="input input-sm input-bordered w-full" + placeholder="0" + min={0} + max={state.depth.max - 1} + value={state.depth.min} + onChange={(e) => setState({ ...state, depth: { min: parseInt(e.target.value), max: state.depth.max } })} + onKeyDown={(e) => { + if (e.key === 'Enter') { + submit(); + } + }} + /> + </div> + <div className=""> + <label className="label"> + <span className="label-text">Max Depth Default</span> + </label> + <input + type="number" + className="input input-sm input-bordered w-full" + placeholder="0" + min={state.depth.min + 1} + max={99} + value={state.depth.max} + onChange={(e) => setState({ ...state, depth: { max: parseInt(e.target.value), min: state.depth.min } })} + onKeyDown={(e) => { + if (e.key === 'Enter') { + submit(); + } + }} + /> + </div> + </div> + <div className="divider m-0"></div> <div className="card-actions mt-1 w-full px-5 flex flex-row"> <button className="btn btn-secondary flex-grow" diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index 55bd8637be941f88f525666569eef61e47f2a5b3..0ba2b7ecefda28ac70e6a6b0ba630693962c7380 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -4,6 +4,7 @@ import { useConfig, useQuerybuilderGraph, useQuerybuilderGraphology, + useQuerybuilderSettings, useSchemaGraphology, } from '@graphpolaris/shared/lib/data-access/store'; import ReactFlow, { @@ -54,14 +55,15 @@ export type QueryBuilderProps = { onRunQuery?: () => void; }; +type SettingsPanel = 'settings' | 'ml' | 'logic' | undefined; + /** * This is the main querybuilder component. It is responsible for holding all pills and fire off the visual part of the querybuilder panel logic */ export const QueryBuilderInner = (props: QueryBuilderProps) => { - const [openLogicDialog, setOpenLogicDialog] = useState(false); - const [openMLDialog, setOpenMLDialog] = useState(false); - const [toggleSettings, setToggleSettings] = useState(false); + const [toggleSettings, setToggleSettings] = useState<SettingsPanel>(); const reactFlowWrapper = useRef<HTMLDivElement>(null); + const queryBuilderSettings = useQuerybuilderSettings(); var nodeTypes = { entity: EntityFlowElement, @@ -232,7 +234,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { type: QueryElementTypes.Relation, x: position.x, y: position.y, - depth: { min: 0, max: 1 }, + depth: { min: queryBuilderSettings.depth.min, max: queryBuilderSettings.depth.max }, name: dragData.collection, collection: dragData.collection, }, @@ -334,7 +336,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { const position = reactFlow.project({ x: clientX, y: clientY }); if (connectingNodeId?.current) connectingNodeId.current.position = position; - setOpenLogicDialog(true); + setToggleSettings('logic'); } }, [reactFlow.project] @@ -385,7 +387,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { } dispatch(setQuerybuilderNodes(graphologyGraph.export())); - setOpenLogicDialog(false); + setToggleSettings(undefined); connectingNodeId.current = null; }; @@ -439,12 +441,12 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { return ( <div ref={reactFlowWrapper} className="h-full w-full"> - <QuerySettingsDialog open={toggleSettings} onClose={() => setToggleSettings(false)} /> + <QuerySettingsDialog open={toggleSettings === 'settings'} onClose={() => setToggleSettings(undefined)} /> <Dialog - open={openLogicDialog} + open={toggleSettings === 'logic'} onClose={() => { - setOpenLogicDialog(false); + setToggleSettings(undefined); }} > <QueryBuilderLogicPillsPanel @@ -454,7 +456,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { onDrag={() => {}} /> </Dialog> - <Popup open={openMLDialog} hAnchor="right"> + <Popup open={toggleSettings === 'ml'} hAnchor="right"> <QueryBuilderMLPanel /> </Popup> <ReactFlow @@ -504,11 +506,12 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { <ExportIcon /> </ControlButton> <ControlButton - className={styles.buttons} + className={styles.buttons + (toggleSettings === 'settings' ? ' btn-active' : '')} title={'Other settings'} onClick={(event) => { event.stopPropagation(); - setToggleSettings(!toggleSettings); + if (toggleSettings === 'settings') setToggleSettings(undefined); + else setToggleSettings('settings'); }} > <SettingsIcon /> @@ -524,21 +527,23 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { <CachedIcon /> </ControlButton> <ControlButton - className={styles.buttons + (openLogicDialog ? ' btn-active' : '')} + className={styles.buttons + (toggleSettings === 'logic' ? ' btn-active' : '')} title={'Logic Pills'} onClick={(event) => { event.stopPropagation(); - setOpenLogicDialog(!openLogicDialog); + if (toggleSettings === 'logic') setToggleSettings(undefined); + else setToggleSettings('logic'); }} > <DifferenceIcon /> </ControlButton> <ControlButton - className={styles.buttons + (openMLDialog ? ' btn-active' : '')} + className={styles.buttons + (toggleSettings === 'ml' ? ' btn-active' : '')} title={'Machine Learning'} onClick={(event) => { event.stopPropagation(); - setOpenMLDialog(!openMLDialog); + if (toggleSettings === 'ml') setToggleSettings(undefined); + else setToggleSettings('ml'); }} > <LightbulbIcon /> diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderMLPanel.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderMLPanel.tsx index 5ba2dfec5fd9efe754c3d212f430ab973e341aa3..88290f2e29eedb445aecb3d0c8a42b00f4b06e58 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderMLPanel.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderMLPanel.tsx @@ -11,10 +11,14 @@ export const QueryBuilderMLPanel = () => { const ml = useML(); return ( - <div className="card w-max"> - <div className="card-body p-5"> - <h2>Machine Learning Options</h2> - <div className="form-control"> + <div className="card card-bordered w-max rounded-none p-0"> + <div className="card-body px-0 w-72 py-5"> + <div className="p-5 py-0 flex w-full"> + <h2>Machine Learning Options</h2> + </div> + <div className="divider m-0"></div> + + <div className="form-control px-5"> <label className="label cursor-pointer gap-2 w-fit"> <input type="checkbox" @@ -26,8 +30,6 @@ export const QueryBuilderMLPanel = () => { </label> {ml.linkPrediction.enabled && ml.linkPrediction.result && <span># of predictions: {ml.linkPrediction.result.length}</span>} {ml.linkPrediction.enabled && !ml.linkPrediction.result && <span>Loading...</span>} - </div> - <div className="form-control"> <label className="label cursor-pointer gap-2 w-fit"> <input type="checkbox" @@ -46,8 +48,6 @@ export const QueryBuilderMLPanel = () => { </span> )} {ml.centrality.enabled && Object.values(ml.centrality.result).length === 0 && <span>No Centers Found</span>} - </div> - <div className="form-control"> <label className="label cursor-pointer gap-2 w-fit"> <input type="checkbox" @@ -61,8 +61,6 @@ export const QueryBuilderMLPanel = () => { <span># of communities: {ml.communityDetection.result.length}</span> )} {ml.communityDetection.enabled && !ml.communityDetection.result && <span>Loading...</span>} - </div> - <div className="form-control"> <label className="label cursor-pointer gap-2 w-fit"> <input type="checkbox" diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx index 90c89512a7df84df25be0dd3fb459766a87c3d26..09501947bf5415ebfe28dcd5e7df0f57b41f6a9f 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx @@ -1,7 +1,15 @@ -import { memo, useRef, useState } from 'react'; +import { memo, useRef, useState, useMemo, useEffect } from 'react'; import { Handle, Position } from 'reactflow'; -import { SchemaReactflowRelationNode, toHandleId } from '../../../model'; +import { RelationNodeAttributes, SchemaReactflowRelationNode, toHandleId } from '../../../model'; import styles from './relationpill.module.scss'; +import { + setQuerybuilderNodes, + useAppDispatch, + useConfig, + useQuerybuilderGraphology, + useQuerybuilderSettings, +} from '@graphpolaris/shared/lib/data-access'; +import { addWarning } from '@graphpolaris/shared/lib/data-access/store/configSlice'; /** * Component to render a relation flow element @@ -9,38 +17,32 @@ import styles from './relationpill.module.scss'; */ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { const data = node.data; - const minRef = useRef<HTMLInputElement>(null); - const maxRef = useRef<HTMLInputElement>(null); - - const [readOnlyMin, setReadOnlyMin] = useState(true); - const [readOnlyMax, setReadOnlyMax] = useState(true); - - const onDepthChanged = (depth: string) => { - // Don't allow depth above 99 - const limit = 99; - if (data?.depth != undefined) { - data.depth.min = data.depth.min >= limit ? limit : data.depth.min; - data.depth.max = data.depth.max >= limit ? limit : data.depth.max; - - // Check for for valid depth: min <= max - if (depth == 'min') { - if (data.depth.min > data.depth.max) data.depth.max = data.depth.min; - setReadOnlyMin(true); - } else if (depth == 'max') { - if (data.depth.max < data.depth.min) data.depth.min = data.depth.max; - setReadOnlyMax(true); - } + const graphology = useQuerybuilderGraphology(); + const settings = useQuerybuilderSettings(); + const dispatch = useAppDispatch(); + const graphologyNodeAttributes = useMemo<RelationNodeAttributes | undefined>( + () => (graphology.hasNode(node.id) ? { ...(graphology.getNodeAttributes(node.id) as RelationNodeAttributes) } : undefined), + [node.id] + ); + const [depth, setDepth] = useState<{ min: number; max: number }>({ + min: data.depth.min || settings.depth.min, + max: data.depth.max || settings.depth.max, + }); - // Set to the correct width - if (maxRef.current) maxRef.current.style.maxWidth = calcWidth(data.depth.max); - if (minRef.current) minRef.current.style.maxWidth = calcWidth(data.depth.min); - } - }; + useEffect(() => { + setDepth({ min: data.depth.min || settings.depth.min, max: data.depth.max || settings.depth.max }); + }, [data.depth]); - const isNumber = (x: string) => { - { - if (typeof x != 'string') return false; - return !Number.isNaN(x) && !Number.isNaN(parseFloat(x)); + const onNodeUpdated = () => { + if (depth.min < 0) { + dispatch(addWarning('The minimum depth cannot be smaller than 0')); + } else if (depth.max > 99) { + dispatch(addWarning('The maximum depth cannot be larger than 99')); + } else if (depth.min >= depth.max) { + dispatch(addWarning('The minimum depth cannot be larger than the maximum depth')); + } else { + graphology.setNodeAttribute<any>(node.id, 'depth', depth); + dispatch(setQuerybuilderNodes(graphology.export())); } }; @@ -77,55 +79,47 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { <span className={styles.relationInputHolder}> <span>[</span> <input - className={styles.relationInput + ' ' + (readOnlyMin ? styles.relationReadonly : '')} - ref={minRef} - type="string" + className={ + 'bg-inherit text-center appearance-none mx-0.5 rounded-sm ' + + (depth.min < 0 || depth.min >= depth.max ? ' bg-red-400 ' : '') + } + style={{ maxWidth: calcWidth(depth.min) }} + type="number" min={0} - readOnly={readOnlyMin} placeholder={'?'} - value={data?.depth.min} + value={depth.min} onChange={(e) => { - if (data != undefined) { - data.depth.min = isNumber(e.target.value) ? parseInt(e.target.value) : 0; - e.target.style.maxWidth = calcWidth(data.depth.min); - } - }} - onDoubleClick={() => { - setReadOnlyMin(false); + setDepth({ ...depth, min: parseInt(e.target.value) }); }} onBlur={(e) => { - onDepthChanged('min'); + onNodeUpdated(); }} onKeyDown={(e) => { if (e.key === 'Enter') { - onDepthChanged('min'); + onNodeUpdated(); } }} ></input> <span>..</span> <input - className={styles.relationInput + ' ' + (readOnlyMax ? styles.relationReadonly : '')} - ref={maxRef} - type="string" - min={0} - readOnly={readOnlyMax} + className={ + 'bg-inherit text-center appearance-none mx-0.5 rounded-sm ' + + (depth.max > 99 || depth.min >= depth.max ? ' bg-red-400 ' : '') + } + style={{ maxWidth: calcWidth(depth.max) }} + type="number" + min={1} placeholder={'?'} - value={data?.depth.max} + value={depth.max} onChange={(e) => { - if (data != undefined) { - data.depth.max = isNumber(e.target.value) ? parseInt(e.target.value) : 0; - e.target.style.maxWidth = calcWidth(data.depth.max); - } - }} - onDoubleClick={() => { - setReadOnlyMax(false); + setDepth({ ...depth, max: parseInt(e.target.value) }); }} onBlur={(e) => { - onDepthChanged('max'); + onNodeUpdated(); }} onKeyDown={(e) => { if (e.key === 'Enter') { - onDepthChanged('max'); + onNodeUpdated(); } }} ></input> diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts index c709605ae6ac96f71bc192ea2c955e005af12851..1109d58a9c55450ab31085a3b35f67c90ec27074 100644 --- a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts @@ -16,6 +16,7 @@ const defaultQuery = { const defaultSettings = { limit: 500, + depth: { min: 0, max: 1 }, }; describe('QueryUtils Entity and Relations', () => { @@ -998,6 +999,7 @@ it('should no connections between entities and relations', () => { x: 100, y: 100, name: 'Relation 1', + depth: { min: 0, max: 1 }, }, [{ name: 'age', type: 'string' }] ); @@ -1031,6 +1033,7 @@ it('should no connections between entities and relations', () => { label: 'Relation 1', direction: 'TO', node: {}, + depth: { min: 0, max: 1 }, }, }, }, diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.ts index 5f662b11195a7a357654f751737bf3a355ba9062..4eb4c47a29f4def87af7614099c4a2b5d5cde1d5 100644 --- a/libs/shared/lib/querybuilder/query-utils/query2backend.ts +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.ts @@ -23,7 +23,8 @@ const traverseEntityRelationPaths = ( currentIdx: number, graph: QueryMultiGraph, entities: SerializedNode<EntityNodeAttributes>[], - relations: SerializedNode<RelationNodeAttributes>[] + relations: SerializedNode<RelationNodeAttributes>[], + settings: QueryBuilderSettings ): number => { if (!node?.attributes) throw Error('Malformed Graph! Node has no attributes'); // console.log(paths); @@ -38,24 +39,18 @@ const traverseEntityRelationPaths = ( const lastNode = paths[currentIdx][paths[currentIdx].length - 1]; if (lastNode.type === node.attributes.type) { if (lastNode.type === QueryElementTypes.Entity) { - paths[currentIdx].push({ type: QueryElementTypes.Relation, x: node.attributes.x, y: node.attributes.x }); + paths[currentIdx].push({ + type: QueryElementTypes.Relation, + x: node.attributes.x, + y: node.attributes.x, + depth: { min: settings.depth.min, max: settings.depth.max }, + }); } else { paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.x }); } } } paths[currentIdx].push(node.attributes); - // if ( - // (node.attributes.type === QueryElementTypes.Entity || node.attributes.type === QueryElementTypes.Relation) && - // paths[currentIdx].some((n) => n.type === QueryElementTypes.Logic) - // ) { - // return 0; - // } - - // const rightHandle = - // node.type === QueryElementTypes.Entity - // ? (node as EntityNodeAttributes)?.rightRelationHandleId - // : (node as RelationNodeAttributes)?.rightEntityHandleId; const edges = graph.edges.filter( (n) => @@ -94,7 +89,7 @@ const traverseEntityRelationPaths = ( if (i > 0) { paths.push([...pathBeforeTraversing]); // clone previous path in case of branching } - chunkOffset += traverseEntityRelationPaths(rightNode, paths, currentIdx + i + chunkOffset, graph, entities, relations); + chunkOffset += traverseEntityRelationPaths(rightNode, paths, currentIdx + i + chunkOffset, graph, entities, relations, settings); }); return chunkOffset + nodesToRight.length - 1; // offset @@ -160,14 +155,14 @@ function queryLogicUnion(graphLogicChunks: AllLogicStatement[]): AllLogicStateme export function Query2BackendQuery( databaseName: string, graph: QueryMultiGraph, - options: QueryBuilderSettings, + settings: QueryBuilderSettings, ml: ML = mlDefaultState ): BackendQueryFormat { let query: BackendQueryFormat = { databaseName: databaseName, query: [], machineLearning: [], - limit: options.limit, + limit: settings.limit, return: ['*'], // TODO }; @@ -201,7 +196,7 @@ export function Query2BackendQuery( }); }); - return Query2BackendQuery(databaseName, graphologyQuery.export(), options, ml); + return Query2BackendQuery(databaseName, graphologyQuery.export(), settings, ml); } // Chunk extraction: traverse graph to find all paths of logic between relations and entities let graphSequenceChunks: QueryGraphNodes[][] = []; @@ -214,12 +209,12 @@ export function Query2BackendQuery( // let entitiesEmptyRightHandle = entities.filter((n) => !n?.attributes?.rightRelationHandleId); entitiesEmptyLeftHandle.map((entity, i) => { // start with all entities that have no left handle, which means it "starts" a logic - chunkOffset += traverseEntityRelationPaths(entity, graphSequenceChunks, i + chunkOffset, graph, entities, relations); + chunkOffset += traverseEntityRelationPaths(entity, graphSequenceChunks, i + chunkOffset, graph, entities, relations, settings); }); if (entitiesEmptyLeftHandle.length > 0) chunkOffset += entitiesEmptyLeftHandle.length; relationsEmptyLeftHandle.map((relation, i) => { // then, for all relations that have no left handle, since they weren't accounted by the loop above - chunkOffset += traverseEntityRelationPaths(relation, graphSequenceChunks, i + chunkOffset, graph, entities, relations); + chunkOffset += traverseEntityRelationPaths(relation, graphSequenceChunks, i + chunkOffset, graph, entities, relations, settings); }); graphSequenceChunks.forEach((chunkSequence, i) => { chunkSequence.forEach((chunk, j) => { @@ -255,10 +250,11 @@ export function Query2BackendQuery( const currNode = chunk[position]; if (currNode.type === QueryElementTypes.Relation) { + const _currNode = currNode as RelationNodeAttributes; const ret: RelationStruct = { - ID: currNode?.id, - label: currNode?.name || undefined, - // depth: QuerySearchDepthStruct; + ID: _currNode.id, + label: _currNode.name || undefined, + depth: _currNode.depth, direction: 'TO', node: chunk.length === position + 1 ? undefined : (processConnection(chunk, position + 1) as NodeStruct), };