From beb01ffd19d87f61813d75e67ab6d1bede307a21 Mon Sep 17 00:00:00 2001
From: Milho001 <l.milhomemfrancochristino@uu.nl>
Date: Tue, 7 May 2024 18:35:22 +0000
Subject: [PATCH] fix(qb): save active attributes from dropdown and fixes to QB
 drag and hover

---
 .../DatabaseManagement/forms/settings.tsx     |  22 ++-
 libs/shared/lib/components/buttons/index.tsx  |   4 +-
 libs/shared/lib/data-access/api/eventBus.tsx  |  11 +-
 .../shared/lib/data-access/broker/wsState.tsx |   1 +
 libs/shared/lib/data-access/store/hooks.ts    |   4 +-
 .../data-access/store/querybuilderSlice.ts    |  21 ++-
 .../lib/querybuilder/panel/QueryBuilder.tsx   |   6 -
 .../entitypill/QueryEntityPill.tsx            |  32 +++-
 .../pills/pilldropdown/PillDropdown.tsx       | 154 ++++++++++--------
 9 files changed, 165 insertions(+), 90 deletions(-)

diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
index f9bb74087..bf84e5acf 100644
--- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
+++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
@@ -6,6 +6,7 @@ import {
   wsTestDatabaseConnection,
   wsCreateState,
   useAuthorizationCache,
+  nilUUID,
 } from '@graphpolaris/shared/lib/data-access';
 import { ErrorOutline } from '@mui/icons-material';
 import { Dialog } from '@graphpolaris/shared/lib/components/layout';
@@ -46,7 +47,7 @@ export const SettingsForm = (props: { onClose(): void; open: 'add' | 'update'; s
     }
   }, [props.saveState]);
 
-  async function handleSubmit(saveStateData?: SaveStateI) {
+  async function handleSubmit(saveStateData?: SaveStateI, forceAdd: boolean = false): Promise<void> {
     if (!saveStateData) saveStateData = formData;
     setConnection(() => ({
       updating: true,
@@ -69,7 +70,7 @@ export const SettingsForm = (props: { onClose(): void; open: 'add' | 'update'; s
           status: 'Database connection verified',
           verified: true,
         }));
-        if (props.open === 'add') {
+        if (props.open === 'add' || forceAdd) {
           wsCreateState(saveStateData, (_data) => {
             dispatch(addSaveState(_data));
             dispatch(testedSaveState(_data.id));
@@ -159,11 +160,10 @@ export const SettingsForm = (props: { onClose(): void; open: 'add' | 'update'; s
           </div>
         )}
 
-        <div
-          className={`grid md:grid-cols-2 gap-3 card-actions w-full justify-stretch items-center ${sampleDataPanel === true && 'hidden'}`}
-        >
+        <div className={`flex flex-row  gap-3 card-actions w-full justify-stretch items-center ${sampleDataPanel === true && 'hidden'}`}>
           <Button
             type="primary"
+            className="flex-grow"
             label={connection.updating ? formTitle.slice(0, -1) + 'ing...' : formTitle}
             onClick={(event) => {
               event.preventDefault();
@@ -171,8 +171,20 @@ export const SettingsForm = (props: { onClose(): void; open: 'add' | 'update'; s
             }}
             disabled={connection.updating || hasError}
           />
+          {props.open === 'update' && (
+            <Button
+              type="secondary"
+              className="flex-grow"
+              label={'Clone'}
+              onClick={(event) => {
+                handleSubmit({ ...formData, name: formData.name + ' (copy)', id: nilUUID }, true);
+              }}
+              disabled={connection.updating || hasError}
+            />
+          )}
           <Button
             variant="outline"
+            className="flex-grow"
             label="Cancel"
             disabled={props.disableCancel}
             onClick={(event) => {
diff --git a/libs/shared/lib/components/buttons/index.tsx b/libs/shared/lib/components/buttons/index.tsx
index 87cc57e16..2cfdfea12 100644
--- a/libs/shared/lib/components/buttons/index.tsx
+++ b/libs/shared/lib/components/buttons/index.tsx
@@ -114,6 +114,8 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps & React.HT
       [iconComponent, label, children],
     );
 
+    additionalClasses = (additionalClasses || '') + (props.className || '');
+
     if (notAButton)
       return (
         <div
@@ -124,12 +126,12 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps & React.HT
       );
     return (
       <button
-        className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass} ${additionalClasses}`}
         onClick={onClick}
         disabled={disabled}
         aria-label={ariaLabel}
         ref={forwardRef}
         {...props}
+        className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass} ${additionalClasses}`}
       >
         {iconPosition === 'leading' && icon}
         {label && <span>{label}</span>}
diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx
index dfb139648..f49d266e0 100644
--- a/libs/shared/lib/data-access/api/eventBus.tsx
+++ b/libs/shared/lib/data-access/api/eventBus.tsx
@@ -12,11 +12,17 @@ import {
   useVisualization,
   wsSchemaRequest,
   wsSchemaSubscription,
+  useQuerybuilderAttributesShown,
 } from '@graphpolaris/shared/lib/data-access';
 import { Broker, wsQuerySubscription, wsQueryTranslationSubscription } from '@graphpolaris/shared/lib/data-access/broker';
 import { addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice';
 import { allMLTypes, LinkPredictionInstance, setMLResult } from '@graphpolaris/shared/lib/data-access/store/mlSlice';
-import { QueryBuilderText, setQueryText, setQuerybuilderNodes } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
+import {
+  QueryBuilderText,
+  attributeShownToggle,
+  setQueryText,
+  setQuerybuilderNodes,
+} from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 import { useEffect } from 'react';
 import {
   SaveStateI,
@@ -52,6 +58,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
   const session = useSessionCache();
   const queryHash = useQuerybuilderHash();
   const queryBuilder = useQuerybuilder();
+  const attributeShown = useQuerybuilderAttributesShown();
   const mlHash = useMLEnabledHash();
   const visState = useVisualization();
   const queryBuilderSettings = useQuerybuilderSettings();
@@ -171,7 +178,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
         wsUpdateState(state);
       }
     }
-  }, [queryBuilderSettings, queryHash]);
+  }, [queryBuilderSettings, queryHash, attributeShown]);
 
   useEffect(() => {
     if (session.currentSaveState) {
diff --git a/libs/shared/lib/data-access/broker/wsState.tsx b/libs/shared/lib/data-access/broker/wsState.tsx
index 8ad7ca2df..ce016f28d 100644
--- a/libs/shared/lib/data-access/broker/wsState.tsx
+++ b/libs/shared/lib/data-access/broker/wsState.tsx
@@ -146,6 +146,7 @@ export function wsTestSaveStateConnectionSubscription(callback: TestSaveStateCon
 }
 
 export function wsUpdateState(request: SaveStateI, callback?: GetStateResponse) {
+  console.log('wsUpdateState', request);
   Broker.instance().sendMessage(
     {
       key: 'state',
diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts
index bcdce1c93..729be1655 100644
--- a/libs/shared/lib/data-access/store/hooks.ts
+++ b/libs/shared/lib/data-access/store/hooks.ts
@@ -6,6 +6,7 @@ import { ConfigStateI, configState } from '@graphpolaris/shared/lib/data-access/
 import {
   QueryBuilderSettings,
   QueryBuilderState,
+  queryBuilderAttributesShown,
   queryBuilderSettingsState,
   queryBuilderState,
   selectQuerybuilderGraph,
@@ -25,7 +26,7 @@ import {
   CategoryDataI,
 } from './searchResultSlice';
 import { AllLayoutAlgorithms } from '../../graph-layout';
-import { QueryMultiGraph } from '../../querybuilder';
+import { QueryGraphEdgeHandle, QueryMultiGraph } from '../../querybuilder';
 import { SchemaGraph } from '../../schema';
 import { GraphMetadata } from '../statistics';
 
@@ -48,6 +49,7 @@ export const useQuerybuilderGraph: () => QueryMultiGraph = () => useAppSelector(
 export const useQuerybuilderHash: () => string = () => useAppSelector(selectQuerybuilderHash);
 export const useQuerybuilderSettings: () => QueryBuilderSettings = () => useAppSelector(queryBuilderSettingsState);
 export const useQuerybuilder: () => QueryBuilderState = () => useAppSelector(queryBuilderState);
+export const useQuerybuilderAttributesShown: () => QueryGraphEdgeHandle[] = () => useAppSelector(queryBuilderAttributesShown);
 
 // Overall Configuration of the app
 export const useConfig: () => ConfigStateI = () => useAppSelector(configState);
diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts
index e9e6fc3ab..da7dbe228 100644
--- a/libs/shared/lib/data-access/store/querybuilderSlice.ts
+++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts
@@ -5,6 +5,9 @@ import Graph, { MultiGraph } from 'graphology';
 import { Attributes, SerializedGraph } from 'graphology-types';
 import { QueryMultiGraph, QueryMultiGraphology as QueryGraphology } from '../../querybuilder/model/graphology/utils';
 import { AllLayoutAlgorithms } from '../../graph-layout';
+import { QueryGraphEdgeHandle } from '../../querybuilder';
+import { isEqual } from 'lodash-es';
+import { settings } from 'pixi.js';
 
 const defaultGraph = () => ({ nodes: [], edges: [], attributes: {}, options: {} });
 
@@ -20,11 +23,14 @@ export type QueryBuilderText = {
   result: string;
 };
 
+export type QueryBuilderAttributeBeingShown = {};
+
 export type QueryBuilderState = {
   graph: QueryMultiGraph;
   ignoreReactivity: boolean;
   settings: QueryBuilderSettings;
   queryTranslation: QueryBuilderText;
+  attributesBeingShown: QueryGraphEdgeHandle[];
 };
 
 // Define the initial state using that type
@@ -41,6 +47,7 @@ export const initialState: QueryBuilderState = {
     queryId: '',
     result: '',
   },
+  attributesBeingShown: [],
   // schemaLayout: 'Graphology_noverlap',
 };
 
@@ -58,6 +65,7 @@ export const querybuilderSlice = createSlice({
       if (action.payload.graph?.nodes && action.payload.graph?.edges) {
         state.graph = action.payload.graph;
         state.settings = action.payload.settings;
+        state.attributesBeingShown = action.payload.attributesBeingShown || [];
         // state.ignoreReactivity = true;
       }
     },
@@ -70,6 +78,14 @@ export const querybuilderSlice = createSlice({
     setQueryText: (state: QueryBuilderState, action: PayloadAction<QueryBuilderText>) => {
       state.queryTranslation = action.payload;
     },
+    attributeShownToggle: (state: QueryBuilderState, action: PayloadAction<QueryGraphEdgeHandle>) => {
+      const existing = state.attributesBeingShown.findIndex((a) => isEqual(a, action.payload));
+      if (existing === -1) {
+        state.attributesBeingShown.push(action.payload);
+      } else {
+        state.attributesBeingShown.splice(existing, 1);
+      }
+    },
   },
 });
 
@@ -127,4 +143,7 @@ export const selectQuerybuilderHash = (state: RootState): string => {
 //   state.schema.schemaLayout;
 
 export default querybuilderSlice.reducer;
-export const { setQuerybuilderGraph, clearQB, setQuerybuilderSettings, setQuerybuilderNodes, setQueryText } = querybuilderSlice.actions;
+export const { setQuerybuilderGraph, clearQB, setQuerybuilderSettings, setQuerybuilderNodes, setQueryText, attributeShownToggle } =
+  querybuilderSlice.actions;
+
+export const queryBuilderAttributesShown = (state: RootState) => state.querybuilder.attributesBeingShown;
diff --git a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx
index 526868395..6fca5ec41 100644
--- a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx
+++ b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx
@@ -347,12 +347,6 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
         if ('touches' in event) clientY = event?.touches?.[0]?.clientY;
         else if ('clientY' in event) clientY = event?.clientY;
 
-        if (reactFlowWrapper.current) {
-          const { top, left } = reactFlowWrapper.current.getBoundingClientRect();
-          clientX -= left;
-          clientY -= top;
-        }
-
         const position = reactFlow.screenToFlowPosition({ x: clientX, y: clientY });
         if (connectingNodeId?.current) connectingNodeId.current.position = position;
 
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx
index 0f842fa97..32d00d60c 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx
@@ -1,5 +1,5 @@
 import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access';
-import React, { useMemo, useState } from 'react';
+import React, { useMemo, useRef, useState } from 'react';
 import { Handle, Position, useUpdateNodeInternals } from 'reactflow';
 import { NodeAttribute, SchemaReactflowEntityNode, toHandleId } from '../../../model';
 import { PillDropdown } from '../../pilldropdown/PillDropdown';
@@ -11,6 +11,8 @@ import { EntityPill } from '@graphpolaris/shared/lib/components';
  */
 export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
   const updateNodeInternals = useUpdateNodeInternals();
+  const ref = useRef<HTMLDivElement | null>(null);
+  const dropdownActive = useRef(false);
 
   const data = node.data;
   if (!data.leftRelationHandleId) throw new Error('EntityFlowElement: data.leftRelationHandleId is undefined');
@@ -23,16 +25,19 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
   );
 
   const [hovered, setHovered] = useState(false);
+  const [dragging, setDragging] = useState(false);
   const [handleBeingDragged, setHandleBeingDragged] = useState(-1);
 
   const onMouseEnter = (event: React.MouseEvent) => {
     if (!hovered) setHovered(true);
+    ref.current?.addEventListener('mousedown', onMouseDown, true);
     setTimeout(() => {
       updateNodeInternals(node.id);
     }, 100);
   };
 
   const onMouseLeave = (event: React.MouseEvent) => {
+    ref.current?.removeEventListener('mousedown', onMouseDown, true);
     if (hovered) setHovered(false);
     setTimeout(() => {
       updateNodeInternals(node.id);
@@ -49,12 +54,31 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
     window.removeEventListener('mouseup', onHandleMouseUp, true);
   };
 
+  const onMouseDown = React.useCallback(() => {
+    window.addEventListener('mouseup', onMouseUp, true);
+    setDragging(true);
+  }, []);
+
+  const onMouseUp = () => {
+    setDragging(false);
+    setHovered(true);
+    window.removeEventListener('mouseup', onMouseUp, true);
+  };
+
   const onConnect = (params: any) => {
     console.log('EntityPill onConnect', params);
   };
 
+  const onMouseEnterDropdown = (event: React.MouseEvent) => {
+    ref.current?.removeEventListener('mousedown', onMouseDown, true);
+  };
+
+  const onMouseLeaveDropdown = (event: React.MouseEvent) => {
+    ref.current?.addEventListener('mousedown', onMouseDown, true);
+  };
+
   return (
-    <div className="w-fit h-fit nowheel" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
+    <div className="w-fit h-fit nowheel" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} ref={ref} id="asd">
       <EntityPill
         title={data.name || ''}
         withHandles="horizontal"
@@ -78,10 +102,12 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
       >
         {data?.attributes && (
           <PillDropdown
+            onMouseEnterDropdown={onMouseEnterDropdown}
+            onMouseLeaveDropdown={onMouseLeaveDropdown}
             node={node}
             attributes={data.attributes}
             attributeEdges={attributeEdges.map((edge) => edge?.attributes)}
-            hovered={hovered}
+            hovered={hovered && !dragging}
             handleBeingDraggedIdx={handleBeingDragged}
             onHandleMouseDown={onHandleMouseDown}
           />
diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx
index a5c4e9cb5..fae11e3ea 100644
--- a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx
+++ b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx
@@ -5,7 +5,9 @@ import { Abc, CalendarToday, Map, Numbers, Place, QuestionMarkOutlined } from '@
 import Icon from '@graphpolaris/shared/lib/components/icon';
 import { PillHandle } from '@graphpolaris/shared/lib/components/pills/PillHandle';
 import { pillDropdownPadding } from '@graphpolaris/shared/lib/components/pills/pill.const';
-import { Button, TextInput } from '../../..';
+import { Button, TextInput, useAppDispatch, useQuerybuilderAttributesShown } from '../../..';
+import { attributeShownToggle } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
+import { isEqual } from 'lodash-es';
 
 type PillDropdownProps = {
   node: SchemaReactflowEntityNode;
@@ -14,6 +16,8 @@ type PillDropdownProps = {
   hovered: boolean;
   handleBeingDraggedIdx: number;
   onHandleMouseDown: (attribute: NodeAttribute, i: number, event: React.MouseEvent) => void;
+  onMouseEnterDropdown?: (event: React.MouseEvent) => void;
+  onMouseLeaveDropdown?: (event: React.MouseEvent) => void;
 };
 
 type IconMapType = {
@@ -31,82 +35,90 @@ const IconMap: IconMapType = {
 export const PillDropdown = (props: PillDropdownProps) => {
   const forceOpen = false;
   const [filter, setFilter] = useState<string>('');
+  const dispatch = useAppDispatch();
+  const attributesBeingShown = useQuerybuilderAttributesShown();
 
-  const [attributesOfInterest, setAttributesOfInterest] = useState<Set<number>>(new Set());
+  const attributesOfInterest = useMemo(() => {
+    return props.attributes.map((attribute) =>
+      attributesBeingShown.findIndex((x) => isEqual(x, attribute.handleData)) === -1 ? false : true,
+    );
+  }, [attributesBeingShown]);
 
   return (
-    <>
-      <div className={'border-[1px] border-secondary-200 divide-y divide-secondary-200'}>
-        {attributesOfInterest &&
-          [...attributesOfInterest].map((i) => {
-            const attribute = props.attributes[i];
-            if (attribute.handleData.attributeName === undefined) {
-              throw new Error('attribute.handleData.attributeName is undefined');
-            }
+    <div
+      className={'border-[1px] border-secondary-200 divide-y divide-secondary-200'}
+      onMouseEnter={(e) => {
+        if (props.onMouseEnterDropdown) props.onMouseEnterDropdown(e);
+      }}
+      onMouseLeave={(e) => {
+        if (props.onMouseLeaveDropdown) props.onMouseLeaveDropdown(e);
+      }}
+    >
+      {attributesOfInterest &&
+        attributesOfInterest.map((showing, i) => {
+          if (showing === false) return null;
 
-            return (
-              <div
-                className="px-2 py-1 bg-secondary-100 flex justify-between items-center"
-                key={(attribute.handleData.attributeName || '') + i}
-                onMouseDown={(event: React.MouseEvent) => {
-                  props.onHandleMouseDown(attribute, i, event);
-                }}
+          const attribute = props.attributes[i];
+          if (attribute.handleData.attributeName === undefined) {
+            throw new Error('attribute.handleData.attributeName is undefined');
+          }
+
+          return (
+            <div
+              className="px-2 py-1 bg-secondary-100 flex justify-between items-center"
+              key={(attribute.handleData.attributeName || '') + i}
+              onMouseDown={(event: React.MouseEvent) => {
+                props.onHandleMouseDown(attribute, i, event);
+              }}
+            >
+              <p className="truncate text-[0.6rem]">{attribute.handleData.attributeName}</p>
+              {attribute.handleData?.attributeDimension && <Icon component={IconMap[attribute.handleData.attributeDimension]} size={16} />}
+              <PillHandle
+                mr={-pillDropdownPadding}
+                handleTop="auto"
+                position={Position.Right}
+                className={'fill-accent-500 stroke-white'}
+                type="square"
               >
-                <p className="truncate text-[0.6rem]">{attribute.handleData.attributeName}</p>
-                {attribute.handleData?.attributeDimension && (
-                  <Icon component={IconMap[attribute.handleData.attributeDimension]} size={16} />
-                )}
-                <PillHandle
-                  mr={-pillDropdownPadding}
-                  handleTop="auto"
+                <Handle
+                  id={toHandleId(handleDataFromReactflowToDataId(props.node, attribute))}
+                  type="source"
                   position={Position.Right}
-                  className={'fill-accent-500 stroke-white'}
-                  type="square"
-                >
-                  <Handle
-                    id={toHandleId(handleDataFromReactflowToDataId(props.node, attribute))}
-                    type="source"
-                    position={Position.Right}
-                    className={'!rounded-none !bg-transparent !w-full !h-full !right-0 !left-0 !border-0'}
-                  ></Handle>
-                </PillHandle>
-              </div>
-            );
-          })}
-        {(props.hovered || forceOpen) && (
-          <>
-            <h4 className="p-1 bg-white border-t-[2px] font-semibold text-2xs">Available Attributes:</h4>
-            <TextInput type={'text'} placeholder="Filter" className="!p-0.5" value={filter} onChange={(v) => setFilter(v)} />
-            <div className="max-h-28 overflow-auto flex flex-col bg-white">
-              {props.attributes.map((attribute, i) => {
-                if (filter && !attribute.handleData.attributeName?.toLowerCase().includes(filter.toLowerCase())) return null;
-                if (attribute.handleData.attributeName === undefined) {
-                  throw new Error('attribute.handleData.attributeName is undefined');
-                }
-
-                return (
-                  <Button
-                    key={(attribute.handleData.attributeName || '') + i}
-                    iconComponent={attribute?.handleData?.attributeDimension ? IconMap[attribute.handleData.attributeDimension] : undefined}
-                    iconPosition="trailing"
-                    additionalClasses={`w-full ${attributesOfInterest.has(i) ? 'bg-secondary-100' : 'bg-white'} justify-between rounded-none text-[0.7em] hover:cursor-copy`}
-                    variant="ghost"
-                    size={'xs'}
-                    label={attribute.handleData.attributeName}
-                    onClick={(event: React.MouseEvent) => {
-                      if (attributesOfInterest.has(i)) {
-                        setAttributesOfInterest(new Set([...attributesOfInterest].filter((x) => x !== i)));
-                      } else {
-                        setAttributesOfInterest(new Set([...attributesOfInterest, i]));
-                      }
-                    }}
-                  ></Button>
-                );
-              })}
+                  className={'!rounded-none !bg-transparent !w-full !h-full !right-0 !left-0 !border-0'}
+                ></Handle>
+              </PillHandle>
             </div>
-          </>
-        )}
-      </div>
-    </>
+          );
+        })}
+      {(props.hovered || forceOpen) && (
+        <>
+          <h4 className="p-1 bg-white border-t-[2px] font-semibold text-2xs">Available Attributes:</h4>
+          <TextInput type={'text'} placeholder="Filter" className="!p-0.5" value={filter} onChange={(v) => setFilter(v)} />
+          <div className="max-h-28 overflow-auto flex flex-col bg-white">
+            {props.attributes.map((attribute, i) => {
+              if (filter && !attribute.handleData.attributeName?.toLowerCase().includes(filter.toLowerCase())) return null;
+              if (attribute.handleData.attributeName === undefined) {
+                throw new Error('attribute.handleData.attributeName is undefined');
+              }
+
+              return (
+                <Button
+                  key={(attribute.handleData.attributeName || '') + i}
+                  iconComponent={attribute?.handleData?.attributeDimension ? IconMap[attribute.handleData.attributeDimension] : undefined}
+                  iconPosition="trailing"
+                  additionalClasses={`w-full ${attributesOfInterest[i] ? 'bg-secondary-100' : 'bg-white'} justify-between rounded-none text-[0.7em]`}
+                  variant="ghost"
+                  size={'xs'}
+                  label={attribute.handleData.attributeName}
+                  onClick={(event: React.MouseEvent) => {
+                    dispatch(attributeShownToggle(attribute.handleData));
+                  }}
+                ></Button>
+              );
+            })}
+          </div>
+        </>
+      )}
+    </div>
   );
 };
-- 
GitLab