From fd7477c669bca6726747752a1b5c95d1e2b77799 Mon Sep 17 00:00:00 2001
From: Michael Behrisch <m.behrisch@uu.nl>
Date: Sat, 2 Apr 2022 20:34:13 +0200
Subject: [PATCH] fix(schema): :art: adds ui/pills shared library and uses it
 for schema

Pills and React Flow components impl can be shared for consistency.
Schema vs QueryBuilder can provide subclasses.
---
 apps/web-graphpolaris/project.json            |  36 ++----
 .../components/querybuilder/querybuilder.tsx  |   1 +
 .../src/components/schema/schema.tsx          |  24 ++++
 .../usecases/src/lib/schema-usecases.ts       | 112 ++++++++++--------
 .../graph-layout/src/lib/cytoscape-layouts.ts |   4 +-
 libs/shared/ui/pills/.babelrc                 |  12 ++
 libs/shared/ui/pills/.eslintrc.json           |  18 +++
 libs/shared/ui/pills/README.md                |   7 ++
 libs/shared/ui/pills/jest.config.js           |   9 ++
 libs/shared/ui/pills/project.json             |  23 ++++
 .../pills/src/customFlowLines/connection.tsx  |  74 ++++++++++++
 .../src/customFlowLines/connectionDrag.tsx    |  41 +++++++
 .../attributepill/attributepill.module.scss   |  60 ++++++++++
 .../attributepill/attributepill.tsx           |  96 +++++++++++++++
 .../attributepill/operatorselect.module.scss  |  70 +++++++++++
 .../attributepill/operatorselect.tsx          |  85 +++++++++++++
 .../attributepill/variables.module.scss       |   3 +
 .../entitypill/entitypill.module.scss         |  49 ++++++++
 .../customFlowPills/entitypill/entitypill.tsx |  64 ++++++++++
 .../src/customFlowPills/entitypill/index.ts   |   1 +
 .../src/customFlowPills/relationpill/index.ts |   1 +
 .../relationpill/relationpill.module.scss     | 110 +++++++++++++++++
 .../relationpill/relationpill.tsx             |  98 +++++++++++++++
 libs/shared/ui/pills/src/index.ts             |   9 ++
 .../pills/src/lib/shared-ui-pills.module.scss |   0
 .../ui/pills/src/lib/shared-ui-pills.spec.tsx |  10 ++
 .../ui/pills/src/lib/shared-ui-pills.tsx      |  14 +++
 .../pills/src/schema/entitypill.component.tsx |  62 ++++++++++
 .../pills/src/schema/entitypill.module.scss   |  49 ++++++++
 libs/shared/ui/pills/src/schema/index.ts      |   0
 libs/shared/ui/pills/tsconfig.json            |  25 ++++
 libs/shared/ui/pills/tsconfig.lib.json        |  22 ++++
 libs/shared/ui/pills/tsconfig.spec.json       |  19 +++
 tsconfig.base.json                            |   5 +-
 workspace.json                                |   1 +
 35 files changed, 1136 insertions(+), 78 deletions(-)
 create mode 100644 libs/shared/ui/pills/.babelrc
 create mode 100644 libs/shared/ui/pills/.eslintrc.json
 create mode 100644 libs/shared/ui/pills/README.md
 create mode 100644 libs/shared/ui/pills/jest.config.js
 create mode 100644 libs/shared/ui/pills/project.json
 create mode 100644 libs/shared/ui/pills/src/customFlowLines/connection.tsx
 create mode 100644 libs/shared/ui/pills/src/customFlowLines/connectionDrag.tsx
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/attributepill/attributepill.module.scss
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/attributepill/attributepill.tsx
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/attributepill/operatorselect.module.scss
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/attributepill/operatorselect.tsx
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/attributepill/variables.module.scss
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/entitypill/entitypill.module.scss
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/entitypill/entitypill.tsx
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/entitypill/index.ts
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/relationpill/index.ts
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/relationpill/relationpill.module.scss
 create mode 100644 libs/shared/ui/pills/src/customFlowPills/relationpill/relationpill.tsx
 create mode 100644 libs/shared/ui/pills/src/index.ts
 create mode 100644 libs/shared/ui/pills/src/lib/shared-ui-pills.module.scss
 create mode 100644 libs/shared/ui/pills/src/lib/shared-ui-pills.spec.tsx
 create mode 100644 libs/shared/ui/pills/src/lib/shared-ui-pills.tsx
 create mode 100644 libs/shared/ui/pills/src/schema/entitypill.component.tsx
 create mode 100644 libs/shared/ui/pills/src/schema/entitypill.module.scss
 create mode 100644 libs/shared/ui/pills/src/schema/index.ts
 create mode 100644 libs/shared/ui/pills/tsconfig.json
 create mode 100644 libs/shared/ui/pills/tsconfig.lib.json
 create mode 100644 libs/shared/ui/pills/tsconfig.spec.json

diff --git a/apps/web-graphpolaris/project.json b/apps/web-graphpolaris/project.json
index 864038895..7cc05ad71 100644
--- a/apps/web-graphpolaris/project.json
+++ b/apps/web-graphpolaris/project.json
@@ -19,9 +19,7 @@
     },
     "build": {
       "executor": "@nrwl/web:webpack",
-      "outputs": [
-        "{options.outputPath}"
-      ],
+      "outputs": ["{options.outputPath}"],
       "defaultConfiguration": "production",
       "options": {
         "compiler": "babel",
@@ -35,18 +33,18 @@
           "apps/web-graphpolaris/src/favicon.ico",
           "apps/web-graphpolaris/src/assets"
         ],
-        "styles": [
-          "apps/web-graphpolaris/src/styles.scss"
-        ],
+        "styles": ["apps/web-graphpolaris/src/styles.scss"],
         "scripts": [],
         "webpackConfig": "@nrwl/react/plugins/webpack"
       },
       "configurations": {
         "production": {
-          "fileReplacements": [{
-            "replace": "apps/graphpolaris/src/environments/environment.ts",
-            "with": "apps/graphpolaris/src/environments/environment.prod.ts"
-          }],
+          "fileReplacements": [
+            {
+              "replace": "apps/graphpolaris/src/environments/environment.ts",
+              "with": "apps/graphpolaris/src/environments/environment.prod.ts"
+            }
+          ],
           "optimization": true,
           "outputHashing": "all",
           "sourceMap": false,
@@ -71,20 +69,14 @@
     },
     "lint": {
       "executor": "@nrwl/linter:eslint",
-      "outputs": [
-        "{options.outputFile}"
-      ],
+      "outputs": ["{options.outputFile}"],
       "options": {
-        "lintFilePatterns": [
-          "apps/graphpolaris/**/*.{ts,tsx,js,jsx}"
-        ]
+        "lintFilePatterns": ["apps/graphpolaris/**/*.{ts,tsx,js,jsx}"]
       }
     },
     "test": {
       "executor": "@nrwl/jest:jest",
-      "outputs": [
-        "coverage/apps/graphpolaris"
-      ],
+      "outputs": ["coverage/apps/graphpolaris"],
       "options": {
         "jestConfig": "apps/web-graphpolaris/jest.config.js",
         "passWithNoTests": true
@@ -113,9 +105,7 @@
     },
     "build-storybook": {
       "executor": "@nrwl/storybook:build",
-      "outputs": [
-        "{options.outputPath}"
-      ],
+      "outputs": ["{options.outputPath}"],
       "options": {
         "uiFramework": "@storybook/react",
         "outputPath": "dist/storybook/graphpolaris",
@@ -131,4 +121,4 @@
     }
   },
   "tags": []
-}
\ No newline at end of file
+}
diff --git a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx
index 3870f76de..063330aa0 100644
--- a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx
+++ b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx
@@ -40,6 +40,7 @@ const QueryBuilder = (props: {}) => {
   const nodes = useQuerybuilderNodes();
   const dispatch = useAppDispatch();
   const isDraggingPill = useRef(false);
+  console.log('inputnodes', nodes);
 
   const elements = useMemo(() => createReactFlowElements(nodes), [nodes]);
 
diff --git a/apps/web-graphpolaris/src/components/schema/schema.tsx b/apps/web-graphpolaris/src/components/schema/schema.tsx
index e0a4d713b..dfe7a159b 100644
--- a/apps/web-graphpolaris/src/components/schema/schema.tsx
+++ b/apps/web-graphpolaris/src/components/schema/schema.tsx
@@ -18,6 +18,18 @@ import ReactFlow, {
 } from 'react-flow-renderer';
 import styles from './schema.module.scss';
 
+import {
+  EntityRFPill,
+  RelationRFPill,
+  AttributeRFPill,
+  ConnectionDragLine,
+  ConnectionLine
+} from '@graphpolaris/shared/ui/pills';
+// import ConnectionDragLine from '@graphpolaris/shared/ui/pills';
+// import AttributeRFPill from '@graphpolaris/shared/ui/pills';
+// import EntityRFPill from '@graphpolaris/shared/ui/pills';
+// import RelationRFPill from '@graphpolaris/shared/ui/pills';
+
 interface Props {
   // content: string;
 }
@@ -26,6 +38,15 @@ const onLoad = (reactFlowInstance: any) => {
   setTimeout(() => reactFlowInstance.fitView(), 100);
 };
 
+const nodeTypes = {
+  entity: EntityRFPill,
+  relation: RelationRFPill,
+  attribute: AttributeRFPill,
+};
+const edgeTypes = {
+  connection: ConnectionLine,
+};
+
 const Schema = (props: Props) => {
   const [elements, setElements] = useState([] as FlowElement[]);
   // In case the schema is updated
@@ -76,6 +97,9 @@ const Schema = (props: Props) => {
           className={styles.schemaPanel}
           onlyRenderVisibleElements={false}
           nodesDraggable={false}
+          nodeTypes={nodeTypes}
+          edgeTypes={edgeTypes}
+          connectionLineComponent={ConnectionDragLine}
           elements={elements}
           style={graphStyles}
           onLoad={onLoad}
diff --git a/libs/schema/usecases/src/lib/schema-usecases.ts b/libs/schema/usecases/src/lib/schema-usecases.ts
index 630db92a8..d3151bfe9 100644
--- a/libs/schema/usecases/src/lib/schema-usecases.ts
+++ b/libs/schema/usecases/src/lib/schema-usecases.ts
@@ -8,19 +8,25 @@ const ANIMATEDEDGES = false;
 export function expandSchema(graph: Graph): Graph {
   const newGraph = graph.copy();
 
-  // newGraph.forEachNode((node, attributes) => {
-  //   console.log(node, attributes);
-  // });
+  newGraph.forEachNode((node, attributes) => {
+    // console.log(node, attributes);
+    newGraph.mergeNodeAttributes(node, { type: 'entity' });
+  });
 
   //makeNewRelationNodes
   graph.forEachEdge((edge, attributes, source, target): void => {
     const newID = 'RelationNode:' + edge;
-    console.log('making relationnode', edge, attributes, source, target, newID);
+    // console.log('making relationnode', edge, attributes, source, target, newID);
     newGraph.addNode(newID, {
       name: edge,
+      data: {
+        label: edge,
+        name: edge,
+      },
       attributes,
       x: 0,
       y: 0,
+      type: 'relation',
     });
 
     const id = 'RelationEdge' + source + '->' + newID;
@@ -58,8 +64,10 @@ export function createReactFlowNodes(graph: Graph): Elements<Node> {
       id: node,
       data: {
         label: attributes.name,
+        name: attributes.name,
       },
       position: { x: attributes.x, y: attributes.y },
+      type: attributes.type,
     };
     nodeElements.push(newNode);
   });
@@ -67,53 +75,6 @@ export function createReactFlowNodes(graph: Graph): Elements<Node> {
   return nodeElements;
 }
 
-export function createReactFlowRelationNodes(graph: Graph): Elements<Node> {
-  const nodeElements: Elements<Node> = [];
-  graph.forEachEdge((edge, attributes, source, target): void => {
-    const newRelationNode: Node = {
-      id: edge,
-      data: {
-        label: edge,
-      },
-      position: { x: attributes.x, y: attributes.y },
-    };
-    nodeElements.push(newRelationNode);
-  });
-
-  return nodeElements;
-}
-
-export function createReactFlowRelationEdges(graph: Graph): Elements<Edge> {
-  const edgeElements: Elements<Edge> = [];
-  graph.forEachEdge((edge, attributes, source, target): void => {
-    const newEdgeIncoming: Edge = {
-      //into relation node
-      id: edge + '' + source,
-      source: source,
-      target: edge,
-      // label: edge,
-      type: 'smoothstep',
-      animated: ANIMATEDEDGES,
-      arrowHeadType: ArrowHeadType.ArrowClosed,
-    };
-    edgeElements.push(newEdgeIncoming);
-
-    const newEdgeOutgoing: Edge = {
-      //out of relation node
-      id: edge + '' + target,
-      source: edge,
-      target: target,
-      // label: edge,
-      type: 'smoothstep',
-      animated: ANIMATEDEDGES,
-      arrowHeadType: ArrowHeadType.ArrowClosed,
-    };
-    edgeElements.push(newEdgeOutgoing);
-  });
-
-  return edgeElements;
-}
-
 export function createReactFlowEdges(graph: Graph): Elements<Edge> {
   const edgeElements: Elements<Edge> = [];
 
@@ -134,6 +95,55 @@ export function createReactFlowEdges(graph: Graph): Elements<Edge> {
   return edgeElements;
 }
 
+// export function createReactFlowRelationNodes(graph: Graph): Elements<Node> {
+//   const nodeElements: Elements<Node> = [];
+//   graph.forEachEdge((edge, attributes, source, target): void => {
+//     const newRelationNode: Node = {
+//       id: edge,
+//       data: {
+//         label: edge,
+//         name: edge,
+//       },
+//       position: { x: attributes.x, y: attributes.y },
+//       type: 'relation',
+//     };
+//     nodeElements.push(newRelationNode);
+//   });
+
+//   return nodeElements;
+// }
+
+// export function createReactFlowRelationEdges(graph: Graph): Elements<Edge> {
+//   const edgeElements: Elements<Edge> = [];
+//   graph.forEachEdge((edge, attributes, source, target): void => {
+//     const newEdgeIncoming: Edge = {
+//       //into relation node
+//       id: edge + '' + source,
+//       source: source,
+//       target: edge,
+//       // label: edge,
+//       type: 'smoothstep',
+//       animated: ANIMATEDEDGES,
+//       arrowHeadType: ArrowHeadType.ArrowClosed,
+//     };
+//     edgeElements.push(newEdgeIncoming);
+
+//     const newEdgeOutgoing: Edge = {
+//       //out of relation node
+//       id: edge + '' + target,
+//       source: edge,
+//       target: target,
+//       // label: edge,
+//       type: 'smoothstep',
+//       animated: ANIMATEDEDGES,
+//       arrowHeadType: ArrowHeadType.ArrowClosed,
+//     };
+//     edgeElements.push(newEdgeOutgoing);
+//   });
+
+//   return edgeElements;
+// }
+
 // export function parseSchemaFromBackend(
 //   schemaFromBackend: SchemaFromBackend
 // ): Graph {
diff --git a/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts b/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts
index 0d1903a98..ea32eba73 100644
--- a/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts
+++ b/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts
@@ -244,10 +244,10 @@ class CytoscapeKlay extends Cytoscape {
       // boundingBox: undefined,
 
       ready: function () {
-        console.log('Layout.ready');
+        console.info('Layout.ready');
       }, // on layoutready
       stop: function () {
-        console.log('Layout.stop');
+        console.debug('Layout.stop');
       }, // on layoutstop
     } as any);
     layout.run();
diff --git a/libs/shared/ui/pills/.babelrc b/libs/shared/ui/pills/.babelrc
new file mode 100644
index 000000000..ccae900be
--- /dev/null
+++ b/libs/shared/ui/pills/.babelrc
@@ -0,0 +1,12 @@
+{
+  "presets": [
+    [
+      "@nrwl/react/babel",
+      {
+        "runtime": "automatic",
+        "useBuiltIns": "usage"
+      }
+    ]
+  ],
+  "plugins": []
+}
diff --git a/libs/shared/ui/pills/.eslintrc.json b/libs/shared/ui/pills/.eslintrc.json
new file mode 100644
index 000000000..3cd642175
--- /dev/null
+++ b/libs/shared/ui/pills/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+  "extends": ["plugin:@nrwl/nx/react", "../../../../.eslintrc.json"],
+  "ignorePatterns": ["!**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.ts", "*.tsx"],
+      "rules": {}
+    },
+    {
+      "files": ["*.js", "*.jsx"],
+      "rules": {}
+    }
+  ]
+}
diff --git a/libs/shared/ui/pills/README.md b/libs/shared/ui/pills/README.md
new file mode 100644
index 000000000..84610a47f
--- /dev/null
+++ b/libs/shared/ui/pills/README.md
@@ -0,0 +1,7 @@
+# shared-ui-pills
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test shared-ui-pills` to execute the unit tests via [Jest](https://jestjs.io).
diff --git a/libs/shared/ui/pills/jest.config.js b/libs/shared/ui/pills/jest.config.js
new file mode 100644
index 000000000..7d0623362
--- /dev/null
+++ b/libs/shared/ui/pills/jest.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+  displayName: 'shared-ui-pills',
+  preset: '../../../../jest.preset.js',
+  transform: {
+    '^.+\\.[tj]sx?$': 'babel-jest',
+  },
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
+  coverageDirectory: '../../../../coverage/libs/shared/ui/pills',
+};
diff --git a/libs/shared/ui/pills/project.json b/libs/shared/ui/pills/project.json
new file mode 100644
index 000000000..ea222183f
--- /dev/null
+++ b/libs/shared/ui/pills/project.json
@@ -0,0 +1,23 @@
+{
+  "root": "libs/shared/ui/pills",
+  "sourceRoot": "libs/shared/ui/pills/src",
+  "projectType": "library",
+  "tags": [],
+  "targets": {
+    "lint": {
+      "executor": "@nrwl/linter:eslint",
+      "outputs": ["{options.outputFile}"],
+      "options": {
+        "lintFilePatterns": ["libs/shared/ui/pills/**/*.{ts,tsx,js,jsx}"]
+      }
+    },
+    "test": {
+      "executor": "@nrwl/jest:jest",
+      "outputs": ["coverage/libs/shared/ui/pills"],
+      "options": {
+        "jestConfig": "libs/shared/ui/pills/jest.config.js",
+        "passWithNoTests": true
+      }
+    }
+  }
+}
diff --git a/libs/shared/ui/pills/src/customFlowLines/connection.tsx b/libs/shared/ui/pills/src/customFlowLines/connection.tsx
new file mode 100644
index 000000000..23fb1ec2e
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowLines/connection.tsx
@@ -0,0 +1,74 @@
+import { handles } from '@graphpolaris/querybuilder/usecases';
+import React from 'react';
+import { EdgeProps, getSmoothStepPath, Position } from 'react-flow-renderer';
+
+/**
+ * A custom query element edge line component.
+ * @param {EdgeProps} param0 The coordinates for the start and end point, the id and the style.
+ */
+//  export const EntityRFPill = React.memo(({ data }: { data: any }) => {
+export function ConnectionLine({
+  id,
+  sourceX,
+  sourceY,
+  targetX,
+  targetY,
+  style,
+  sourceHandleId,
+  targetHandleId,
+}: EdgeProps) {
+  //Centering the line
+  sourceY -= 3;
+  targetY -= 3;
+
+  // Correct line positions with hardcoded numbers, because react flow lacks this functionality
+  // if (sourceHandleId == ) sourceX += 2;
+
+  // if (targetHandleId == Handles.ToAttributeHandle) targetX += 2;
+
+  let spos: Position = Position.Bottom;
+  if (sourceHandleId == handles.relation.fromEntity) {
+    spos = Position.Left;
+    sourceX += 7;
+    sourceY += 3;
+  } else if (sourceHandleId == handles.relation.toEntity) {
+    spos = Position.Right;
+    sourceX -= 2;
+    sourceY -= 3;
+  } else if (
+    sourceHandleId !== undefined &&
+    sourceHandleId !== null &&
+    sourceHandleId.includes('functionHandle')
+  ) {
+    spos = Position.Top;
+    sourceX -= 4;
+    sourceY += 3;
+  }
+
+  let tpos: Position = Position.Bottom;
+  if (targetHandleId == handles.relation.fromEntity) {
+    tpos = Position.Left;
+    targetX += 7;
+    targetY += 3;
+  } else if (targetHandleId == handles.relation.toEntity) {
+    tpos = Position.Right;
+    targetX -= 2;
+    targetY -= 3;
+  }
+
+  // Create smoothstep line
+  const path = getSmoothStepPath({
+    sourceX: sourceX,
+    sourceY: sourceY,
+    sourcePosition: spos,
+    targetX: targetX,
+    targetY: targetY,
+    targetPosition: tpos,
+  });
+
+  return (
+    <g stroke="#2e2e2e">
+      <path id={id} fill="none" strokeWidth={3} style={style} d={path} />
+    </g>
+  );
+}
diff --git a/libs/shared/ui/pills/src/customFlowLines/connectionDrag.tsx b/libs/shared/ui/pills/src/customFlowLines/connectionDrag.tsx
new file mode 100644
index 000000000..0d567e584
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowLines/connectionDrag.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { ConnectionLineComponentProps } from 'react-flow-renderer';
+
+/**
+ * A custom query element to render the line when connecting flow elements.
+ * @param {ConnectionLineComponentProps} param0 Source and target coordinates of the edges.
+ */
+export function ConnectionDragLine({
+  sourceX,
+  sourceY,
+  targetX,
+  targetY,
+}: ConnectionLineComponentProps) {
+  return (
+    <g>
+      <path
+        fill="none"
+        stroke="#222"
+        strokeWidth={2.5}
+        className="animated"
+        d={`M${sourceX},${sourceY}L ${targetX},${targetY}`}
+      />
+      <circle
+        cx={sourceX}
+        cy={sourceY}
+        fill="#fff"
+        r={3}
+        stroke="#222"
+        strokeWidth={1.5}
+      />
+      <circle
+        cx={targetX}
+        cy={targetY}
+        fill="#fff"
+        r={3}
+        stroke="#222"
+        strokeWidth={1.5}
+      />
+    </g>
+  );
+}
diff --git a/libs/shared/ui/pills/src/customFlowPills/attributepill/attributepill.module.scss b/libs/shared/ui/pills/src/customFlowPills/attributepill/attributepill.module.scss
new file mode 100644
index 000000000..9ba5ba22c
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/attributepill/attributepill.module.scss
@@ -0,0 +1,60 @@
+@use './variables.module.scss';
+
+.attribute {
+  display: flex;
+  font-family: monospace;
+  font-weight: bold;
+  font-size: variables.$fontsize;
+  border-radius: 2px;
+}
+
+// .handle {
+//   border: 0px;
+//   border-radius: 10px;
+//   left: 12px;
+//   width: 7px;
+//   height: 7px;
+//   margin-bottom: 11px;
+//   background: rgba(255, 255, 255, 0.6);
+//   box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
+//   transform-origin: center;
+// }
+
+.contentWrapper {
+  display: flex;
+  align-items: center;
+
+  .content {
+    padding: variables.$ypad 0 variables.$ypad 1ch;
+    max-width: 15ch;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+  }
+}
+
+.attributeInput {
+  float: right;
+  padding: 0 1ch 0 0;
+  display: flex;
+  align-items: center;
+
+  input {
+    background-color: rgba(100, 100, 100, 0.1);
+    font-family: monospace;
+    font-size: variables.$fontsize;
+    border: 1px solid rgba(100, 100, 100, 0.3);
+    border-radius: 2px;
+    height: variables.$height;
+    outline: none;
+    transition: border 0.3s;
+    color: black;
+    &::placeholder {
+      color: black;
+    }
+
+    &:focus {
+      border: 1px solid rgba(0, 0, 0, 0.3);
+    }
+  }
+}
diff --git a/libs/shared/ui/pills/src/customFlowPills/attributepill/attributepill.tsx b/libs/shared/ui/pills/src/customFlowPills/attributepill/attributepill.tsx
new file mode 100644
index 000000000..41b5803cc
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/attributepill/attributepill.tsx
@@ -0,0 +1,96 @@
+import {
+  CheckDatatypeConstraint,
+  GetAttributeBoolOperators,
+} from '@graphpolaris/querybuilder/usecases';
+import {
+  updateQBAttributeOperator,
+  updateQBAttributeValue,
+  useAppDispatch,
+} from '@graphpolaris/shared/data-access/store';
+import { useTheme } from '@mui/material';
+import React, { useMemo, useState } from 'react';
+import styles from './attributepill.module.scss';
+import AttributeOperatorSelect from './operatorselect';
+
+/**
+ * Component to render an attribute flow element
+ * @param {FlowElement<EntityData>)} param0 The data of an entity flow element.
+ */
+export const AttributeRFPill = React.memo(
+  ({ id, data }: { id: string; data: any }) => {
+    const theme = useTheme();
+    const dispatch = useAppDispatch();
+    const [value, setValue] = useState(data?.value || '');
+
+    const onChange = (e: any) => {
+      setValue(e.target.value);
+    };
+    const validateInput = () => {
+      const newValue = CheckDatatypeConstraint(data.datatype, value);
+      setValue(newValue);
+      dispatch(updateQBAttributeValue({ id, value: newValue }));
+    };
+
+    // Calculates the size of the input
+    const getInputWidth = () => {
+      if (value == '') return 1;
+      else if (value.length > 10) return 10;
+      return value.length;
+    };
+
+    const boolOperators = useMemo(
+      () => GetAttributeBoolOperators(data?.datatype),
+      [data?.datatype]
+    );
+
+    // Determine the backgroundcolor based on if the attribute is connected to a entity or relation
+    let bgcolor;
+    if (data?.attributeOfA == 'entity')
+      bgcolor = theme.palette.queryBuilder.entity.lighterbg;
+    else if (data?.attributeOfA == 'relation')
+      bgcolor = theme.palette.queryBuilder.relation.lighterbg;
+    else bgcolor = theme.palette.queryBuilder.attribute.background;
+
+    return (
+      <div
+        className={styles.attribute}
+        style={{
+          background: bgcolor,
+          color: theme.palette.queryBuilder.text,
+        }}
+      >
+        {/* <Handle
+        id={Handles.Attribute}
+        type="source"
+        position={Position.Bottom}
+        className={styles.handle}
+      /> */}
+        <div className={styles.contentWrapper}>
+          <span className={styles.content} title={data.name}>
+            {data.name}
+          </span>
+          <AttributeOperatorSelect
+            selected={data?.operator}
+            options={boolOperators}
+            changed={(o) =>
+              dispatch(updateQBAttributeOperator({ id, operator: o.value }))
+            }
+          />
+          <span className={styles.attributeInput}>
+            <input
+              style={{ maxWidth: `${getInputWidth()}ch` }}
+              type="string"
+              placeholder={'?'}
+              value={value}
+              onChange={onChange}
+              onBlur={validateInput}
+              onKeyDown={(e) => e.key == 'Enter' && validateInput()}
+            ></input>
+          </span>
+        </div>
+      </div>
+    );
+  }
+);
+
+export default AttributeRFPill;
diff --git a/libs/shared/ui/pills/src/customFlowPills/attributepill/operatorselect.module.scss b/libs/shared/ui/pills/src/customFlowPills/attributepill/operatorselect.module.scss
new file mode 100644
index 000000000..1c31d2744
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/attributepill/operatorselect.module.scss
@@ -0,0 +1,70 @@
+@use './variables.module.scss';
+
+.container {
+  position: relative;
+  vertical-align: baseline;
+  margin: 0 1ch;
+  font-weight: normal;
+  font-size: 7px;
+}
+
+.valueContainer {
+  color: #6a6a6a;
+  border: 1px solid rgba(0, 0, 0, 0);
+  border-radius: 2px;
+  background-color: transparent;
+
+  transition: border-color 0.2s;
+
+  height: variables.$height;
+  align-items: center;
+  display: flex;
+  padding: 0 1px 1px 1px;
+
+  &.highlighted,
+  &:hover {
+    border-color: rgba(0, 0, 0, 0.4);
+  }
+}
+
+.listbox {
+  font-size: 10px;
+  box-sizing: border-box;
+  padding: 5px;
+  margin: 5px 0 0 0;
+  list-style: none;
+  position: absolute;
+  height: auto;
+  box-shadow: 0 5px 13px -3px #e0e3e7;
+  background: white;
+  border: 1px solid #cdd2d7;
+  border-radius: 0.75em;
+  color: #1a2027;
+  overflow: auto;
+  z-index: 1;
+  outline: 0px;
+  left: -8px;
+
+  &.hidden {
+    opacity: 0;
+    visibility: hidden;
+    transition: opacity 0.4s 0.1s ease, visibility 0.4s 0.1s step-end;
+  }
+
+  & > li {
+    padding: 1px 4px;
+    border-radius: 2px;
+
+    &.selected {
+      background: #f1f1f1;
+    }
+
+    &:hover {
+      background: #e7ebf0;
+    }
+
+    &[aria-selected='true'] {
+      background: #e0e3e7;
+    }
+  }
+}
diff --git a/libs/shared/ui/pills/src/customFlowPills/attributepill/operatorselect.tsx b/libs/shared/ui/pills/src/customFlowPills/attributepill/operatorselect.tsx
new file mode 100644
index 000000000..470c87618
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/attributepill/operatorselect.tsx
@@ -0,0 +1,85 @@
+import * as React from 'react';
+import { SelectOption } from '@mui/base';
+import styles from './operatorselect.module.scss';
+import { useRef, useState } from 'react';
+
+// const grey = {
+//   100: '#E7EBF0',
+//   200: '#E0E3E7',
+//   300: '#CDD2D7',
+//   400: '#B2BAC2',
+//   500: '#A0AAB4',
+//   600: '#6F7E8C',
+//   700: '#3E5060',
+//   800: '#2D3843',
+//   900: '#1A2027',
+// };
+
+interface Props {
+  options: SelectOption<string>[];
+  selected: string;
+  changed?: (newSelected: SelectOption<string>) => void;
+}
+
+function AttributeOperatorSelect({
+  options,
+  selected,
+  changed = () => {},
+}: Props) {
+  const listboxRef = useRef<HTMLUListElement>(null);
+  const [listboxVisible, setListboxVisible] = useState(false);
+  const [currSelected, setCurrSelected] = useState(
+    options.find((o) => o.value == selected)?.label || options[0].label
+  );
+
+  React.useEffect(() => {
+    if (listboxVisible) {
+      listboxRef.current?.focus();
+    }
+  }, [listboxVisible]);
+
+  const changeSelected = (option: SelectOption<string>) => {
+    if (option.label != currSelected) {
+      setCurrSelected(option.label);
+      changed(option);
+    }
+  };
+
+  return (
+    <div
+      className={styles.container}
+      // onMouseOver={() => setListboxVisible(true)}
+      onMouseOut={() => setListboxVisible(false)}
+      onClick={() => setListboxVisible(true)}
+      onFocus={() => setListboxVisible(true)}
+      onBlur={() => setListboxVisible(false)}
+    >
+      <div
+        className={
+          styles.valueContainer + ' ' + (listboxVisible && styles.highlighted)
+        }
+      >
+        <span>{currSelected}</span>
+      </div>
+      {options.length > 1 && (
+        <ul
+          className={styles.listbox + ' ' + (!listboxVisible && styles.hidden)}
+          ref={listboxRef}
+          onMouseOver={() => setListboxVisible(true)}
+        >
+          {options.map((option) => (
+            <li
+              className={option.label == currSelected ? styles.selected : ''}
+              key={option.value}
+              onClick={() => changeSelected(option)}
+            >
+              {option.label}
+            </li>
+          ))}
+        </ul>
+      )}
+    </div>
+  );
+}
+
+export default AttributeOperatorSelect;
diff --git a/libs/shared/ui/pills/src/customFlowPills/attributepill/variables.module.scss b/libs/shared/ui/pills/src/customFlowPills/attributepill/variables.module.scss
new file mode 100644
index 000000000..08bc31bb6
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/attributepill/variables.module.scss
@@ -0,0 +1,3 @@
+$height: 5px;
+$fontsize: 6px;
+$ypad: 2px;
diff --git a/libs/shared/ui/pills/src/customFlowPills/entitypill/entitypill.module.scss b/libs/shared/ui/pills/src/customFlowPills/entitypill/entitypill.module.scss
new file mode 100644
index 000000000..755d2b41d
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/entitypill/entitypill.module.scss
@@ -0,0 +1,49 @@
+.entity {
+  display: flex;
+  font-family: monospace;
+  font-weight: bold;
+  font-size: 10px;
+  padding: 4px 2ch;
+  border-radius: 3px;
+}
+
+.highlighted {
+  box-shadow: black 0 0 2px;
+}
+
+.handleLeft {
+  border: 0px;
+  border-radius: 0px;
+  left: 12px;
+  width: 7px;
+  height: 7px;
+  margin-bottom: 11px;
+  background: rgba(255, 255, 255, 0.6);
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
+  transform-origin: center;
+}
+
+// .handleBottom {
+//   border: 0px;
+//   border-radius: 0px;
+//   width: 7px;
+//   height: 7px;
+//   left: 27.5px;
+//   margin-bottom: 11px;
+//   background: rgba(255, 255, 255, 0.6);
+//   box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
+//   transform: rotate(-45deg);
+//   transform-origin: center;
+// }
+
+.contentWrapper {
+  margin-left: 3ch;
+
+  span {
+    max-width: 20ch;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+    display: block;
+  }
+}
diff --git a/libs/shared/ui/pills/src/customFlowPills/entitypill/entitypill.tsx b/libs/shared/ui/pills/src/customFlowPills/entitypill/entitypill.tsx
new file mode 100644
index 000000000..c6ce0d86f
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/entitypill/entitypill.tsx
@@ -0,0 +1,64 @@
+import { handles } from '@graphpolaris/querybuilder/usecases';
+import { useTheme } from '@mui/material';
+import React, { useEffect } from 'react';
+import { FlowElement, Handle, Position } from 'react-flow-renderer';
+import styles from './entitypill.module.scss';
+import cn from 'classnames';
+
+// export const Handless = {
+//   entity: {
+//     attributes: 'attributesHandle',
+//     relations: 'relationsHandle',
+//   },
+// };
+
+// /** Links need handles to what they are connected to (and which side) */
+// export enum Handles {
+//   RelationLeft = 'leftEntityHandle', //target
+//   RelationRight = 'rightEntityHandle', //target
+//   ToAttributeHandle = 'attributesHandle', //target
+//   ToRelation = 'relationsHandle', //source
+//   Attribute = 'AttributeHandle', //source
+//   ReceiveFunction = 'receiveFunctionHandle', //target
+//   FunctionBase = 'functionHandle_', // + name from FunctionTypes args //source
+// }
+
+/**
+ * Component to render an entity flow element
+ * @param {FlowElement<EntityData>)} param0 The data of an entity flow element.
+ */
+export const EntityRFPill = React.memo(({ data }: { data: any }) => {
+  const theme = useTheme();
+
+  // console.log('EntityRFPill', data);
+  return (
+    <div
+      className={cn(styles['entity'], {
+        [styles['highlighted']]: data.suggestedForConnection,
+      })}
+      style={{
+        background: theme.palette.queryBuilder.entity.background,
+        color: theme.palette.queryBuilder.text,
+      }}
+    >
+      <Handle
+        id={handles.entity.relation}
+        type="source"
+        position={Position.Bottom}
+        className={styles['handleLeft']}
+        style={data?.isConnected ? { backgroundColor: '#2e2e2e' } : {}}
+      />
+      {/* <Handle
+        id={Handles.ToAttributeHandle}
+        type="target"
+        position={Position.Bottom}
+        className={styles.handleBottom}
+      /> */}
+      <div className={styles['contentWrapper']}>
+        <span title={data.name}>{data.name}</span>
+      </div>
+    </div>
+  );
+});
+
+export default EntityRFPill;
diff --git a/libs/shared/ui/pills/src/customFlowPills/entitypill/index.ts b/libs/shared/ui/pills/src/customFlowPills/entitypill/index.ts
new file mode 100644
index 000000000..8f909017f
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/entitypill/index.ts
@@ -0,0 +1 @@
+export * from './entitypill';
diff --git a/libs/shared/ui/pills/src/customFlowPills/relationpill/index.ts b/libs/shared/ui/pills/src/customFlowPills/relationpill/index.ts
new file mode 100644
index 000000000..deef2e611
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/relationpill/index.ts
@@ -0,0 +1 @@
+export * from './relationpill';
diff --git a/libs/shared/ui/pills/src/customFlowPills/relationpill/relationpill.module.scss b/libs/shared/ui/pills/src/customFlowPills/relationpill/relationpill.module.scss
new file mode 100644
index 000000000..aff84b2e5
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/relationpill/relationpill.module.scss
@@ -0,0 +1,110 @@
+.relation {
+  display: flex;
+  text-align: center;
+  font-family: monospace;
+  font-weight: bold;
+  font-size: 10px;
+  background-color: transparent;
+}
+
+.highlighted {
+  box-shadow: black 0 0 2px;
+}
+
+.contentWrapper {
+  display: flex;
+  align-items: center;
+
+  .handleLeft {
+    position: relative;
+    z-index: 3;
+
+    top: 25%;
+    border: 0px;
+    border-radius: 0px;
+
+    background: transparent;
+    transform-origin: center;
+
+    width: 0;
+    height: 0;
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+    border-right: rgba(255, 255, 255, 0.7) 6px solid;
+
+    &::after {
+      content: '';
+      display: block;
+      position: absolute;
+      width: 0;
+      height: 0;
+      border-top: 7px solid transparent;
+      border-bottom: 7px solid transparent;
+      border-right: rgba(0, 0, 0, 0.1) 8px solid;
+      top: -7px;
+      right: -7px;
+    }
+  }
+  .highlighted {
+    z-index: -1;
+    box-shadow: 0 0 2px 1px gray;
+  }
+
+  .content {
+    margin: 0 2ch;
+    padding: 3px 0;
+    max-width: 20ch;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+  }
+
+  .handleRight {
+    position: relative;
+    top: 25%;
+    border: 0px;
+    border-radius: 0px;
+
+    background: transparent;
+    transform-origin: center;
+
+    width: 0;
+    height: 0;
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+    border-left: rgba(255, 255, 255, 0.7) 6px solid;
+
+    &::after {
+      content: '';
+      display: block;
+      position: absolute;
+      width: 0;
+      height: 0;
+      border-top: 7px solid transparent;
+      border-bottom: 7px solid transparent;
+      border-left: rgba(0, 0, 0, 0.1) 8px solid;
+      top: -7px;
+      left: -7px;
+    }
+  }
+}
+
+$height: 10px;
+.arrowLeft {
+  z-index: 2;
+  width: 0;
+  height: 0;
+  border-top: $height solid transparent;
+  border-bottom: $height solid transparent;
+
+  border-right: $height solid;
+}
+
+.arrowRight {
+  width: 0;
+  height: 0;
+  border-top: $height solid transparent;
+  border-bottom: $height solid transparent;
+
+  border-left: $height solid;
+}
diff --git a/libs/shared/ui/pills/src/customFlowPills/relationpill/relationpill.tsx b/libs/shared/ui/pills/src/customFlowPills/relationpill/relationpill.tsx
new file mode 100644
index 000000000..8861d5e0e
--- /dev/null
+++ b/libs/shared/ui/pills/src/customFlowPills/relationpill/relationpill.tsx
@@ -0,0 +1,98 @@
+import { handles } from '@graphpolaris/querybuilder/usecases';
+import { useTheme } from '@mui/material';
+import { Handle, Position } from 'react-flow-renderer';
+import cn from 'classnames';
+
+import styles from './relationpill.module.scss';
+import React from 'react';
+
+/**
+ * Component to render a relation flow element
+ * @param { FlowElement<RelationData>} param0 The data of a relation flow element.
+ */
+export const RelationRFPill = React.memo(({ data }: { data: any }) => {
+  // export default function RelationRFPill({ data }: { data: any }) {
+  const theme = useTheme();
+
+  // const minRef = useRef<HTMLInputElement>(null);
+  // const maxRef = useRef<HTMLInputElement>(null);
+
+  // const [readOnlyMin, setReadOnlyMin] = useState(true);
+  // const [readOnlyMax, setReadOnlyMax] = useState(true);
+
+  // const onDepthChanged = (depth: string) => {
+  //   // Don't allow depth above 99
+  //   const limit = 99;
+  //   if (data != undefined) {
+  //     data.depth.min = data.depth.min >= limit ? limit : data.depth.min;
+  //     data.depth.max = data.depth.max >= limit ? limit : data.depth.max;
+
+  //     // Check for for valid depth: min <= max
+  //     if (depth == 'min') {
+  //       if (data.depth.min > data.depth.max) data.depth.max = data.depth.min;
+  //       setReadOnlyMin(true);
+  //     } else if (depth == 'max') {
+  //       if (data.depth.max < data.depth.min) data.depth.min = data.depth.max;
+  //       setReadOnlyMax(true);
+  //     }
+
+  //     // Set to the correct width
+  //     if (maxRef.current)
+  //       maxRef.current.style.maxWidth = calcWidth(data.depth.max);
+  //     if (minRef.current)
+  //       minRef.current.style.maxWidth = calcWidth(data.depth.min);
+  //   }
+  // };
+
+  // const calcWidth = (data: number) => {
+  //   return data.toString().length + 0.5 + 'ch';
+  // };
+
+  return (
+    <div className={styles['relation']}>
+      <div
+        className={styles['arrowLeft']}
+        style={{
+          borderRightColor: theme.palette.queryBuilder.relation.background,
+        }}
+      />
+      <div
+        className={cn(styles['contentWrapper'], {
+          [styles['highlighted']]: data.suggestedForConnection,
+        })}
+        style={{
+          color: theme.palette.queryBuilder.text,
+          background: theme.palette.queryBuilder.relation.background,
+        }}
+      >
+        <Handle
+          id={handles.relation.fromEntity}
+          type="target"
+          position={Position.Left}
+          className={styles['handleLeft']}
+          style={
+            data?.isFromEntityConnected ? { borderRightColor: '#2e2e2e' } : {} // TODO: this should be color from theme
+          }
+        />
+        <span className={styles['content']} title={data.name}>
+          {data.name}
+        </span>
+        <Handle
+          id={handles.relation.toEntity}
+          type="source"
+          position={Position.Right}
+          className={styles['handleRight']}
+          style={
+            data?.isToEntityConnected ? { borderLeftColor: '#2e2e2e' } : {}
+          }
+        />
+      </div>
+      <div
+        className={styles['arrowRight']}
+        style={{
+          borderLeftColor: theme.palette.queryBuilder.relation.background,
+        }}
+      />
+    </div>
+  );
+});
diff --git a/libs/shared/ui/pills/src/index.ts b/libs/shared/ui/pills/src/index.ts
new file mode 100644
index 000000000..6ac42e28d
--- /dev/null
+++ b/libs/shared/ui/pills/src/index.ts
@@ -0,0 +1,9 @@
+export * from './lib/shared-ui-pills';
+
+export * from './customFlowLines/connection';
+export * from './customFlowLines/connectionDrag';
+
+export * from './customFlowPills/entitypill/entitypill';
+export * from './customFlowPills/attributepill/attributepill';
+export * from './customFlowPills/attributepill/operatorselect';
+export * from './customFlowPills/relationpill/relationpill';
diff --git a/libs/shared/ui/pills/src/lib/shared-ui-pills.module.scss b/libs/shared/ui/pills/src/lib/shared-ui-pills.module.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/libs/shared/ui/pills/src/lib/shared-ui-pills.spec.tsx b/libs/shared/ui/pills/src/lib/shared-ui-pills.spec.tsx
new file mode 100644
index 000000000..d9561ecbd
--- /dev/null
+++ b/libs/shared/ui/pills/src/lib/shared-ui-pills.spec.tsx
@@ -0,0 +1,10 @@
+import { render } from '@testing-library/react';
+
+import SharedUiPills from './shared-ui-pills';
+
+describe('SharedUiPills', () => {
+  it('should render successfully', () => {
+    const { baseElement } = render(<SharedUiPills />);
+    expect(baseElement).toBeTruthy();
+  });
+});
diff --git a/libs/shared/ui/pills/src/lib/shared-ui-pills.tsx b/libs/shared/ui/pills/src/lib/shared-ui-pills.tsx
new file mode 100644
index 000000000..6a804d611
--- /dev/null
+++ b/libs/shared/ui/pills/src/lib/shared-ui-pills.tsx
@@ -0,0 +1,14 @@
+import './shared-ui-pills.module.scss';
+
+/* eslint-disable-next-line */
+export interface SharedUiPillsProps {}
+
+export function SharedUiPills(props: SharedUiPillsProps) {
+  return (
+    <div>
+      <h1>Welcome to SharedUiPills!</h1>
+    </div>
+  );
+}
+
+export default SharedUiPills;
diff --git a/libs/shared/ui/pills/src/schema/entitypill.component.tsx b/libs/shared/ui/pills/src/schema/entitypill.component.tsx
new file mode 100644
index 000000000..049328d00
--- /dev/null
+++ b/libs/shared/ui/pills/src/schema/entitypill.component.tsx
@@ -0,0 +1,62 @@
+import { useTheme } from '@mui/material';
+import React, { useEffect } from 'react';
+import { FlowElement, Handle, Position } from 'react-flow-renderer';
+import cn from 'classnames';
+import styles from './entitypill.module.scss';
+
+// export const Handless = {
+//   entity: {
+//     attributes: 'attributesHandle',
+//     relations: 'relationsHandle',
+//   },
+// };
+
+// /** Links need handles to what they are connected to (and which side) */
+// export enum Handles {
+//   RelationLeft = 'leftEntityHandle', //target
+//   RelationRight = 'rightEntityHandle', //target
+//   ToAttributeHandle = 'attributesHandle', //target
+//   ToRelation = 'relationsHandle', //source
+//   Attribute = 'AttributeHandle', //source
+//   ReceiveFunction = 'receiveFunctionHandle', //target
+//   FunctionBase = 'functionHandle_', // + name from FunctionTypes args //source
+// }
+
+/**
+ * Component to render an entity flow element
+ * @param {FlowElement<EntityData>)} param0 The data of an entity flow element.
+ */
+export const EntityRFPill = React.memo(({ data }: { data: any }) => {
+  const theme = useTheme();
+
+  return (
+    <div
+      className={cn(styles['entity'], {
+        [styles['highlighted']]: data.suggestedForConnection,
+      })}
+      style={{
+        background: theme.palette.queryBuilder.entity.background,
+        color: theme.palette.queryBuilder.text,
+      }}
+    >
+      <Handle
+        id={handles.entity.relation}
+        type="source"
+        position={Position.Bottom}
+        className={styles['handleLeft']}
+        style={data?.isConnected ? { backgroundColor: '#2e2e2e' } : {}}
+      />
+      {/* <Handle
+        id={Handles.ToAttributeHandle}
+        type="target"
+        position={Position.Bottom}
+        className={styles.handleBottom}
+      /> */}
+      <div className={styles['contentWrapper']}>
+        <span title={data.name}>{data.name}</span>
+      </div>
+    </div>
+  );
+});
+
+export default EntityRFPill;
diff --git a/libs/shared/ui/pills/src/schema/entitypill.module.scss b/libs/shared/ui/pills/src/schema/entitypill.module.scss
new file mode 100644
index 000000000..755d2b41d
--- /dev/null
+++ b/libs/shared/ui/pills/src/schema/entitypill.module.scss
@@ -0,0 +1,49 @@
+.entity {
+  display: flex;
+  font-family: monospace;
+  font-weight: bold;
+  font-size: 10px;
+  padding: 4px 2ch;
+  border-radius: 3px;
+}
+
+.highlighted {
+  box-shadow: black 0 0 2px;
+}
+
+.handleLeft {
+  border: 0px;
+  border-radius: 0px;
+  left: 12px;
+  width: 7px;
+  height: 7px;
+  margin-bottom: 11px;
+  background: rgba(255, 255, 255, 0.6);
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
+  transform-origin: center;
+}
+
+// .handleBottom {
+//   border: 0px;
+//   border-radius: 0px;
+//   width: 7px;
+//   height: 7px;
+//   left: 27.5px;
+//   margin-bottom: 11px;
+//   background: rgba(255, 255, 255, 0.6);
+//   box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
+//   transform: rotate(-45deg);
+//   transform-origin: center;
+// }
+
+.contentWrapper {
+  margin-left: 3ch;
+
+  span {
+    max-width: 20ch;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+    display: block;
+  }
+}
diff --git a/libs/shared/ui/pills/src/schema/index.ts b/libs/shared/ui/pills/src/schema/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/libs/shared/ui/pills/tsconfig.json b/libs/shared/ui/pills/tsconfig.json
new file mode 100644
index 000000000..3512bf7af
--- /dev/null
+++ b/libs/shared/ui/pills/tsconfig.json
@@ -0,0 +1,25 @@
+{
+  "extends": "../../../../tsconfig.base.json",
+  "compilerOptions": {
+    "jsx": "react-jsx",
+    "allowJs": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "noImplicitOverride": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "files": [],
+  "include": [],
+  "references": [
+    {
+      "path": "./tsconfig.lib.json"
+    },
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}
diff --git a/libs/shared/ui/pills/tsconfig.lib.json b/libs/shared/ui/pills/tsconfig.lib.json
new file mode 100644
index 000000000..1ab8e06a7
--- /dev/null
+++ b/libs/shared/ui/pills/tsconfig.lib.json
@@ -0,0 +1,22 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../../../dist/out-tsc",
+    "types": ["node"]
+  },
+  "files": [
+    "../../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
+    "../../../../node_modules/@nrwl/react/typings/image.d.ts"
+  ],
+  "exclude": [
+    "**/*.spec.ts",
+    "**/*.test.ts",
+    "**/*.spec.tsx",
+    "**/*.test.tsx",
+    "**/*.spec.js",
+    "**/*.test.js",
+    "**/*.spec.jsx",
+    "**/*.test.jsx"
+  ],
+  "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
+}
diff --git a/libs/shared/ui/pills/tsconfig.spec.json b/libs/shared/ui/pills/tsconfig.spec.json
new file mode 100644
index 000000000..315a5b0bb
--- /dev/null
+++ b/libs/shared/ui/pills/tsconfig.spec.json
@@ -0,0 +1,19 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../../../dist/out-tsc",
+    "module": "commonjs",
+    "types": ["jest", "node"]
+  },
+  "include": [
+    "**/*.test.ts",
+    "**/*.spec.ts",
+    "**/*.test.tsx",
+    "**/*.spec.tsx",
+    "**/*.test.js",
+    "**/*.spec.js",
+    "**/*.test.jsx",
+    "**/*.spec.jsx",
+    "**/*.d.ts"
+  ]
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index a5bafb69c..1d989caf1 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -17,11 +17,11 @@
     "paths": {
       "@graphpolaris/graph-layout": ["libs/shared/graph-layout/src/index.ts"],
       "@graphpolaris/models": ["libs/shared/models/src/index.ts"],
-      "@graphpolaris/schema-utils": ["libs/shared/schema-utils/src/index.ts"],
-      "@graphpolaris/schema/usecases": ["libs/schema/usecases/src/index.ts"],
       "@graphpolaris/querybuilder/usecases": [
         "libs/querybuilder/usecases/src/index.ts"
       ],
+      "@graphpolaris/schema-utils": ["libs/shared/schema-utils/src/index.ts"],
+      "@graphpolaris/schema/usecases": ["libs/schema/usecases/src/index.ts"],
       "@graphpolaris/shared/data-access/api": [
         "libs/shared/data-access/api/src/index.ts"
       ],
@@ -35,6 +35,7 @@
         "libs/shared/data-access/theme/src/index.ts"
       ],
       "@graphpolaris/shared/mock-data": ["libs/shared/mock-data/src/index.ts"],
+      "@graphpolaris/shared/ui/pills": ["libs/shared/ui/pills/src/index.ts"],
       "@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"]
     }
   },
diff --git a/workspace.json b/workspace.json
index d829d3ca0..d5bbe8a52 100644
--- a/workspace.json
+++ b/workspace.json
@@ -11,6 +11,7 @@
     "shared-mock-data": "libs/shared/mock-data",
     "shared-models": "libs/shared/models",
     "shared-schema-utils": "libs/shared/schema-utils",
+    "shared-ui-pills": "libs/shared/ui/pills",
     "web-graphpolaris": "apps/web-graphpolaris",
     "web-graphpolaris-e2e": "apps/web-graphpolaris-e2e"
   }
-- 
GitLab