diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx index 1c761a6a293617198708186c3b2b5b7b5b294722..fb0fcb6a919311c900b1ba40e13b5cac290330d6 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx @@ -172,7 +172,7 @@ export const SettingsForm = (props: { onClose(): void; open: 'add' | 'update'; s <Button variantType="primary" className="flex-grow" - label={connection.updating ? formTitle.slice(0, -1) + 'ing...' : formTitle} + label={connection.updating ? (formTitle === 'Add' ? formTitle + 'ing...' : formTitle.slice(0, -1) + 'ing...') : formTitle} onClick={(event) => { event.preventDefault(); handleSubmit(); diff --git a/libs/shared/lib/components/pills/Pill.tsx b/libs/shared/lib/components/pills/Pill.tsx index b107d1d88062ddbacf58b7ca2ac36cdc839a38c2..a479ef713d6e2e8282d2671b432397e356139429 100644 --- a/libs/shared/lib/components/pills/Pill.tsx +++ b/libs/shared/lib/components/pills/Pill.tsx @@ -101,7 +101,7 @@ export const Pill = React.memo((props: PillI) => { ></div> )} <div - className={'font-semibold bg-neutral-100 ' + (corner !== 'square' ? 'rounded-b-[3px]' : '')} + className={'font-semibold ' + (corner !== 'square' ? 'rounded-b-[3px]' : '')} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} > diff --git a/libs/shared/lib/querybuilder/model/graphology/metaAttributes.ts b/libs/shared/lib/querybuilder/model/graphology/metaAttributes.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c34d0884ba11b1602fcb13596f35e0b1cdcee00 --- /dev/null +++ b/libs/shared/lib/querybuilder/model/graphology/metaAttributes.ts @@ -0,0 +1,28 @@ +import { SchemaAttribute } from '../../..'; +import { Handles, QueryElementTypes } from '../reactflow'; +import { QueryGraphEdgeAttribute, QueryGraphEdgeHandle, QueryGraphNodes } from './model'; + +const metaAttribute: Record<string, QueryGraphEdgeAttribute> = { + '(# Connection)': { + attributeName: '(# Connection)', + attributeType: 'float', + attributeDimension: 'numerical', + }, +}; + +export function checkForMetaAttributes(graphologyAttributes: QueryGraphNodes): QueryGraphEdgeHandle[] { + const ret: QueryGraphEdgeHandle[] = []; + const defaultHandleData = { + nodeId: graphologyAttributes.id, + nodeName: graphologyAttributes.name || '', + nodeType: graphologyAttributes.type, + handleType: graphologyAttributes.type === QueryElementTypes.Entity ? Handles.EntityAttribute : Handles.RelationAttribute, + }; + + // Only include if not already there + const metaAttributesToInclude = Object.keys(metaAttribute).filter((attributeName) => !(attributeName in graphologyAttributes.attributes)); + return metaAttributesToInclude.map((attributeName) => ({ + ...defaultHandleData, + ...metaAttribute[attributeName], + })) as QueryGraphEdgeHandle[]; +} diff --git a/libs/shared/lib/querybuilder/model/graphology/model.ts b/libs/shared/lib/querybuilder/model/graphology/model.ts index dc4a2bc1a42ebfa4cc9a2f823458dd3fa4a1cd42..b47deeb6e08999538cef80e4c2d163a78125eda8 100644 --- a/libs/shared/lib/querybuilder/model/graphology/model.ts +++ b/libs/shared/lib/querybuilder/model/graphology/model.ts @@ -23,7 +23,7 @@ export type NodeDefaults = { type: QueryElementTypes; width?: number; height?: number; - attributes?: NodeAttribute[]; + attributes: NodeAttribute[]; selected?: boolean; }; @@ -33,6 +33,7 @@ export interface EntityData { leftRelationHandleId?: QueryGraphEdgeHandle; rightRelationHandleId?: QueryGraphEdgeHandle; selected?: boolean; + type: QueryElementTypes.Entity; } /** Interface for the data in an relation node. */ @@ -44,6 +45,7 @@ export interface RelationData { rightEntityHandleId?: QueryGraphEdgeHandle; direction?: 'left' | 'right' | 'both'; selected?: boolean; + type: QueryElementTypes.Relation; } export interface LogicData { @@ -53,18 +55,19 @@ export interface LogicData { // key: string; logic: GeneralDescription<AllLogicTypes>; inputs: Record<string, InputNodeTypeTypes>; // name from InputNode -> InputNodeTypeTypes + type: QueryElementTypes.Logic; } -export type EntityNodeAttributes = XYPosition & EntityData & NodeDefaults; -export type RelationNodeAttributes = XYPosition & RelationData & NodeDefaults; -export type LogicNodeAttributes = XYPosition & LogicData & NodeDefaults; +export type EntityNodeAttributes = XYPosition & NodeDefaults & EntityData; +export type RelationNodeAttributes = XYPosition & NodeDefaults & RelationData; +export type LogicNodeAttributes = XYPosition & NodeDefaults & LogicData; export type QueryGraphNodes = EntityNodeAttributes | RelationNodeAttributes | LogicNodeAttributes; export type QueryGraphEdgeAttribute = { - attributeName?: string; - attributeType?: InputNodeType; - attributeDimension?: InputNodeDimension; + attributeName: string; + attributeType: InputNodeType; + attributeDimension: InputNodeDimension; }; export type QueryGraphEdgeHandle = { @@ -72,7 +75,7 @@ export type QueryGraphEdgeHandle = { nodeName: string; nodeType: QueryElementTypes; handleType: Handles; -} & QueryGraphEdgeAttribute; +} & Partial<QueryGraphEdgeAttribute>; export type QueryGraphEdges = { type: string; @@ -80,10 +83,6 @@ export type QueryGraphEdges = { targetHandleData: QueryGraphEdgeHandle; }; -export type QueryGraphEdgesOpt = { - type?: string; - sourceHandleData?: QueryGraphEdgeHandle; - targetHandleData?: QueryGraphEdgeHandle; -}; +export type QueryGraphEdgesOpt = Partial<QueryGraphEdges>; // export class QueryGraph extends Graph<QueryGraphNodes, GAttributes, GAttributes>; // is in utils.ts diff --git a/libs/shared/lib/querybuilder/model/graphology/utils.ts b/libs/shared/lib/querybuilder/model/graphology/utils.ts index 041fe0cde310d5a3a9ab8b2b9056c66d86f14015..f08865788e95edfd3ffd42c6ae8a4002f2e4dae2 100644 --- a/libs/shared/lib/querybuilder/model/graphology/utils.ts +++ b/libs/shared/lib/querybuilder/model/graphology/utils.ts @@ -4,7 +4,6 @@ import { Attributes as GAttributes, Attributes, SerializedGraph } from 'grapholo import { EntityNodeAttributes, LogicNodeAttributes, - QueryGraphEdgeAttribute, QueryGraphEdgeHandle, QueryGraphEdges, QueryGraphEdgesOpt, @@ -15,6 +14,7 @@ import { XYPosition } from 'reactflow'; import { Handles, QueryElementTypes } from '../reactflow'; import { SchemaAttribute, SchemaAttributeTypes } from '@graphpolaris/shared/lib/schema'; import { InputNodeType, InputNodeTypeTypes } from '../logic/general'; +import { checkForMetaAttributes } from './metaAttributes'; /** monospace fontsize table */ const widthPerFontsize = { @@ -54,6 +54,9 @@ export class QueryMultiGraphology extends Graph<QueryGraphNodes, QueryGraphEdges if (!attributes.id) attributes.id = 'id_' + (Date.now() + Math.floor(Math.random() * 1000)).toString(); + // Add to the beginning the meta attributes, such as (# Connection) + attributes.attributes = [...checkForMetaAttributes(attributes).map((a) => ({ handleData: a })), ...attributes.attributes]; + return attributes; } @@ -113,19 +116,17 @@ export class QueryMultiGraphology extends Graph<QueryGraphNodes, QueryGraphEdges return attributes; } - public addLogicPill2Graphology(attributes: QueryGraphNodes, inputValues: Record<string, InputNodeTypeTypes> = {}): QueryGraphNodes { - attributes = this.configureDefaults(attributes); - if (!attributes.type) attributes.type = QueryElementTypes.Logic; + public addLogicPill2Graphology(attributes: LogicNodeAttributes, inputValues: Record<string, InputNodeTypeTypes> = {}): QueryGraphNodes { + attributes = this.configureDefaults(attributes) as LogicNodeAttributes; if (!attributes.name || !attributes.id) throw Error('type or name is not defined'); // add default inputs, but only if not there yet if (attributes.type === QueryElementTypes.Logic) { - if ((attributes as LogicNodeAttributes).inputs === undefined) { - attributes = attributes as LogicNodeAttributes; - (attributes as LogicNodeAttributes).logic.inputs.forEach((input, i) => { + if (attributes.inputs === undefined || Object.keys(attributes.inputs).length === 0) { + attributes.logic.inputs.forEach((input, i) => { // Setup default non-linked inputs as regular values matching the input expected type - if (!(attributes as LogicNodeAttributes).inputs) (attributes as LogicNodeAttributes).inputs = {}; - (attributes as LogicNodeAttributes).inputs[input.name] = inputValues?.[input.name] || input.default; + if (!attributes.inputs) attributes.inputs = {}; + attributes.inputs[input.name] = inputValues?.[input.name] || input.default; }); // (attributes as LogicNodeAttributes).leftEntityHandleId = getHandleId(attributes.id, name, type, Handles.RelationLeft, ''); // (attributes as LogicNodeAttributes).rightEntityHandleId = getHandleId(attributes.id, name, type, Handles.RelationRight, ''); diff --git a/libs/shared/lib/querybuilder/model/reactflow/utils.ts b/libs/shared/lib/querybuilder/model/reactflow/utils.ts index b8a0dd7913527737b527ede89bd5d96d80c884cd..7ee9c55c078ee19fdc957b43d1efa4c220f1c756 100644 --- a/libs/shared/lib/querybuilder/model/reactflow/utils.ts +++ b/libs/shared/lib/querybuilder/model/reactflow/utils.ts @@ -4,7 +4,7 @@ import { toHandleId } from '..'; // Takes the querybuilder graph as an input and creates react flow elements for them. export function createReactFlowElements<T extends Graph>( - graph: T + graph: T, ): { nodes: Array<Node>; edges: Array<Edge>; @@ -13,8 +13,6 @@ export function createReactFlowElements<T extends Graph>( const edges: Array<Edge> = []; graph.forEachNode((node, attributes): void => { - // console.log('attributes', attributes); - let position = { x: attributes?.x || 0, y: attributes?.y || 0 }; const RFNode: Node<typeof attributes> = { id: node, @@ -28,8 +26,6 @@ export function createReactFlowElements<T extends Graph>( // Add the reactflow edges graph.forEachEdge((edge, attributes, source, target): void => { // connection from attributes don't have visible connection lines - // if (attributes.type == 'attribute_connection') return; - const RFEdge: Edge<typeof attributes> = { id: edge, source: source, @@ -43,7 +39,5 @@ export function createReactFlowElements<T extends Graph>( edges.push(RFEdge); }); - // console.log('nodes', nodes, 'edges', edges); - return { nodes, edges }; } diff --git a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx index 26da1edfb41b03e3e960c6d7ee7e1acf8b5028fc..a54e5e213aba9371059578c3a8d60269755d8d8f 100644 --- a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx @@ -195,6 +195,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { y: position.y - mouse_y, name: dragData.name, schemaKey: dragData.name, + attributes: [], }, schema.getNodeAttribute(dragData.name, 'attributes'), ); @@ -212,6 +213,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { name: dragData.collection, schemaKey: dragData.label, collection: dragData.collection, + attributes: [], }, schema.getEdgeAttribute(dragData.label, 'attributes'), ); @@ -238,6 +240,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { y: position.y, name: fromNodeID, schemaKey: fromNodeID, + attributes: [], }, schema.getNodeAttribute(fromNodeID, 'attributes'), ); @@ -249,6 +252,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { y: position.y, name: toNodeID, schemaKey: toNodeID, + attributes: [], }, schema.getNodeAttribute(toNodeID, 'attributes'), ); @@ -280,6 +284,8 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { x: position.x, y: position.y, logic: logic, + attributes: [], + inputs: {}, }); dispatch(setQuerybuilderGraphology(graphologyGraph)); diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx index 041acb8dfad4987c3d2863f57333983d0243ecde..0b9f7cff881741335e9413acaf28882b0b38eba6 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx @@ -86,6 +86,8 @@ export const QueryBuilderLogicPillsPanel = (props: { x: bounds.width / 2, y: bounds.height / 2, logic: logic, + attributes: [], + inputs: {}, }); } else { const params = props.connection.params; @@ -97,6 +99,8 @@ export const QueryBuilderLogicPillsPanel = (props: { x: position.x, y: position.y, logic: logic, + attributes: [], + inputs: {}, }); if (!logicNode?.id) throw new Error('Logic node has no id'); diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderRelatedNodesPanel.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderRelatedNodesPanel.tsx index 3b57002c3b1a5fc19656a596182ecd959f5e6425..5b398a8d50a65ec8cc45697e1bd2dd78950dbdac 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderRelatedNodesPanel.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderRelatedNodesPanel.tsx @@ -91,6 +91,7 @@ export const QueryBuilderRelatedNodesPanel = (props: { y: position.y, name: entity.name, schemaKey: entity.name, + attributes: [], }, schemaGraph.getNodeAttribute(entity.name, 'attributes'), ); @@ -124,6 +125,7 @@ export const QueryBuilderRelatedNodesPanel = (props: { depth: { min: queryBuilderSettings.depth.min, max: queryBuilderSettings.depth.max }, name: relation.collection, collection: relation.collection, + attributes: [], }, schemaGraph.getEdgeAttribute(relation.label, 'attributes'), ); diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx index 3bb9e75d3b2a75f6547594fc47946df45407dfb7..1fd01d879868d91cffd1051af362c58e97eea402 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx @@ -62,10 +62,6 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { // dispatch(setQuerybuilderGraphology(graphologyGraph)); // }; - const calcWidth = (data: number) => { - return data.toString().length + 0.5 + 'ch'; - }; - return ( <div className="w-fit h-fit p-3 bg-transparent nowheel"> <RelationPill @@ -84,55 +80,6 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { }} className={openDropdown ? 'border-secondary-200' : ''} /> - {/* <span className="pr-1"> - <span> [</span> - <input - className={ - 'bg-inherit text-center appearance-none mx-0.1 rounded-sm ' + - (depth.min < 0 || depth.min > depth.max ? ' bg-danger-400 ' : '') - } - style={{ maxWidth: calcWidth(depth.min) }} - type="number" - min={0} - placeholder={'?'} - value={depth.min} - onChange={(e) => { - setDepth({ ...depth, min: parseInt(e.target.value) }); - }} - onBlur={(e) => { - onNodeUpdated(); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onNodeUpdated(); - } - }} - ></input> - <span>..</span> - <input - className={ - 'bg-inherit text-center appearance-none mx-0.1 rounded-sm ' + - (depth.max > 99 || depth.min > depth.max ? ' bg-danger-400 ' : '') - } - style={{ maxWidth: calcWidth(depth.max) }} - type="number" - min={1} - placeholder={'?'} - value={depth.max} - onChange={(e) => { - setDepth({ ...depth, max: parseInt(e.target.value) }); - }} - onBlur={(e) => { - onNodeUpdated(); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onNodeUpdated(); - } - }} - ></input> - <span>]</span> - </span> */} </div> } withHandles="horizontal" @@ -155,15 +102,13 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { ></Handle> } > - {data?.attributes && ( - <PillDropdown - node={node} - attributes={data.attributes} - attributeEdges={attributeEdges.map((edge) => edge?.attributes)} - open={openDropdown} - mr={-pillWidth * 0.05} - /> - )} + <PillDropdown + node={node} + attributes={data?.attributes || []} + attributeEdges={attributeEdges.map((edge) => edge?.attributes)} + open={openDropdown} + mr={-pillWidth * 0.05} + /> </RelationPill> </div> ); diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx index 07caaabbf76bed3384b3992afe6a4a15f0102242..0868585845f4df0ac04de36f8d7fe5887fab34c5 100644 --- a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx +++ b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx @@ -1,17 +1,21 @@ import { useMemo, ReactElement, useState, useContext } from 'react'; -import { NodeAttribute, QueryGraphEdges, SchemaReactflowEntityNode, handleDataFromReactflowToDataId, toHandleId } from '../../model'; -import { Handle, Position, useUpdateNodeInternals } from 'reactflow'; +import { + Handles, + NodeAttribute, + QueryElementTypes, + QueryGraphEdges, + SchemaReactflowEntityNode, + SchemaReactflowRelationNode, +} from '../../model'; import { Abc, CalendarToday, Map, Numbers, Place, QuestionMarkOutlined } from '@mui/icons-material'; -import { Icon } from '@graphpolaris/shared/lib/components/icon'; -import { PillHandle } from '@graphpolaris/shared/lib/components/pills/PillHandle'; -import { pillDropdownPadding } from '@graphpolaris/shared/lib/components/pills/pill.const'; import { Button, TextInput, useAppDispatch, useQuerybuilderAttributesShown } from '../../..'; import { attributeShownToggle } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { isEqual } from 'lodash-es'; import { QueryBuilderDispatcherContext } from '../../panel/QueryBuilderDispatcher'; +import { PillDropdownItem } from './PillDropdownItem'; type PillDropdownProps = { - node: SchemaReactflowEntityNode; + node: SchemaReactflowEntityNode | SchemaReactflowRelationNode; attributes: NodeAttribute[]; attributeEdges: (QueryGraphEdges | undefined)[]; open: boolean; @@ -36,7 +40,6 @@ export const PillDropdown = (props: PillDropdownProps) => { const [filter, setFilter] = useState<string>(''); const dispatch = useAppDispatch(); const attributesBeingShown = useQuerybuilderAttributesShown(); - const { openLogicPillCreate } = useContext(QueryBuilderDispatcherContext); const attributesOfInterest = useMemo(() => { return props.attributes.map((attribute) => @@ -48,58 +51,22 @@ export const PillDropdown = (props: PillDropdownProps) => { <div className={'border-[1px] border-secondary-200 divide-y divide-secondary-200 !z-50'}> {attributesOfInterest && attributesOfInterest.map((showing, i) => { - if (showing === false) return null; - - const attribute = props.attributes[i]; - if (attribute.handleData.attributeName === undefined) { - throw new Error('attribute.handleData.attributeName is undefined'); - } - - const handleId = toHandleId(handleDataFromReactflowToDataId(props.node, attribute)); - const handleType = 'source'; - + if (!showing) return null; return ( - <div - className="px-2 py-1 bg-secondary-100 flex justify-between items-center" - key={(attribute.handleData.attributeName || '') + i} - > - <p className="truncate text-[0.6rem]">{attribute.handleData.attributeName}</p> - <Button - variantType="secondary" - variant="ghost" - size="2xs" - iconComponent={ - attribute.handleData?.attributeDimension ? IconMap[attribute.handleData.attributeDimension] : <QuestionMarkOutlined /> - } - onClick={() => { - openLogicPillCreate( - { - nodeId: props.node.id, - handleId: handleId, - handleType: handleType, - }, - { - x: props.node.xPos + 200, - y: props.node.yPos + 50, - }, - ); - }} - /> - <PillHandle - mr={-pillDropdownPadding + (props.mr || 0)} - handleTop="auto" - position={Position.Right} - className={`stroke-white${props.className ? ` ${props.className}` : ''}`} - type="square" - > - <Handle - id={handleId} - type={handleType} - position={Position.Right} - className={'!rounded-none !bg-transparent !w-full !h-full !right-0 !left-0 !border-0'} - ></Handle> - </PillHandle> - </div> + <PillDropdownItem + key={props.attributes[i].handleData.attributeName || i} + node={props.node} + attribute={props.attributes[i]} + mr={props.mr} + className={props.className} + icon={ + props.attributes[i].handleData?.attributeDimension ? ( + IconMap[props.attributes[i].handleData.attributeDimension || 0] + ) : ( + <QuestionMarkOutlined /> + ) + } + /> ); })} {(props.open || forceOpen) && ( diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdownItem.tsx b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdownItem.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6304229d14cbdff0438f6d2b60c623002456171a --- /dev/null +++ b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdownItem.tsx @@ -0,0 +1,71 @@ +import { ReactElement, useContext } from 'react'; +import { + NodeAttribute, + SchemaReactflowEntityNode, + SchemaReactflowRelationNode, + handleDataFromReactflowToDataId, + toHandleId, +} from '../../model'; +import { Handle, Position } from 'reactflow'; +import { PillHandle } from '@graphpolaris/shared/lib/components/pills/PillHandle'; +import { pillDropdownPadding } from '@graphpolaris/shared/lib/components/pills/pill.const'; +import { Button } from '../../..'; +import { QueryBuilderDispatcherContext } from '../../panel/QueryBuilderDispatcher'; + +type PillDropdownItemProps = { + attribute: NodeAttribute; + node: SchemaReactflowEntityNode | SchemaReactflowRelationNode; + className?: string; + mr?: number; + icon: ReactElement; +}; + +export const PillDropdownItem = (props: PillDropdownItemProps) => { + const { openLogicPillCreate } = useContext(QueryBuilderDispatcherContext); + + if (props.attribute.handleData.attributeName === undefined) { + throw new Error('attribute.handleData.attributeName is undefined'); + } + + const handleId = toHandleId(handleDataFromReactflowToDataId(props.node, props.attribute)); + const handleType = 'source'; + + return ( + <div className="px-2 py-1 bg-secondary-100 flex justify-between items-center"> + <p className="truncate text-[0.6rem]">{props.attribute.handleData.attributeName}</p> + <Button + variantType="secondary" + variant="ghost" + size="2xs" + iconComponent={props.icon} + onClick={() => { + openLogicPillCreate( + { + nodeId: props.node.id, + handleId: handleId, + handleType: handleType, + }, + { + x: props.node.xPos + 200, + y: props.node.yPos + 50, + }, + ); + }} + /> + <PillHandle + mr={-pillDropdownPadding + (props.mr || 0)} + handleTop="auto" + position={Position.Right} + className={`stroke-white${props.className ? ` ${props.className}` : ''}`} + type="square" + > + <Handle + id={handleId} + type={handleType} + position={Position.Right} + className={'!rounded-none !bg-transparent !w-full !h-full !right-0 !left-0 !border-0'} + ></Handle> + </PillHandle> + </div> + ); +}; diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts index a2ed912b992d56b664adc30a5bf22b9dda77477a..8b35dba9498b26054d57f7706d4792ca838a1693 100644 --- a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts @@ -14,6 +14,7 @@ const defaultQuery = { query: [], limit: 500, return: ['*'], + machineLearning: [], }; const defaultSettings: QueryBuilderSettings = { @@ -38,6 +39,7 @@ describe('QueryUtils Entity and Relations', () => { x: 100, y: 100, name: 'Airport 1', + attributes: [], }); const entity2 = graph.addPill2Graphology({ id: '10', @@ -45,6 +47,7 @@ describe('QueryUtils Entity and Relations', () => { x: 200, y: 200, name: 'Airport 2', + attributes: [], }); const relation1 = graph.addPill2Graphology({ @@ -55,6 +58,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(entity1, relation1); @@ -90,8 +94,8 @@ describe('QueryUtils Entity and Relations', () => { it('should run multiple path query translation', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); - const e2 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2' }); + const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); + const e2 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2', attributes: [] }); const r1 = graph.addPill2Graphology({ id: 'r1', @@ -101,13 +105,14 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(e1, r1, { type: 'connection' }); graph.addEdge2Graphology(r1, e2, { type: 'connection' }); - const e12 = graph.addPill2Graphology({ id: 'e12', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 12' }); - const e22 = graph.addPill2Graphology({ id: 'e22', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 22' }); + const e12 = graph.addPill2Graphology({ id: 'e12', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 12', attributes: [] }); + const e22 = graph.addPill2Graphology({ id: 'e22', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 22', attributes: [] }); const r12 = graph.addPill2Graphology({ id: 'r12', @@ -117,6 +122,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports 2', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(e12, r12); @@ -169,10 +175,10 @@ describe('QueryUtils Entity and Relations', () => { it('should run one relation multiple entity query translation', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); - const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2' }); - const e12 = graph.addPill2Graphology({ id: 'e12', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 12' }); - const e22 = graph.addPill2Graphology({ id: 'e22', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 22' }); + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2', attributes: [] }); + const e12 = graph.addPill2Graphology({ id: 'e12', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 12', attributes: [] }); + const e22 = graph.addPill2Graphology({ id: 'e22', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 22', attributes: [] }); const r1 = graph.addPill2Graphology({ id: 'r1', @@ -182,6 +188,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(e1, r1); @@ -235,10 +242,10 @@ describe('QueryUtils Entity and Relations', () => { it('should run lone entities query translation', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); - const e2 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2' }); - const e12 = graph.addPill2Graphology({ id: 'e12', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 12' }); - const e22 = graph.addPill2Graphology({ id: 'e22', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 22' }); + const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); + const e2 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2', attributes: [] }); + const e12 = graph.addPill2Graphology({ id: 'e12', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 12', attributes: [] }); + const e22 = graph.addPill2Graphology({ id: 'e22', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 22', attributes: [] }); const r1 = graph.addPill2Graphology({ id: 'r1', @@ -248,6 +255,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(e1, r1); @@ -276,8 +284,8 @@ describe('QueryUtils Entity and Relations', () => { it('should run lone relations query translation', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); - const e2 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2' }); + const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); + const e2 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 200, y: 200, name: 'Airport 2', attributes: [] }); const r1 = graph.addPill2Graphology({ id: 'r1', @@ -287,6 +295,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); const r2 = graph.addPill2Graphology({ id: 'r2', @@ -296,6 +305,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports 2', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(e1, r1); @@ -326,7 +336,7 @@ describe('QueryUtils Entity and Relations', () => { it('should run relation only left side connected query translation', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e1 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); const r1 = graph.addPill2Graphology({ id: 'r1', @@ -336,6 +346,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(e1, r1); @@ -361,7 +372,7 @@ describe('QueryUtils Entity and Relations', () => { it('should run relation only right side connected query translation', () => { const graph = new QueryMultiGraphology(); - const e2 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 2' }); + const e2 = graph.addPill2Graphology({ id: 'e0', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 2', attributes: [] }); const r1 = graph.addPill2Graphology({ id: 'r1', @@ -371,6 +382,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(r1, e2); @@ -394,8 +406,8 @@ describe('QueryUtils Entity and Relations', () => { it('should run entity and relations multi connection', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); - const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 2' }); + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 2', attributes: [] }); const r1 = graph.addPill2Graphology({ id: 'r1', @@ -405,6 +417,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); const r2 = graph.addPill2Graphology({ @@ -415,6 +428,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports 2', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(e1, r1); @@ -451,7 +465,7 @@ describe('QueryUtils Entity and Relations', () => { it('should run in case of loops', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); const r1 = graph.addPill2Graphology({ id: 'r1', @@ -461,6 +475,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(e1, r1, { type: 'connection' }); @@ -487,8 +502,8 @@ describe('QueryUtils Entity and Relations', () => { it('should run in case of loops and regular', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); - const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); const r1 = graph.addPill2Graphology({ id: 'r1', @@ -498,6 +513,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(e1, r1, { type: 'connection' }); @@ -538,8 +554,8 @@ describe('QueryUtils Entity and Relations', () => { it('should run in case of loops and regular left', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); - const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); const r1 = graph.addPill2Graphology({ id: 'r1', @@ -549,6 +565,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(e1, r1, { type: 'connection' }); @@ -584,8 +601,8 @@ describe('QueryUtils Entity and Relations', () => { it('should run in case of direct entity connection', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); - const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); graph.addEdge2Graphology(e1, e2, { type: 'connection' }); @@ -618,6 +635,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); const r2 = graph.addPill2Graphology({ @@ -628,6 +646,7 @@ describe('QueryUtils Entity and Relations', () => { name: 'Flight between airports 2', collection: 'Relation Pill', depth: { min: 0, max: 1 }, + attributes: [], }); graph.addEdge2Graphology(r1, r2, { type: 'connection' }); @@ -656,8 +675,8 @@ describe('QueryUtils Entity and Relations', () => { it('should run in case of entity only loop', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); - const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); + const e2 = graph.addPill2Graphology({ id: 'e2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); graph.addEdge2Graphology(e1, e2, { type: 'connection' }); graph.addEdge2Graphology(e2, e1, { type: 'connection' }); @@ -691,7 +710,7 @@ describe('QueryUtils Entity and Relations', () => { it('should run in case of entity only self loop', () => { const graph = new QueryMultiGraphology(); - const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1' }); + const e1 = graph.addPill2Graphology({ id: 'e1', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Airport 1', attributes: [] }); graph.addEdge2Graphology(e1, e1, { type: 'connection' }); @@ -721,6 +740,7 @@ describe('QueryUtils calculateQueryLogic', () => { x: 100, y: 100, name: 'Airport 1', + attributes: [], }, [{ name: 'age', type: 'string' }], ); @@ -732,6 +752,8 @@ describe('QueryUtils calculateQueryLogic', () => { y: 100, name: 'Logic 1', logic: MathFilters[NumberFilterTypes.EQUAL], + attributes: [], + inputs: {}, }); graph.addEdge2Graphology(e1, l1, { type: 'connection' }, { sourceHandleName: 'age', targetHandleName: '1' }); @@ -754,6 +776,7 @@ describe('QueryUtils with Logic', () => { x: 100, y: 100, name: 'Airport 1', + attributes: [], }, [{ name: 'age', type: 'string' }], ); @@ -765,6 +788,8 @@ describe('QueryUtils with Logic', () => { y: 100, name: 'Logic 1', logic: MathFilters[NumberFilterTypes.EQUAL], + attributes: [], + inputs: {}, }); graph.addEdge2Graphology(e1, l1, { type: 'connection' }, { sourceHandleName: 'age', targetHandleName: 'Value' }); @@ -800,6 +825,7 @@ describe('QueryUtils with Logic', () => { x: 100, y: 100, name: 'Airport 1', + attributes: [], }, [{ name: 'age', type: 'string' }], ); @@ -811,6 +837,7 @@ describe('QueryUtils with Logic', () => { x: 100, y: 100, name: 'Airport 2', + attributes: [], }, [{ name: 'age', type: 'string' }], ); @@ -822,6 +849,8 @@ describe('QueryUtils with Logic', () => { y: 100, name: 'Filter EQ', logic: MathFilters[NumberFilterTypes.EQUAL], + attributes: [], + inputs: {}, }); const l2 = graph.addLogicPill2Graphology({ @@ -831,6 +860,8 @@ describe('QueryUtils with Logic', () => { y: 100, name: 'Logic ADD', logic: NumberFunctions[NumberFunctionTypes.ADD], + attributes: [], + inputs: {}, }); graph.addEdge2Graphology(e1, l2, { type: 'connection' }, { sourceHandleName: 'age', targetHandleName: 'Value' }); @@ -876,6 +907,7 @@ describe('QueryUtils with Logic', () => { x: 100, y: 100, name: 'Airport 1', + attributes: [], }, [{ name: 'age', type: 'string' }], ); @@ -887,6 +919,8 @@ describe('QueryUtils with Logic', () => { y: 100, name: 'Logic LT', logic: MathFilters[NumberFilterTypes.LESS_THAN], + attributes: [], + inputs: {}, }); const l2 = graph.addLogicPill2Graphology({ @@ -896,6 +930,8 @@ describe('QueryUtils with Logic', () => { y: 100, name: 'Logic average', logic: MathAggregations[NumberAggregationTypes.AVG], + attributes: [], + inputs: {}, }); graph.addEdge2Graphology(e1, l2, { type: 'connection' }, { sourceHandleName: 'age', targetHandleName: 'Value' }); @@ -932,6 +968,7 @@ describe('QueryUtils with Logic', () => { x: 100, y: 100, name: 'Airport 1', + attributes: [], }, [{ name: 'age', type: 'string' }], ); @@ -944,6 +981,8 @@ describe('QueryUtils with Logic', () => { y: 100, name: 'Logic LT', logic: MathFilters[NumberFilterTypes.LESS_THAN], + attributes: [], + inputs: {}, }, { '1': 5 }, ); @@ -982,6 +1021,7 @@ it('should no connections between entities and relations', () => { x: 100, y: 100, name: 'Airport 1', + attributes: [], }, [{ name: 'age', type: 'string' }], ); @@ -993,6 +1033,7 @@ it('should no connections between entities and relations', () => { x: 100, y: 100, name: 'Airport 2', + attributes: [], }, [{ name: 'age', type: 'string' }], ); @@ -1004,6 +1045,8 @@ it('should no connections between entities and relations', () => { y: 100, name: 'Relation 1', depth: { min: 0, max: 1 }, + attributes: [], + collection: 'r', }, [{ name: 'age', type: 'string' }], ); diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.ts index 92fd9e6f2c4a21c6150f05f6f1969adbcb88db89..f04b9f1cf72eddc9e47b1308d97d007400dcf044 100644 --- a/libs/shared/lib/querybuilder/query-utils/query2backend.ts +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.ts @@ -31,7 +31,7 @@ const traverseEntityRelationPaths = ( // console.log('new path'); paths.push([]); if (node.attributes.type === QueryElementTypes.Relation) { - paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.y }); + paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.y, attributes: [] }); } } else if (paths[currentIdx].length > 0) { const lastNode = paths[currentIdx][paths[currentIdx].length - 1]; @@ -39,13 +39,15 @@ const traverseEntityRelationPaths = ( if (lastNode.type === QueryElementTypes.Entity) { paths[currentIdx].push({ type: QueryElementTypes.Relation, + collection: node.key, x: node.attributes.x, y: node.attributes.x, depth: { min: settings.depth.min, max: settings.depth.max }, direction: 'both', + attributes: [], }); } else { - paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.x }); + paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.x, attributes: [] }); } } } @@ -59,7 +61,7 @@ const traverseEntityRelationPaths = ( let connections = edges.filter((e) => e.source === node.key); if (connections.length === 0) { if (node.attributes.type === QueryElementTypes.Relation) { - paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.x }); + paths[currentIdx].push({ type: QueryElementTypes.Entity, x: node.attributes.x, y: node.attributes.x, attributes: [] }); } return 0; } @@ -139,6 +141,9 @@ export function calculateQueryLogic( if (!connectionToInputRef.attributes?.sourceHandleData) throw Error('Malformed Graph! Logic node is connected but has no sourceHandleData'); // Is connected to entity or relation node + if (connectionToInputRef.attributes.sourceHandleData.attributeName === '(# Connection)') { + return ['Count', `@${connectionToInputRef.attributes.sourceHandleData.nodeId}`]; + } return `@${connectionToInputRef.attributes.sourceHandleData.nodeId}.${connectionToInputRef.attributes.sourceHandleData.attributeName}`; } } else { diff --git a/libs/shared/lib/vis/components/config/SelectionConfig.tsx b/libs/shared/lib/vis/components/config/SelectionConfig.tsx index 7d41cf4a15e61c3a5d3dfb8ab41cf2a7021445e4..1d5e96f046c6b5255e4b6ac3f8862924d610bdee 100644 --- a/libs/shared/lib/vis/components/config/SelectionConfig.tsx +++ b/libs/shared/lib/vis/components/config/SelectionConfig.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { SelectionStateI, unSelect } from '@graphpolaris/shared/lib/data-access/store/interactionSlice'; import { Delete } from '@mui/icons-material'; import { useDispatch } from 'react-redux'; @@ -25,8 +26,8 @@ export const SelectionConfig = () => { /> </div> {selection.content.map((item, index) => ( - <> - <div key={index + 'id'} className="flex justify-between items-center px-4 py-1 gap-1"> + <React.Fragment key={index + 'id'}> + <div className="flex justify-between items-center px-4 py-1 gap-1"> <span className="text-xs font-normal">ID</span> <span className="text-xs">{item._id}</span> </div> @@ -43,7 +44,7 @@ export const SelectionConfig = () => { </div> ); })} - </> + </React.Fragment> ))} </div> ); diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx index 78d36400c8efd19494d706d1e84a56ad86c1d62d..19c50b62913aa5d8551f7f57e2714792d2ca3946 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx @@ -6,18 +6,18 @@ export function processLinkPrediction(ml: ML, graph: GraphType): GraphType { if (ml === undefined || ml.linkPrediction === undefined) return graph; if (ml.linkPrediction.enabled) { - let allNodeIds = new Set(graph.nodes.map((n) => n._id)); + let allNodeIds = new Set(Object.keys(graph.nodes)); ml.linkPrediction.result.forEach((link) => { if (allNodeIds.has(link.from) && allNodeIds.has(link.to)) { const toAdd: LinkType = { - id: link.from + link.to, + id: link.from + ':LP:' + link.to, // TODO: this only supports one link between two nodes source: link.from, target: link.to, value: link.attributes.jaccard_coefficient as number, mlEdge: true, color: 0x000000, }; - graph.links.push(toAdd); + graph.links[toAdd.id] = toAdd; } }); } @@ -35,18 +35,16 @@ export function processCommunityDetection(ml: ML, graph: GraphType): GraphType { }); }); - graph.nodes = graph.nodes.map((node, i) => { - if (allNodeIdMap.has(node._id)) { - node.cluster = allNodeIdMap.get(node._id); + Object.keys(graph.nodes).forEach((nodeId) => { + if (allNodeIdMap.has(nodeId)) { + graph.nodes[nodeId].cluster = allNodeIdMap.get(nodeId); } else { - node.cluster = -1; + graph.nodes[nodeId].cluster = -1; } - return node; }); } else { - graph.nodes = graph.nodes.map((node, i) => { - node.cluster = undefined; - return node; + Object.keys(graph.nodes).forEach((nodeId) => { + graph.nodes[nodeId].cluster = undefined; }); } return graph; @@ -101,6 +99,7 @@ export const useNLMachineLearning = (props: { * Gets the edges corresponding to the shortestPath. * @param pathString The path as a string. * @returns The path as a LinkType[] + * @deprecated This function is not working anymore */ function getShortestPathEdges(pathString: string[]): LinkType[] { try { @@ -112,13 +111,14 @@ export const useNLMachineLearning = (props: { continue; } let edgeFound = false; - props.graph.links.forEach((link: any) => { - const { source, target } = link; + Object.keys(props.graph.links).forEach((key) => { + const link = props.graph.links[key]; if ( - (pathString[index] == source.id && pathString[index + 1] == target.id) || - (pathString[index] == source && pathString[index + 1] == target) || - (pathString[index + 1] == source.id && pathString[index] == target.id) || - (pathString[index + 1] == source && pathString[index] == target) + false // FIXME: This is not working anymore + // (pathString[index] == source.id && pathString[index + 1] == target.id) || + // (pathString[index] == source && pathString[index + 1] == target) || + // (pathString[index + 1] == source.id && pathString[index] == target.id) || + // (pathString[index + 1] == source && pathString[index] == target) ) { newPath.push(link); edgeFound = true; @@ -172,10 +172,10 @@ export const useNLMachineLearning = (props: { * after a community detection algorithm, where the cluster of these nodes could have been changed. */ const resetClusterOfNodes = (type: number): void => { - props.graph.nodes.forEach((node: NodeType) => { - const numberOfClusters = props.numberOfMlClusters; + Object.keys(props.graph.nodes).forEach((key) => { + const node = props.graph.nodes[key]; if (node.cluster == type) { - node.cluster = numberOfClusters; + node.cluster = props.numberOfMlClusters; } if (node.type == type) { node.cluster = node.type; diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index b9bfe5a06084e73f66b7a0241e5fe023a983ab7c..90919a26e89ab3f610a5a02111bf264aa2f853fb 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -1,4 +1,4 @@ -import { GraphType, LinkType, NodeType } from '../types'; +import { GraphType, GraphTypeD3, LinkType, LinkTypeD3, NodeType, NodeTypeD3 } from '../types'; import { dataColors, visualizationColors } from 'config'; import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { @@ -19,23 +19,22 @@ import { NLPopup } from './NLPopup'; import { hslStringToHex, nodeColor, nodeColorHex } from './utils'; import { CytoscapeLayout, GraphologyLayout, LayoutFactory, Layouts } from '../../../../graph-layout'; import { MultiGraph } from 'graphology'; -import { VisualizationSettingsType } from '../../../common'; import { Viewport } from 'pixi-viewport'; import { NodelinkVisProps } from '../nodelinkvis'; import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip'; import { MovedEvent } from 'pixi-viewport/dist/types'; - +import { ConstructionOutlined } from '@mui/icons-material'; import { CardToolTipVis, CardToolTipVisProps } from '@graphpolaris/shared/lib/components/CardToolTipVis'; type Props = { - onClick: (event?: { node: NodeType; pos: IPointData }) => void; + onClick: (event?: { node: NodeTypeD3; pos: IPointData }) => void; // onHover: (data: { node: NodeType; pos: IPointData }) => void; // onUnHover: (data: { node: NodeType; pos: IPointData }) => void; highlightNodes: NodeType[]; configuration: NodelinkVisProps; currentShortestPathEdges?: LinkType[]; highlightedLinks?: LinkType[]; - graph?: GraphType; + graph: GraphType; layoutAlgorithm: string; showPopupsOnHover: boolean; }; @@ -48,13 +47,13 @@ type LayoutState = 'reset' | 'running' | 'paused'; export const NLPixi = (props: Props) => { const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>(); - const [popups, setPopups] = useState<{ node: NodeType; pos: IPointData }[]>([]); + const [popups, setPopups] = useState<{ node: NodeTypeD3; pos: IPointData }[]>([]); const [assetsLoaded, setAssetsLoaded] = useState(false); const app = useMemo( () => new Application({ - background: 0xffffff, + backgroundAlpha: 0, antialias: true, autoDensity: true, eventMode: 'auto', @@ -74,6 +73,7 @@ export const NLPixi = (props: Props) => { const isSetup = useRef(false); const ml = useML(); const searchResults = useSearchResultData(); + const graph = useRef<GraphTypeD3>({ nodes: [], links: [] }); const layoutAlgorithm = useRef<CytoscapeLayout | GraphologyLayout>(new LayoutFactory().createLayout(Layouts.DAGRE)); @@ -128,10 +128,14 @@ export const NLPixi = (props: Props) => { useImperativeHandle(imperative, () => ({ onMouseDown(event: FederatedPointerEvent) { + if (props.configuration.showPopUpOnHover) return; + (event as any).mouseDownTimeStamp = event.timeStamp; }, onMouseUpNode(event: FederatedPointerEvent) { + if (props.configuration.showPopUpOnHover) return; + // If its a short click (not a drag) on the stage but not on a node: clear the selection and remove all popups. const holdDownTime = event.timeStamp - (event as any).mouseDownTimeStamp; if (holdDownTime > mouseClickThreshold) { @@ -139,7 +143,7 @@ export const NLPixi = (props: Props) => { } const sprite = event.target as Sprite; - const node = (sprite as any).node as NodeType; + const node = (sprite as any).node as NodeTypeD3; if (event.shiftKey) { setPopups([...popups, { node: node, pos: toGlobal(node) }]); @@ -150,6 +154,7 @@ export const NLPixi = (props: Props) => { sprite.texture = Assets.get(textureId(false)); } } + sprite.texture = Assets.get(textureId(true)); props.onClick({ node: node, pos: toGlobal(node) }); @@ -158,6 +163,8 @@ export const NLPixi = (props: Props) => { }, onMouseUpStage(event: FederatedPointerEvent) { + if (props.configuration.showPopUpOnHover) return; + // If its a short click (not a drag) on the stage but not on a node: clear the selection and remove all popups. const holdDownTime = event.timeStamp - (event as any).mouseDownTimeStamp; if (holdDownTime < mouseClickThreshold) { @@ -171,8 +178,10 @@ export const NLPixi = (props: Props) => { }, onHover(event: FederatedPointerEvent) { + if (!props.configuration.showPopUpOnHover) return; + const sprite = event.target as Sprite; - const node = (sprite as any).node as NodeType; + const node = (sprite as any).node as NodeTypeD3; if ( mouseInCanvas.current && viewport?.current && @@ -180,13 +189,17 @@ export const NLPixi = (props: Props) => { node && popups.filter((p) => p.node._id === node._id).length === 0 ) { - setQuickPopup({ node, pos: toGlobal(node) }); + setQuickPopup({ node: props.graph.nodes[node._id], pos: toGlobal(node) }); } }, onUnHover() { + if (!props.configuration.showPopUpOnHover) return; + setQuickPopup(undefined); }, onMoved(event: MovedEvent) { + if (props.configuration.showPopUpOnHover) return; + for (const popup of popups) { if (popup.node.x == null || popup.node.y == null) continue; popup.pos.x = event.viewport.transform.position.x + popup.node.x * event.viewport.scale.x; @@ -224,7 +237,7 @@ export const NLPixi = (props: Props) => { } }, [ref]); - function toGlobal(node: NodeType): IPointData { + function toGlobal(node: NodeTypeD3): IPointData { if (viewport?.current) { // const rect = ref.current?.getBoundingClientRect(); const rect = { x: 0, y: 0 }; @@ -234,18 +247,19 @@ export const NLPixi = (props: Props) => { } else return { x: 0, y: 0 }; } - const updateNode = (node: NodeType) => { + const updateNode = (node: NodeTypeD3) => { const gfx = nodeMap.current.get(node._id); if (!gfx) return; // Update texture when selected - const texture = Assets.get(textureId(node.selected)); + const nodeMeta = props.graph.nodes[node._id]; + const texture = Assets.get(textureId(nodeMeta.selected)); gfx.texture = texture; // Cluster colors - if (node?.cluster) { - gfx.tint = node.cluster >= 0 ? nodeColor(node.cluster) : 0x000000; + if (nodeMeta?.cluster) { + gfx.tint = nodeMeta.cluster >= 0 ? nodeColor(nodeMeta.cluster) : 0x000000; } else { - gfx.tint = nodeColor(node.type); + gfx.tint = nodeColor(nodeMeta.type); } gfx.position.set(node.x, node.y); @@ -266,7 +280,9 @@ export const NLPixi = (props: Props) => { // } }; - const createNode = (node: NodeType, selected?: boolean) => { + const createNode = (node: NodeTypeD3, selected?: boolean) => { + const nodeMeta = props.graph.nodes[node._id]; + // check if node is already drawn, and if so, delete it if (node && node?._id && nodeMap.current.has(node._id)) { nodeMap.current.delete(node._id); @@ -278,8 +294,8 @@ export const NLPixi = (props: Props) => { const texture = Assets.get(textureId()); sprite = new Sprite(texture); - sprite.tint = nodeColor(node.type); - const scale = (Math.max(node.radius || 5, 5) / 70) * 2; + sprite.tint = nodeColor(nodeMeta.type); + const scale = (Math.max(nodeMeta.radius || 5, 5) / 70) * 2; sprite.scale.set(scale, scale); sprite.anchor.set(0.5, 0.5); @@ -291,7 +307,6 @@ export const NLPixi = (props: Props) => { nodeMap.current.set(node._id, sprite); nodeLayer.addChild(sprite); - node.selected = selected; updateNode(node); (sprite as any).node = node; @@ -307,30 +322,30 @@ export const NLPixi = (props: Props) => { // }); // }; - const updateLink = (link: LinkType) => { + const updateLink = (link: LinkTypeD3) => { if (!props.graph || nodeMap.current.size === 0) return; + const linkMeta = props.graph.links[link._id]; const _source = link.source; const _target = link.target; if (!_source || !_target) { - console.log('source or target not found', _source, _target); return; } let sourceId = ''; let targetId = ''; - let source: NodeType | undefined; - let target: NodeType | undefined; + let source: NodeTypeD3 | undefined; + let target: NodeTypeD3 | undefined; if (typeof _source === 'string') { sourceId = link.source as string; targetId = link.target as string; - source = nodeMap.current.get(sourceId) as NodeType | undefined; - target = nodeMap.current.get(targetId) as NodeType | undefined; + source = nodeMap.current.get(sourceId) as NodeTypeD3 | undefined; + target = nodeMap.current.get(targetId) as NodeTypeD3 | undefined; } else { - source = link.source as NodeType; - target = link.target as NodeType; + source = link.source as NodeTypeD3; + target = link.target as NodeTypeD3; sourceId = source._id; targetId = target._id; } @@ -343,28 +358,28 @@ export const NLPixi = (props: Props) => { // let color = link.color || 0x000000; let color = config.LINE_COLOR_DEFAULT; let style = config.LINE_WIDTH_DEFAULT; - let alpha = link.alpha || 1; - if (link.mlEdge) { + let alpha = linkMeta.alpha || 1; + if (linkMeta.mlEdge) { color = config.LINE_COLOR_ML; - if (link.value > ml.communityDetection.jaccard_threshold) { - style = link.value * 1.8; + if (linkMeta.value > ml.communityDetection.jaccard_threshold) { + style = linkMeta.value * 1.8; } else { style = 0; alpha = 0.2; } - } else if (props.highlightedLinks && props.highlightedLinks.includes(link)) { - if (link.mlEdge && ml.communityDetection.jaccard_threshold) { - if (link.value > ml.communityDetection.jaccard_threshold) { + } else if (props.highlightedLinks && props.highlightedLinks.includes(linkMeta)) { + if (linkMeta.mlEdge && ml.communityDetection.jaccard_threshold) { + if (linkMeta.value > ml.communityDetection.jaccard_threshold) { color = dataColors.magenta[50]; // 0xaa00ff; - style = link.value * 1.8; + style = linkMeta.value * 1.8; } } else { color = dataColors.red[70]; // color = 0xff0000; style = 1.0; } - } else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(link)) { + } else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(linkMeta)) { color = dataColors.green[50]; // color = 0x00ff00; style = 3.0; @@ -373,7 +388,7 @@ export const NLPixi = (props: Props) => { // Conditional alpha for search results if (searchResults.nodes.length > 0 || searchResults.edges.length > 0) { // FIXME: searchResults.edges should be a hashmap to improve performance. - const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link.id); + const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link._id); alpha = isLinkInSearchResults ? 1 : 0.05; } @@ -413,11 +428,11 @@ export const NLPixi = (props: Props) => { if (isSetup.current === false) setup(); else update(false); } - }, [props.graph, config, assetsLoaded]); + }, [config, assetsLoaded]); useEffect(() => { if (props.graph) { - props.graph.nodes.forEach((node: NodeType) => { + graph.current.nodes.forEach((node) => { const gfx = nodeMap.current.get(node._id); if (!gfx) return; const isNodeInSearchResults = searchResults.nodes.some((resultNode) => resultNode.id === node._id); @@ -438,7 +453,7 @@ export const NLPixi = (props: Props) => { const widthHalf = app.renderer.width / 2; const heightHalf = app.renderer.height / 2; - props.graph.nodes.forEach((node: NodeType, i) => { + graph.current.nodes.forEach((node, i) => { if (!layoutAlgorithm.current) return; const gfx = nodeMap.current.get(node._id); if (!gfx || node.x === undefined || node.y === undefined) return; @@ -449,24 +464,19 @@ export const NLPixi = (props: Props) => { stopped += 1; return; } - try { - if (layoutAlgorithm.current.provider === 'Graphology') { - // this is a dirty hack to fix the graphology layout being out of bounds - node.x = position.x + widthHalf; - node.y = position.y + heightHalf; - } else { - node.x = position.x; - node.y = position.y; - } - } catch (e) { - // node.x and .y become read-only when some layout algorithms are finished - layoutState.current = 'paused'; + if (layoutAlgorithm.current.provider === 'Graphology') { + // this is a dirty hack to fix the graphology layout being out of bounds + node.x = position.x + widthHalf; + node.y = position.y + heightHalf; + } else { + node.x = position.x; + node.y = position.y; } gfx.position.copyFrom(node as IPointData); }); - if (stopped === props.graph.nodes.length) { + if (stopped === graph.current.nodes.length) { layoutStoppedCount.current = layoutStoppedCount.current + 1; if (layoutStoppedCount.current > 1000) { layoutState.current = 'paused'; @@ -482,7 +492,7 @@ export const NLPixi = (props: Props) => { // Draw the links linkGfx.clear(); linkGfx.beginFill(); - props.graph.links.forEach((link: any) => { + graph.current.links.forEach((link: any) => { updateLink(link); }); linkGfx.endFill(); @@ -500,7 +510,7 @@ export const NLPixi = (props: Props) => { } nodeMap.current.forEach((gfx, id) => { - if (!props.graph?.nodes?.find((node) => node._id === id)) { + if (!graph.current.nodes.find((node) => node._id === id)) { nodeLayer.removeChild(gfx); gfx.destroy(); nodeMap.current.delete(id); @@ -509,17 +519,13 @@ export const NLPixi = (props: Props) => { linkGfx.clear(); - props.graph.nodes.forEach((node: NodeType) => { + graph.current.nodes.forEach((node) => { if (!forceClear && nodeMap.current.has(node._id)) { const old = nodeMap.current.get(node._id); - try { - node.x = old?.x || node.x; - node.y = old?.y || node.y; - updateNode(node); - } catch (e) { - // node.x and .y become read-only when some layout algorithms are finished - } + node.x = old?.x || node.x; + node.y = old?.y || node.y; + updateNode(node); } else { createNode(node); } @@ -557,6 +563,16 @@ export const NLPixi = (props: Props) => { if (!props.graph) throw Error('Graph is undefined'); + //Setup d3 graph structure + graph.current = { + nodes: Object.values(props.graph.nodes).map((n) => ({ _id: n._id, x: n.defaultX, y: n.defaultY })), + links: Object.values(props.graph.links).map((l) => ({ + _id: l.id, + source: l.source, + target: l.target, + })), + }; + const size = ref.current?.getBoundingClientRect(); viewport.current = new Viewport({ screenWidth: size?.width || 1000, @@ -593,12 +609,12 @@ export const NLPixi = (props: Props) => { if (!layoutAlgorithm) throw Error('LayoutAlgorithm is undefined'); const graphologyGraph = new MultiGraph(); - props.graph?.nodes.forEach((node) => { - if (forceClear) graphologyGraph.addNode(node._id, { size: node.radius || 5 }); - else graphologyGraph.addNode(node._id, { size: node.radius || 5, x: node.x || 0, y: node.y || 0 }); + graph.current.nodes.forEach((node) => { + if (forceClear) graphologyGraph.addNode(node._id, { size: props.graph.nodes[node._id].radius || 5 }); + else graphologyGraph.addNode(node._id, { size: props.graph.nodes[node._id].radius || 5, x: node.x || 0, y: node.y || 0 }); }); - props.graph?.links.forEach((link) => { + graph.current.links.forEach((link) => { graphologyGraph.addEdge(link.source, link.target); }); const boundingBox = { x1: 0, x2: app.renderer.screen.width, y1: 0, y2: app.renderer.screen.height }; @@ -612,21 +628,36 @@ export const NLPixi = (props: Props) => { return ( <> - {popups.map((popup, index) => ( + {popups.map((popup) => ( <Tooltip key={popup.node._id} open={true} boundaryElement={ref} showArrow={true}> <TooltipTrigger x={popup.pos.x} y={popup.pos.y} /> <TooltipContent> <div> <CardToolTipVis type="popupvis" - name={popup.node.label} - colorHeader={nodeColorHex(popup.node.type)} - data={popup.node.attributes} + name={props.graph.nodes[popup.node._id].label} + colorHeader={nodeColorHex(props.graph.nodes[popup.node._id].type)} + data={props.graph.nodes[popup.node._id].attributes} /> </div> </TooltipContent> </Tooltip> ))} + {quickPopup != null && ( + <Tooltip key={quickPopup.node._id} open={true} boundaryElement={ref} showArrow={true}> + <TooltipTrigger x={quickPopup.pos.x} y={quickPopup.pos.y} /> + <TooltipContent> + <div> + <CardToolTipVis + type="popupvis" + name={props.graph.nodes[quickPopup.node._id].label} + colorHeader={nodeColorHex(props.graph.nodes[quickPopup.node._id].type)} + data={props.graph.nodes[quickPopup.node._id].attributes} + /> + </div> + </TooltipContent> + </Tooltip> + )} <div className="h-full w-full overflow-hidden" ref={ref} diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx index 990719dc6febaa81440ac6f1c8d1aa8fd71c535b..952d6bc4eb2a9ee83f68aa19849474238eddcf39 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx @@ -142,7 +142,11 @@ type OptionsI = { * @returns {GraphType} A node-link graph containing the nodes and links for the diagram. */ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: OptionsI = {}): GraphType { - const nodes: NodeType[] = []; + let ret: GraphType = { + nodes: {}, + links: {}, + }; + const typeDict: { [key: string]: number } = {}; // Counter for the types let counter = 1; @@ -184,8 +188,8 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: type: typeNumber, displayInfo: preferredText, radius: radius, - x: (options.defaultX || 0) + Math.random() * radius * 20 - radius * 10, - y: (options.defaultY || 0) + Math.random() * radius * 20 - radius * 10, + defaultX: (options.defaultX || 0) + Math.random() * radius * 20 - radius * 10, + defaultY: (options.defaultY || 0) + Math.random() * radius * 20 - radius * 10, }; // let mlExtra = {}; @@ -209,13 +213,13 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: // Add mlExtra to the node if necessary // data = { ...data, ...mlExtra }; - nodes.push(data); + ret.nodes[data._id] = data; } // Filter unique edges and transform to LinkTypes // List for all links let links: LinkType[] = []; - let allNodeIds = new Set(nodes.map((n) => n._id)); + let allNodeIds = new Set(Object.keys(ret.nodes)); // Parse ml edges // if (ml != undefined) { @@ -239,14 +243,14 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: for (let i = 0; i < uniqueEdges.length; i++) { if (allNodeIds.has(uniqueEdges[i].from) && allNodeIds.has(uniqueEdges[i].to)) { const toAdd: LinkType = { - id: uniqueEdges[i].from + ':' + uniqueEdges[i].to, + id: uniqueEdges[i].from + ':' + uniqueEdges[i].to, // TODO: this only supports one link between two nodes source: uniqueEdges[i].from, target: uniqueEdges[i].to, value: uniqueEdges[i].count, mlEdge: false, color: 0x000000, }; - links.push(toAdd); + ret.links[toAdd.id] = toAdd; } } @@ -266,13 +270,13 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: } // Graph to be returned - let toBeReturned: GraphType = { - nodes: nodes, - links: links, - // linkPrediction: linkPredictionInResult, - // shortestPath: shortestPathInResult, - // communityDetection: communityDetectionInResult, - }; + // let toBeReturned: GraphType = { + // nodes: nodes, + // links: links, + // linkPrediction: linkPredictionInResult, + // shortestPath: shortestPathInResult, + // communityDetection: communityDetectionInResult, + // }; // If query with community detection; add number of clusters to the graph // const numberOfClusters = { @@ -283,5 +287,5 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: // } // return toBeReturned; - return processML(ml, toBeReturned); + return processML(ml, ret); } diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx index 73012f7de90bd815acdcd9a923e222821bcf8dc5..6e336b56f23cd36053a2b820f49b7a3cd21d4a11 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx @@ -68,11 +68,12 @@ export function hslStringToHex(hsl: string) { */ export const getRelatedLinks = (graph: GraphType, nodes: NodeType[], jaccardThreshold: number): LinkType[] => { const relatedLinks: LinkType[] = []; - graph.links.forEach((link: LinkType) => { + Object.keys(graph.links).forEach((id) => { + const link = graph.links[id]; const { source, target } = link; if (isLinkVisible(link, jaccardThreshold)) { nodes.forEach((node: NodeType) => { - if (source == node || target == node || source == node._id || target == node._id) { + if (source == node._id || target == node._id || source == node._id || target == node._id) { relatedLinks.push(link); } }); diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx index 70c7867ba36adf9dbfe162a15f597dbce985a20d..4daa884911470b09db5daa4058e226ddb7267d54 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { GraphType, LinkType, NodeType } from './types'; +import { GraphType, LinkType, NodeType, NodeTypeD3 } from './types'; import { NLPixi } from './components/NLPixi'; import { parseQueryResult } from './components/query2NL'; import { useImmer } from 'use-immer'; @@ -66,7 +66,7 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel } }, [data, ml]); - const onClickedNode = (event?: { node: NodeType; pos: IPointData }, ml?: ML) => { + const onClickedNode = (event?: { node: NodeTypeD3; pos: IPointData }, ml?: ML) => { if (graph) { if (!event?.node) { handleSelect(); @@ -74,11 +74,12 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel } const node = event.node; - handleSelect({ nodes: [node as Node] }); + const nodeMeta = graph.nodes[node._id]; + handleSelect({ nodes: [nodeMeta as Node] }); if (ml && ml.shortestPath.enabled) { setGraph((draft) => { - let _node = draft?.nodes.find((n) => n._id === node._id); + let _node = draft?.nodes[node._id]; if (!_node) return draft; if (!ml.shortestPath.srcNode) { @@ -105,6 +106,7 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSel } }; + if (!graph) return null; return ( <NLPixi graph={graph} @@ -177,7 +179,7 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza type="dropdown" label="Shape" value={settings.shapes.shape} - options={[{circle: 'Circle'}, {rectangle: 'Square'}]} + options={[{ circle: 'Circle' }, { rectangle: 'Square' }]} onChange={(val) => updateSettings({ shapes: { ...settings.shapes, shape: val as any } })} /> ) : ( diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts index 7374b774b60577320b3a832c1fe096b94fd8c287..321f20198e309108b450dc0d352eb15ef9216950 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts @@ -9,16 +9,21 @@ import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResul /** Types for the nodes and links in the node-link diagram. */ export type GraphType = { - nodes: NodeType[]; - links: LinkType[]; + nodes: Record<string, NodeType>; // _id -> node + links: Record<string, LinkType>; // _id -> link // linkPrediction?: boolean; // shortestPath?: boolean; // communityDetection?: boolean; // numberOfMlClusters?: number; }; +export type GraphTypeD3 = { + nodes: NodeTypeD3[]; + links: LinkTypeD3[]; +}; + /** The interface for a node in the node-link diagram */ -export interface NodeType extends d3.SimulationNodeDatum, Node { +export interface NodeType extends Node { _id: string; // Number to determine the color of the node @@ -41,28 +46,14 @@ export interface NodeType extends d3.SimulationNodeDatum, Node { // The text that will be shown on top of the node if selected. displayInfo?: string; - - // Node’s current x-position. - x?: number; - - // Node’s current y-position. - y?: number; - - // Node’s current x-velocity - vx?: number; - - // Node’s current y-velocity - vy?: number; - - // Node’s fixed x-position (if position was fixed) - fx?: number | null; - - // Node’s fixed y-position (if position was fixed) - fy?: number | null; + defaultX?: number; + defaultY?: number; } +export type NodeTypeD3 = d3.SimulationNodeDatum & { _id: string }; + /** The interface for a link in the node-link diagram */ -export interface LinkType extends d3.SimulationLinkDatum<NodeType> { +export interface LinkType { // The thickness of a line id: string; value: number; @@ -70,14 +61,18 @@ export interface LinkType extends d3.SimulationLinkDatum<NodeType> { mlEdge: boolean; color: number; alpha?: number; + source: string; + target: string; } +export type LinkTypeD3 = d3.SimulationLinkDatum<NodeTypeD3> & { _id: string }; + /**collectionNode holds 1 entry per node kind (so for example a MockNode with name "parties" and all associated attributes,) */ export type TypeNode = { name: string; //Collection name attributes: string[]; //attributes. This includes all attributes found in the collection type: number | undefined; //number that represents collection of node, for colorscheme - visualisations: Visualization[]; //The way to visualize attributes of this Node kind + visualizations: Visualization[]; //The way to visualize attributes of this Node kind }; export type CommunityDetectionNode = {