From cb121b7bbe3d4129a63482ee8681cd3385fd3e15 Mon Sep 17 00:00:00 2001
From: Leonardo <leomilho@gmail.com>
Date: Tue, 14 May 2024 14:37:03 +0200
Subject: [PATCH] feat: inspector panel

---
 libs/shared/lib/data-access/api/eventBus.tsx  |  2 +
 .../store/graphQueryResultSlice.ts            | 12 +--
 libs/shared/lib/data-access/store/hooks.ts    |  4 +
 .../lib/data-access/store/interactionSlice.ts | 46 +++++++---
 libs/shared/lib/data-access/store/store.ts    |  2 +
 libs/shared/lib/schema/model/reactflow.tsx    | 49 +++++------
 libs/shared/lib/vis/common/types.ts           | 32 +++----
 .../config/ActiveVisualizationConfig.tsx      | 45 ++++++++++
 .../vis/components/config/SelectionConfig.tsx | 48 +++++++++++
 .../lib/vis/components/config/panel.tsx       | 48 ++---------
 .../lib/vis/manager/VisualizationManager.tsx  |  8 +-
 .../components/NLMachineLearning.tsx          |  8 +-
 .../nodelinkvis/components/NLPixi.tsx         | 84 +++++++++++--------
 .../nodelinkvis/components/NLPopup.tsx        |  2 +-
 .../nodelinkvis/components/query2NL.tsx       |  4 +-
 .../nodelinkvis/components/utils.tsx          |  2 +-
 .../nodelinkvis/nodelinkvis.tsx               | 34 +++++---
 .../vis/visualizations/nodelinkvis/types.ts   |  9 +-
 18 files changed, 274 insertions(+), 165 deletions(-)
 create mode 100644 libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx
 create mode 100644 libs/shared/lib/vis/components/config/SelectionConfig.tsx

diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx
index f49d266e0..01c72fb6a 100644
--- a/libs/shared/lib/data-access/api/eventBus.tsx
+++ b/libs/shared/lib/data-access/api/eventBus.tsx
@@ -50,6 +50,7 @@ import { setVisualizationState } from '../store/visualizationSlice';
 import { isEqual } from 'lodash-es';
 import { addSchemaAttributeDimensions } from '../store/schemaSlice';
 import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice';
+import { unSelect } from '../store/interactionSlice';
 
 export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }) => {
   const { login } = useAuth();
@@ -88,6 +89,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
       wsQuerySubscription((data) => {
         dispatch(setNewGraphQueryResult(data));
         dispatch(addInfo('Query Executed!'));
+        dispatch(unSelect());
       }),
     );
 
diff --git a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts
index ada9c3028..48968c579 100755
--- a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts
+++ b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts
@@ -31,29 +31,29 @@ export interface GraphQueryResultFromBackend {
   // TODO: The backend should send all the different entitytypes and relationtypes in the result
 }
 
-export interface Node {
+export type Node = {
   _id: string;
   label: string;
   attributes: NodeAttributes;
   mldata?: any; // FIXME
   /* type: string[]; */
-}
-export interface Edge {
+};
+export type Edge = {
   attributes: NodeAttributes;
   from: string;
   to: string;
   _id: string;
   label: string;
   /* type: string; */
-}
+};
 
 // Define a type for the slice state
-export interface GraphQueryResult {
+export type GraphQueryResult = {
   metaData: GraphMetadata;
   nodes: Node[];
   edges: Edge[];
   queryingBackend: boolean;
-}
+};
 
 // Define the initial state using that type
 export const initialState: GraphQueryResult = {
diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts
index 729be1655..247785872 100644
--- a/libs/shared/lib/data-access/store/hooks.ts
+++ b/libs/shared/lib/data-access/store/hooks.ts
@@ -29,6 +29,7 @@ import { AllLayoutAlgorithms } from '../../graph-layout';
 import { QueryGraphEdgeHandle, QueryMultiGraph } from '../../querybuilder';
 import { SchemaGraph } from '../../schema';
 import { GraphMetadata } from '../statistics';
+import { SelectionStateI, selectionState } from './interactionSlice';
 
 // Use throughout your app instead of plain `useDispatch` and `useSelector`
 export const useAppDispatch: () => AppDispatch = useDispatch;
@@ -69,3 +70,6 @@ export const useRecentSearches: () => string[] = () => useAppSelector(recentSear
 
 // Visualization Slices
 export const useVisualization: () => VisState = () => useAppSelector(visualizationState);
+
+// Interaction Slices
+export const useSelection: () => SelectionStateI | undefined = () => useAppSelector(selectionState);
diff --git a/libs/shared/lib/data-access/store/interactionSlice.ts b/libs/shared/lib/data-access/store/interactionSlice.ts
index 89d18d410..4a7434768 100644
--- a/libs/shared/lib/data-access/store/interactionSlice.ts
+++ b/libs/shared/lib/data-access/store/interactionSlice.ts
@@ -1,19 +1,31 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import type { RootState } from './store';
+import { Edge, Node } from './graphQueryResultSlice';
 
-export type HoverType = { [id: string]: any };
+export type HoverStateI = { [id: string]: any };
 
-export type SelectType = { [id: string]: any };
+export type SelectionStateI =
+  | {
+      selectionType: 'node';
+      contentType: 'data';
+      content: Node[];
+    }
+  | {
+      // TODO: other selection types
+      selectionType: 'relation';
+      contentType: 'data';
+      content: Edge[];
+    };
 
 // Define the initial state using that type
 export type InteractionsType = {
-  hover: HoverType | undefined;
-  select: SelectType | undefined;
+  hover?: HoverStateI;
+  selection?: SelectionStateI;
 };
 
 export const initialState: InteractionsType = {
-  hover: {},
-  select: {},
+  hover: undefined,
+  selection: undefined,
 };
 
 export const interactionSlice = createSlice({
@@ -21,15 +33,29 @@ export const interactionSlice = createSlice({
   // `createSlice` will infer the state type from the `initialState` argument
   initialState,
   reducers: {
-    addHover: (state, action: PayloadAction<HoverType | undefined>) => {
+    addHover: (state, action: PayloadAction<HoverStateI | undefined>) => {
       state.hover = action.payload;
     },
-    addSelect: (state, action: PayloadAction<SelectType | undefined>) => {
-      state.select = action.payload;
+    unSelect: (state) => {
+      state.selection = undefined;
+    },
+    resultSetSelection: (state, action: PayloadAction<Node[]>) => {
+      if (action.payload.length === 0) {
+        state.selection = undefined;
+      } else {
+        state.selection = {
+          selectionType: 'node',
+          contentType: 'data',
+          content: action.payload,
+        };
+      }
     },
   },
 });
 
-export const { addHover, addSelect } = interactionSlice.actions;
+export const { addHover, unSelect, resultSetSelection } = interactionSlice.actions;
+
+export const interactionState = (state: RootState) => state.interaction;
+export const selectionState = (state: RootState) => state.interaction.selection;
 
 export default interactionSlice.reducer;
diff --git a/libs/shared/lib/data-access/store/store.ts b/libs/shared/lib/data-access/store/store.ts
index af20b8bef..ec6bce793 100644
--- a/libs/shared/lib/data-access/store/store.ts
+++ b/libs/shared/lib/data-access/store/store.ts
@@ -8,6 +8,7 @@ import authSlice from './authSlice';
 import mlSlice from './mlSlice';
 import searchResultSlice from './searchResultSlice';
 import visualizationSlice from './visualizationSlice';
+import interactionSlice from './interactionSlice';
 
 export const store = configureStore({
   reducer: {
@@ -19,6 +20,7 @@ export const store = configureStore({
     config: configSlice,
     ml: mlSlice,
     searchResults: searchResultSlice,
+    interaction: interactionSlice,
     visualize: visualizationSlice,
   },
   middleware: (getDefaultMiddleware) =>
diff --git a/libs/shared/lib/schema/model/reactflow.tsx b/libs/shared/lib/schema/model/reactflow.tsx
index f6fbbb3db..39a121106 100644
--- a/libs/shared/lib/schema/model/reactflow.tsx
+++ b/libs/shared/lib/schema/model/reactflow.tsx
@@ -20,16 +20,7 @@ export enum AttributeCategory {
   undefined = 'undefined',
 }
 
-// /**
-//  * List of schema elements for react flow
-//  */
-// export type SchemaElements = {
-//   nodes: Node[];
-//   edges: Edge[];
-//   selfEdges: Edge[];
-// };
-
-export interface SchemaReactflowData {
+export type SchemaReactflowData = {
   name: string;
   attributes: SchemaGraphologyNode[];
   nodeCount: number;
@@ -37,37 +28,37 @@ export interface SchemaReactflowData {
   label: string;
   type: string;
   hovered: boolean;
-}
+};
 
-export interface SchemaReactflowEntity extends SchemaReactflowData {
+export type SchemaReactflowEntity = SchemaReactflowData & {
   // handles: string[];
   connectedRatio: number;
   name: string;
-}
+};
 
-export interface SchemaReactflowRelation extends SchemaReactflowData {
+export type SchemaReactflowRelation = SchemaReactflowData & {
   from: string;
   to: string;
   collection: string;
   fromRatio: number;
   toRatio: number;
-}
+};
 
-export interface SchemaReactflowNodeWithFunctions extends SchemaReactflowEntity {
+export type SchemaReactflowNodeWithFunctions = SchemaReactflowEntity & {
   toggleNodeQualityPopup: (id: string) => void;
   toggleAttributeAnalyticsPopupMenu: (id: string) => void;
-}
+};
 
-export interface SchemaReactflowRelationWithFunctions extends SchemaReactflowRelation {
+export type SchemaReactflowRelationWithFunctions = SchemaReactflowRelation & {
   toggleNodeQualityPopup: (id: string) => void;
   toggleAttributeAnalyticsPopupMenu: (id: string) => void;
-}
+};
 
 /**
  * Typing for the Node Quality data of an entity.
  * It is used for the Node quality analytics and will be displayed in the corresponding popup.
  */
-export interface NodeQualityDataForEntities {
+export type NodeQualityDataForEntities = {
   nodeCount: number;
   attributeNullCount: number;
   notConnectedNodeCount: number;
@@ -76,13 +67,13 @@ export interface NodeQualityDataForEntities {
 
   // for user interactions
   onClickCloseButton: () => void;
-}
+};
 
 /**
  * Typing for the Node Quality data of a relation.
  * It is used for the Node quality analytics and will be displayed in the corresponding popup.
  */
-export interface NodeQualityDataForRelations {
+export type NodeQualityDataForRelations = {
   nodeCount: number;
   attributeNullCount: number;
   // from-entity node --relation--> to-entity node
@@ -93,20 +84,20 @@ export interface NodeQualityDataForRelations {
 
   // for user interactions
   onClickCloseButton: () => void;
-}
+};
 
 /**
  * Typing for the Node Quality popup of an entity or relation node.
  */
-export interface NodeQualityPopupNode extends Node {
+export type NodeQualityPopupNode = Node & {
   data: NodeQualityDataForEntities | NodeQualityDataForRelations;
   nodeID: string; //ID of the node for which the popup is
-}
+};
 
 /**
  * Typing for the attribute analytics popup menu data of an entity or relation.
  */
-export interface AttributeAnalyticsData {
+export type AttributeAnalyticsData = {
   nodeType: NodeType;
   nodeID: string;
   attributes: AttributeWithData[];
@@ -120,15 +111,15 @@ export interface AttributeAnalyticsData {
   searchForAttributes: (id: string, searchbarValue: string) => void;
   resetAttributeFilters: (id: string) => void;
   applyAttributeFilters: (id: string, category: AttributeCategory, predicate: string, percentage: number) => void;
-}
+};
 
 /**
  * Typing for the attribute analytics popup menu of entity or relation nodes
  */
-export interface AttributeAnalyticsPopupMenuNode extends Node {
+export type AttributeAnalyticsPopupMenuNode = Node & {
   nodeID: string; //ID of the node for which the popup is
   data: AttributeAnalyticsData;
-}
+};
 
 /** Typing of the attributes which are stored in the popup menu's */
 export type AttributeWithData = {
diff --git a/libs/shared/lib/vis/common/types.ts b/libs/shared/lib/vis/common/types.ts
index 37a3ea76e..2d8f8bf3d 100644
--- a/libs/shared/lib/vis/common/types.ts
+++ b/libs/shared/lib/vis/common/types.ts
@@ -4,8 +4,8 @@ import { SchemaGraph } from '../../schema';
 import type { AppDispatch } from '../../data-access';
 import { FC } from 'react';
 import { Visualizations } from '../manager';
-import { Edge, Node } from 'reactflow';
 import { GraphMetadata } from '../../data-access/statistics';
+import { Node, Edge } from '../../data-access/store/graphQueryResultSlice';
 
 export type VisualizationConfiguration = { [id: string]: any };
 
@@ -25,7 +25,7 @@ export type VisualizationPropTypes = {
   graphMetadata: GraphMetadata;
   updateSettings: (newSettings: any) => void;
   handleHover: (val: any) => void;
-  handleSelect: (val: any) => void;
+  handleSelect: (selection?: { nodes?: Node[]; edges?: Edge[] }) => void;
 };
 
 export type SchemaElements = {
@@ -34,39 +34,39 @@ export type SchemaElements = {
   selfEdges: Edge[];
 };
 
-export interface Point {
+export type Point = {
   x: number;
   y: number;
-}
+};
 
-export interface BoundingBox {
+export type BoundingBox = {
   topLeft: Point;
   bottomRight: Point;
-}
+};
 
-export interface NodeQualityDataForEntities {
+export type NodeQualityDataForEntities = {
   nodeCount: number;
   attributeNullCount: number;
   notConnectedNodeCount: number;
   isAttributeDataIn: boolean; // is true when the data to display has arrived
   onClickCloseButton: () => void;
-}
+};
 
-export interface NodeQualityDataForRelations {
+export type NodeQualityDataForRelations = {
   nodeCount: number;
   attributeNullCount: number;
   fromRatio: number; // the ratio of from-entity nodes to nodes that have this relation
   toRatio: number; // the ratio of to-entity nodes to nodes that have this relation
   isAttributeDataIn: boolean; // is true when the data to display has arrived
   onClickCloseButton: () => void;
-}
+};
 
-export interface NodeQualityPopupNode extends Node {
+export type NodeQualityPopupNode = Node & {
   data: NodeQualityDataForEntities | NodeQualityDataForRelations;
   nodeID: string; //ID of the node for which the popup is
-}
+};
 
-export interface AttributeAnalyticsData {
+export type AttributeAnalyticsData = {
   nodeType: NodeType;
   nodeID: string;
   attributes: AttributeWithData[];
@@ -76,7 +76,7 @@ export interface AttributeAnalyticsData {
   searchForAttributes: (id: string, searchbarValue: string) => void;
   resetAttributeFilters: (id: string) => void;
   applyAttributeFilters: (id: string, category: AttributeCategory, predicate: string, percentage: number) => void;
-}
+};
 
 export enum AttributeCategory {
   categorical = 'Categorical',
@@ -90,10 +90,10 @@ export enum NodeType {
   relation = 'relation',
 }
 
-export interface AttributeAnalyticsPopupMenuNode extends Node {
+export type AttributeAnalyticsPopupMenuNode = Node & {
   nodeID: string; //ID of the node for which the popup is
   data: AttributeAnalyticsData;
-}
+};
 
 export type AttributeWithData = {
   attribute: any;
diff --git a/libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx b/libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx
new file mode 100644
index 000000000..fa36ceed1
--- /dev/null
+++ b/libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx
@@ -0,0 +1,45 @@
+import { Delete } from '@mui/icons-material';
+import { Button, Input } from '../../..';
+import { VisualizationManagerType, VISUALIZATION_TYPES } from '../../manager';
+import { SettingsHeader } from './components';
+
+type Props = {
+  manager: VisualizationManagerType;
+};
+
+export const ActiveVisualizationConfig = ({ manager }: Props) => {
+  return (
+    <>
+      <div className="border-b py-2">
+        <div className="flex justify-between items-center px-4 py-2">
+          <span className="text-xs font-bold">Visualization</span>
+          <Button
+            type="secondary"
+            variant="ghost"
+            size="xs"
+            iconComponent={<Delete />}
+            onClick={() => {
+              if (manager.activeVisualization) manager.deleteVisualization(manager.activeVisualization);
+            }}
+          />
+        </div>
+        <div className="flex justify-between items-center px-4 py-1">
+          <span className="text-xs font-normal">Type</span>
+          <div className="w-36">
+            <Input type="dropdown" size="xs" options={VISUALIZATION_TYPES} value={manager.activeVisualization} onChange={() => {}} />
+          </div>
+        </div>
+        <div className="flex justify-between items-center px-4 py-1">
+          <span className="text-xs font-normal">Name</span>
+          <input type="text" className="border rouded text-xs w-36" value={manager.activeVisualization} onChange={() => {}} />
+        </div>
+      </div>
+      {manager.activeVisualization && (
+        <div className="border-b p-4 w-full">
+          <SettingsHeader name="Configuration" />
+          {manager.renderSettings()}
+        </div>
+      )}
+    </>
+  );
+};
diff --git a/libs/shared/lib/vis/components/config/SelectionConfig.tsx b/libs/shared/lib/vis/components/config/SelectionConfig.tsx
new file mode 100644
index 000000000..d790d651f
--- /dev/null
+++ b/libs/shared/lib/vis/components/config/SelectionConfig.tsx
@@ -0,0 +1,48 @@
+import { SelectionStateI, unSelect } from '@graphpolaris/shared/lib/data-access/store/interactionSlice';
+import { Delete } from '@mui/icons-material';
+import { useDispatch } from 'react-redux';
+import { Button, EntityPill } from '../../..';
+import { VISUALIZATION_TYPES } from '../../manager';
+import { SettingsHeader } from './components';
+
+export const SelectionConfig = (props: { selection: SelectionStateI }) => {
+  const dispatch = useDispatch();
+
+  return (
+    <div className="border-b py-2">
+      <div className="flex justify-between items-center px-4 py-2">
+        <span className="text-xs font-bold">Selection</span>
+        <Button
+          type="secondary"
+          variant="ghost"
+          size="xs"
+          iconComponent={<Delete />}
+          onClick={() => {
+            dispatch(unSelect());
+          }}
+        />
+      </div>
+      {props.selection.content.map((item, index) => (
+        <>
+          <div key={index + 'id'} 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>
+          <div key={index + 'label'} className="flex justify-between items-center px-4 py-1 gap-1">
+            <span className="text-xs font-normal">Label</span>
+            <EntityPill title={item.attributes['labels'] as string}></EntityPill>
+          </div>
+          {Object.entries(item.attributes).map(([key, value]) => {
+            if (key === 'labels' || key === '_id') return null;
+            return (
+              <div key={index + key} className="flex justify-between items-center px-4 py-1 gap-1">
+                <span className="text-xs font-normal break-all max-w-[6rem]">{key}</span>
+                <span className="text-xs break-all">{value as string}</span>
+              </div>
+            );
+          })}
+        </>
+      ))}
+    </div>
+  );
+};
diff --git a/libs/shared/lib/vis/components/config/panel.tsx b/libs/shared/lib/vis/components/config/panel.tsx
index 3e7d04c4e..22adadfbb 100644
--- a/libs/shared/lib/vis/components/config/panel.tsx
+++ b/libs/shared/lib/vis/components/config/panel.tsx
@@ -1,11 +1,9 @@
 import React from 'react';
-import { Button, Icon } from '../../../components';
-import { Delete, Person } from '@mui/icons-material';
-import { Input } from '../../../components/inputs';
-import { VISUALIZATION_TYPES } from '../../manager';
+import { Button } from '../../../components';
 import { VisualizationManagerType } from '../../manager';
-import { SettingsHeader } from './components';
-import { useSessionCache } from '../../../data-access';
+import { useSelection, useSessionCache } from '../../../data-access';
+import { SelectionConfig } from './SelectionConfig';
+import { ActiveVisualizationConfig } from './ActiveVisualizationConfig';
 
 type Props = {
   manager: VisualizationManagerType;
@@ -13,45 +11,15 @@ type Props = {
 
 export function ConfigPanel({ manager }: Props) {
   const session = useSessionCache();
+  const selection = useSelection();
 
   const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION;
 
   return (
     <div className="flex flex-col w-full">
-      {manager.activeVisualization ? (
-        <>
-          <div className="border-b py-2">
-            <div className="flex justify-between items-center px-4 py-2">
-              <span className="text-xs font-bold">Visualization</span>
-              <Button
-                type="secondary"
-                variant="ghost"
-                size="xs"
-                iconComponent={<Delete />}
-                onClick={() => {
-                  if (manager.activeVisualization) manager.deleteVisualization(manager.activeVisualization);
-                }}
-              />
-            </div>
-            <div className="flex justify-between items-center px-4 py-1">
-              <span className="text-xs font-normal">Type</span>
-              <div className="w-36">
-                <Input type="dropdown" size="xs" options={VISUALIZATION_TYPES} value={manager.activeVisualization} onChange={() => {}} />
-              </div>
-            </div>
-            <div className="flex justify-between items-center px-4 py-1">
-              <span className="text-xs font-normal">Name</span>
-              <input type="text" className="border rouded text-xs w-36" value={manager.activeVisualization} onChange={() => {}} />
-            </div>
-          </div>
-          {manager.activeVisualization && (
-            <div className="border-b p-4 w-full">
-              <SettingsHeader name="Configuration" />
-              {manager.renderSettings()}
-            </div>
-          )}
-        </>
-      ) : (
+      {!!selection && <SelectionConfig selection={selection} />}
+      {!selection && manager.activeVisualization && <ActiveVisualizationConfig manager={manager} />}
+      {!selection && !manager.activeVisualization && (
         <div>
           {session && session.currentSaveState && (
             <div className="flex flex-col p-4 border-b">
diff --git a/libs/shared/lib/vis/manager/VisualizationManager.tsx b/libs/shared/lib/vis/manager/VisualizationManager.tsx
index 06add912d..3118611fd 100644
--- a/libs/shared/lib/vis/manager/VisualizationManager.tsx
+++ b/libs/shared/lib/vis/manager/VisualizationManager.tsx
@@ -16,7 +16,8 @@ import {
   useVisualization,
 } from '../../data-access';
 import { VisualizationManagerType } from '.';
-import { SelectType, addSelect } from '../../data-access/store/interactionSlice';
+import { Node, Edge } from '../../data-access/store/graphQueryResultSlice';
+import { SelectionStateI, resultSetSelection, unSelect } from '../../data-access/store/interactionSlice';
 
 export const Visualizations: Record<string, Function> = {
   TableVis: () => import('../visualizations/tablevis/tablevis'),
@@ -100,8 +101,9 @@ export const VisualizationManager = (): VisualizationManagerType => {
     }
   };
 
-  const handleSelect = (item: SelectType | undefined) => {
-    dispatch(addSelect(item));
+  const handleSelect = (selection?: { nodes?: Node[]; edges?: Edge[] }) => {
+    if (selection?.nodes && selection.nodes.length > 0) dispatch(resultSetSelection(selection.nodes));
+    else dispatch(unSelect());
   };
 
   const updateSettings = (newSettings: Record<string, any>) => {
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx
index 6ac50ebe2..78d36400c 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx
@@ -6,7 +6,7 @@ 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(graph.nodes.map((n) => n._id));
     ml.linkPrediction.result.forEach((link) => {
       if (allNodeIds.has(link.from) && allNodeIds.has(link.to)) {
         const toAdd: LinkType = {
@@ -36,8 +36,8 @@ 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);
+      if (allNodeIdMap.has(node._id)) {
+        node.cluster = allNodeIdMap.get(node._id);
       } else {
         node.cluster = -1;
       }
@@ -89,7 +89,7 @@ export const useNLMachineLearning = (props: {
       if (shortestPathData === undefined) {
         console.warn('Something went wrong with shortest path calculation');
       } else {
-        const path: string[] = shortestPathData[shortestPathNodes[index + 1].id];
+        const path: string[] = shortestPathData[shortestPathNodes[index + 1]._id];
         allPaths = allPaths.concat(getShortestPathEdges(path));
       }
       index++;
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
index 20b07dc42..340dcfa7e 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
@@ -9,9 +9,10 @@ import { CytoscapeLayout, GraphologyLayout, LayoutFactory, Layouts } from '../..
 import { MultiGraph } from 'graphology';
 import { VisualizationConfiguration } from '../../../common';
 import { Viewport } from 'pixi-viewport';
+import { c } from 'vite/dist/node/types.d-aGj9QkWt';
 
 type Props = {
-  onClick: (node: NodeType, pos: IPointData) => void;
+  onClick: (event?: { node: NodeType; pos: IPointData }) => void;
   // onHover: (data: { node: NodeType; pos: IPointData }) => void;
   // onUnHover: (data: { node: NodeType; pos: IPointData }) => void;
   highlightNodes: NodeType[];
@@ -53,7 +54,6 @@ export const NLPixi = (props: Props) => {
   const ml = useML();
   const dragging = useRef<{ node: NodeType; gfx: Graphics } | null>(null);
   const onlyClicked = useRef(false);
-  const dispatch = useAppDispatch();
   const searchResults = useSearchResultData();
 
   const layoutAlgorithm = useRef<CytoscapeLayout | GraphologyLayout>(new LayoutFactory().createLayout(Layouts.DAGRE));
@@ -92,19 +92,21 @@ export const NLPixi = (props: Props) => {
 
   useImperativeHandle(imperative, () => ({
     onDragStart(node: NodeType, gfx: Graphics) {
+      dragging.current = { node, gfx };
+      onlyClicked.current = true;
+
       // todo: graphology does not support fixed nodes
       // todo: after vis-settings panel is there, we should to also support the original d3 force to allow interactivity if needed
       if (props.layoutAlgorithm === Layouts.FORCEATLAS2WEBWORKER) return;
       if (viewport.current) viewport.current.pause = true;
-      dragging.current = { node, gfx };
-      onlyClicked.current = true;
     },
 
     onDragMove(movementX: number, movementY: number) {
+      if (props.layoutAlgorithm === Layouts.FORCEATLAS2WEBWORKER) return;
       if (dragging.current) {
         onlyClicked.current = false;
         if (quickPopup) setQuickPopup(undefined);
-        const idx = popups.findIndex((p) => p.node.id === dragging.current?.node.id);
+        const idx = popups.findIndex((p) => p.node._id === dragging.current?.node._id);
         if (idx >= 0) {
           const p = popups[idx];
           p.pos.x += movementX / (viewport.current?.scaled || 1);
@@ -129,17 +131,17 @@ export const NLPixi = (props: Props) => {
         if (onlyClicked.current) {
           onlyClicked.current = false;
 
-          if (popups.filter((d) => d.node.id === dragging.current?.node.id).length > 0) {
-            setPopups(popups.filter((p) => p.node.id !== dragging.current?.node.id));
+          if (popups.filter((d) => d.node._id === dragging.current?.node._id).length > 0) {
+            setPopups(popups.filter((p) => p.node._id !== dragging.current?.node._id));
+            props.onClick();
           } else {
-            console.log('clicked', popups);
             setPopups([...popups, { node: dragging.current.node, pos: toGlobal(dragging.current.node) }]);
+            props.onClick({ node: dragging.current.node, pos: toGlobal(dragging.current.node) });
           }
-
-          props.onClick(dragging.current.node, toGlobal(dragging.current.node));
         }
         this.onHover(dragging.current.node);
         dragging.current = null;
+      } else {
       }
     },
     onHover(node: NodeType) {
@@ -148,7 +150,7 @@ export const NLPixi = (props: Props) => {
         viewport?.current &&
         !viewport?.current?.pause &&
         node &&
-        popups.filter((p) => p.node.id === node.id).length === 0
+        popups.filter((p) => p.node._id === node._id).length === 0
       ) {
         setQuickPopup({ node, pos: toGlobal(node) });
       }
@@ -158,6 +160,7 @@ export const NLPixi = (props: Props) => {
     },
     onPan() {
       setPopups([]);
+      props.onClick();
     },
   }));
 
@@ -216,7 +219,7 @@ export const NLPixi = (props: Props) => {
   }
 
   const updateNode = (node: NodeType) => {
-    const gfx = nodeMap.current.get(node.id);
+    const gfx = nodeMap.current.get(node._id);
     if (!gfx) return;
 
     const lineColor = node.isShortestPathSource
@@ -270,18 +273,18 @@ export const NLPixi = (props: Props) => {
 
   const createNode = (node: NodeType, selected?: boolean) => {
     // 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);
+    if (node && node?._id && nodeMap.current?.has(node._id)) {
+      nodeMap.current.delete(node._id);
     }
     // Do not draw node if it has no position
     if (node.x === undefined || node.y === undefined) return;
     const gfx = new Graphics();
-    nodeMap.current.set(node.id, gfx);
+    nodeMap.current.set(node._id, gfx);
     nodeLayer.addChild(gfx);
     node.selected = selected;
 
     updateNode(node);
-    gfx.name = 'node_' + node.id;
+    gfx.name = 'node_' + node._id;
     gfx.eventMode = 'dynamic';
 
     return gfx;
@@ -319,8 +322,8 @@ export const NLPixi = (props: Props) => {
     } else {
       source = link.source as NodeType;
       target = link.target as NodeType;
-      sourceId = source.id;
-      targetId = target.id;
+      sourceId = source._id;
+      targetId = target._id;
     }
     if (!source || !target) {
       console.error('source or target not found', source, target, sourceId, targetId);
@@ -401,9 +404,9 @@ export const NLPixi = (props: Props) => {
   useEffect(() => {
     if (props.graph) {
       props.graph.nodes.forEach((node: NodeType) => {
-        const gfx = nodeMap.current.get(node.id);
+        const gfx = nodeMap.current.get(node._id);
         if (!gfx) return;
-        const isNodeInSearchResults = searchResults.nodes.some((resultNode) => resultNode.id === node.id);
+        const isNodeInSearchResults = searchResults.nodes.some((resultNode) => resultNode.id === node._id);
         gfx.alpha = isNodeInSearchResults || searchResults.nodes.length === 0 ? 1 : 0.05;
       });
 
@@ -426,22 +429,30 @@ export const NLPixi = (props: Props) => {
       let stopped = 0;
       props.graph.nodes.forEach((node: NodeType, i) => {
         if (!layoutAlgorithm.current) return;
-        const gfx = nodeMap.current.get(node.id);
+        const gfx = nodeMap.current.get(node._id);
         if (!gfx || node.x === undefined || node.y === undefined) return;
 
-        const position = layoutAlgorithm.current.getNodePosition(node.id);
+        const position = layoutAlgorithm.current.getNodePosition(node._id);
 
-        if (Math.abs(node.x - position.x - app.renderer.width / 2) + Math.abs(node.y - position.y - app.renderer.height / 2) < 1) {
+        if (
+          !position ||
+          Math.abs(node.x - position.x - app.renderer.width / 2) + Math.abs(node.y - position.y - app.renderer.height / 2) < 1
+        ) {
           stopped += 1;
           return;
         }
-        if (layoutAlgorithm.current.provider === 'Graphology') {
-          // this is a dirty hack to fix the graphology layout being out of bounds
-          node.x = position.x + app.renderer.width / 2;
-          node.y = position.y + app.renderer.height / 2;
-        } else {
-          node.x = position.x;
-          node.y = position.y;
+        try {
+          if (layoutAlgorithm.current.provider === 'Graphology') {
+            // this is a dirty hack to fix the graphology layout being out of bounds
+            node.x = position.x + app.renderer.width / 2;
+            node.y = position.y + app.renderer.height / 2;
+          } 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 (layoutState.current === 'running') {
@@ -489,7 +500,7 @@ export const NLPixi = (props: Props) => {
       }
 
       nodeMap.current.forEach((gfx, id) => {
-        if (!props.graph?.nodes?.find((node) => node.id === id)) {
+        if (!props.graph?.nodes?.find((node) => node._id === id)) {
           nodeLayer.removeChild(gfx);
           gfx.destroy();
           nodeMap.current.delete(id);
@@ -505,8 +516,8 @@ export const NLPixi = (props: Props) => {
       });
 
       props.graph.nodes.forEach((node: NodeType) => {
-        if (!forceClear && nodeMap.current.has(node.id)) {
-          const old = nodeMap.current.get(node.id);
+        if (!forceClear && nodeMap.current.has(node._id)) {
+          const old = nodeMap.current.get(node._id);
           node.x = old?.x || node.x;
           node.y = old?.y || node.y;
           updateNode(node);
@@ -574,7 +585,6 @@ export const NLPixi = (props: Props) => {
     });
 
     app.stage.eventMode = 'dynamic';
-    app.stage.on('mouseup', onDragEnd);
     app.stage.on('pointerup', onDragEnd);
     app.stage.on('mousemove', onDragMove);
     app.stage.on('mouseup', onDragEnd);
@@ -593,8 +603,8 @@ export const NLPixi = (props: Props) => {
 
     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 });
+      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 });
     });
 
     props.graph?.links.forEach((link) => {
@@ -610,7 +620,7 @@ export const NLPixi = (props: Props) => {
   };
   return (
     <>
-      {mouseInCanvas.current && popups.map((popup) => <NLPopup onClose={() => {}} data={popup} key={popup.node.id} />)}
+      {mouseInCanvas.current && popups.map((popup) => <NLPopup onClose={() => {}} data={popup} key={popup.node._id} />)}
       {quickPopup && <NLPopup onClose={() => {}} data={quickPopup} />}
       <div
         className="h-full w-full overflow-hidden"
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx
index 7e07fbe08..679486e82 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx
@@ -18,7 +18,7 @@ export const NLPopup = (props: NodelinkPopupProps) => {
       <div className="card-body p-0">
         <span className="px-2.5 pt-2">
           <span>Node</span>
-          <span className="float-right">{node.id}</span>
+          <span className="float-right">{node._id}</span>
         </span>
         <div className="h-[1px] w-full bg-secondary-200"></div>
         <div className="px-2.5 text-[0.8rem]">
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx
index 33ea7097b..f5b7381b4 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/query2NL.tsx
@@ -178,7 +178,7 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options:
 
     let radius = options.defaultRadius || 5;
     let data: NodeType = {
-      id: queryResult.nodes[i]._id,
+      _id: queryResult.nodes[i]._id,
       attributes: queryResult.nodes[i].attributes,
       type: typeNumber,
       displayInfo: preferredText,
@@ -214,7 +214,7 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options:
   // 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(nodes.map((n) => n._id));
 
   // Parse ml edges
   //   if (ml != undefined) {
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx
index b8929f4f8..36f17720a 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx
@@ -72,7 +72,7 @@ export const getRelatedLinks = (graph: GraphType, nodes: NodeType[], jaccardThre
     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 || target == node || 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 ca8bc5d5d..c6ba5d9bd 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx
@@ -11,6 +11,8 @@ import { SettingsContainer, SettingsHeader } from '@graphpolaris/shared/lib/vis/
 import { VISComponentType, VisualizationPropTypes } from '../../common';
 import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
 import { nodeColorHex } from './components/utils';
+import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
+import { IPointData } from 'pixi.js';
 
 export interface NodelinkVisProps {
   layout: string;
@@ -43,7 +45,7 @@ const configuration: NodelinkVisProps = {
   nodeList: [],
 };
 
-export const NodeLinkVis = React.memo(({ data, ml, dispatch, configuration }: VisualizationPropTypes) => {
+export const NodeLinkVis = React.memo(({ data, ml, dispatch, configuration, handleSelect }: VisualizationPropTypes) => {
   const ref = useRef<HTMLDivElement>(null);
   const [graph, setGraph] = useImmer<GraphType | undefined>(undefined);
   const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]);
@@ -60,29 +62,37 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, configuration }: Vi
     }
   }, [data, ml]);
 
-  const onClickedNode = (node: NodeType, ml: ML) => {
+  const onClickedNode = (event?: { node: NodeType; pos: IPointData }, ml?: ML) => {
     if (graph) {
-      if (ml.shortestPath.enabled) {
+      if (!event?.node) {
+        handleSelect();
+        return;
+      }
+
+      const node = event.node;
+      handleSelect({ nodes: [node as Node] });
+
+      if (ml && ml.shortestPath.enabled) {
         setGraph((draft) => {
-          let _node = draft?.nodes.find((n) => n.id === node.id);
+          let _node = draft?.nodes.find((n) => n._id === node._id);
           if (!_node) return draft;
 
           if (!ml.shortestPath.srcNode) {
             _node.isShortestPathSource = true;
-            dispatch(setShortestPathSource(node.id));
-          } else if (ml.shortestPath.srcNode === node.id) {
+            dispatch(setShortestPathSource(node._id));
+          } else if (ml.shortestPath.srcNode === node._id) {
             _node.isShortestPathSource = false;
             dispatch(setShortestPathSource(undefined));
           } else if (!ml.shortestPath.trtNode) {
             _node.isShortestPathTarget = true;
-            dispatch(setShortestPathTarget(node.id));
-          } else if (ml.shortestPath.trtNode === node.id) {
+            dispatch(setShortestPathTarget(node._id));
+          } else if (ml.shortestPath.trtNode === node._id) {
             _node.isShortestPathTarget = false;
             dispatch(setShortestPathTarget(undefined));
           } else {
             _node.isShortestPathSource = true;
             _node.isShortestPathTarget = false;
-            dispatch(setShortestPathSource(node.id));
+            dispatch(setShortestPathSource(node._id));
             dispatch(setShortestPathTarget(undefined));
           }
           return draft;
@@ -97,8 +107,8 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, configuration }: Vi
       configuration={configuration}
       highlightNodes={highlightNodes}
       highlightedLinks={highlightedLinks}
-      onClick={(node, pos) => {
-        onClickedNode(node, ml);
+      onClick={(event) => {
+        onClickedNode(event, ml);
       }}
       layoutAlgorithm={configuration.layout}
     />
@@ -127,7 +137,7 @@ const NodelinkSettings = ({
         <div className="m-1 flex flex-col space-y-4">
           <h1>Nodes Labels:</h1>
           {configuration.nodeList.map((item, index) => (
-            <div className="flex m-1 items-center">
+            <div className="flex m-1 items-center" key={item}>
               <div className="w-3/4 mr-6">
                 <EntityPill title={item} />
               </div>
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts
index c8f6bec1d..7374b774b 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts
@@ -5,6 +5,7 @@
  */
 
 import * as PIXI from 'pixi.js';
+import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
 
 /** Types for the nodes and links in the node-link diagram. */
 export type GraphType = {
@@ -17,13 +18,13 @@ export type GraphType = {
 };
 
 /** The interface for a node in the node-link diagram */
-export interface NodeType extends d3.SimulationNodeDatum {
-  id: string;
+export interface NodeType extends d3.SimulationNodeDatum, Node {
+  _id: string;
 
   // Number to determine the color of the node
-  label?: string;
+  label: string;
   type: number;
-  attributes?: Record<string, any>;
+  attributes: Record<string, any>;
   cluster?: number;
   clusterAccoringToMLData?: number;
   shortestPathData?: Record<string, string[]>;
-- 
GitLab