From 3a92f0c4b52bfa1656421643ef9487d21b3f1a27 Mon Sep 17 00:00:00 2001
From: Leonardo Christino <leomilho@gmail.com>
Date: Wed, 7 Feb 2024 12:02:00 +0100
Subject: [PATCH] fix: infinite loop, delete, and empty tooltip

---
 .../dbConnectionSelector.tsx                  | 19 +++---
 .../DatabaseManagement/forms/settings.tsx     | 15 +++--
 libs/shared/lib/components/inputs/index.tsx   | 12 ++--
 libs/shared/lib/data-access/api/eventBus.tsx  | 63 ++++++++++---------
 .../lib/data-access/store/sessionSlice.ts     | 26 ++++++--
 .../table_vis/components/Table.tsx            |  2 +-
 6 files changed, 81 insertions(+), 56 deletions(-)

diff --git a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx
index d8013f96c..4301840ad 100644
--- a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx
+++ b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx
@@ -1,7 +1,7 @@
 import React, { useEffect, useState } from 'react';
 import { Add, Delete, Settings } from '@mui/icons-material';
 import { useAppDispatch, useSchemaGraph, useSessionCache, useAuthorizationCache } from '@graphpolaris/shared/lib/data-access';
-import { selectSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice';
+import { deleteSaveState, selectSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice';
 import { SettingsForm } from './forms/settings';
 import { LoadingSpinner } from '@graphpolaris/shared/lib/components/LoadingSpinner';
 import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice';
@@ -67,18 +67,12 @@ export default function DatabaseSelector({}) {
           }}
         />
       )}
-      {/* <NewDatabaseForm
-        open={addDbConnectionFormOpen}
-        onClose={() => {
-          setAddDbConnectionFormOpen(false);
-        }}
-      /> */}
       <DropdownContainer ref={dbSelectionMenuRef} className="w-[20rem]">
         <DropdownButton
           disabled={connecting || authCache.authorized === false || !!authCache.roomID}
           title={
             <div className="flex items-center">
-              {connecting && session.currentSaveState ? (
+              {connecting && session.currentSaveState && session.currentSaveState in session.saveStates ? (
                 <>
                   <LoadingSpinner />
                   <p className="ml-2 truncate">Connecting to {session.saveStates[session.currentSaveState].name}</p>
@@ -188,10 +182,13 @@ export default function DatabaseSelector({}) {
                       className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300"
                       onClick={(e) => {
                         e.preventDefault();
-                        dispatch(selectSaveState(undefined));
-                        dispatch(clearQB());
-                        dispatch(clearSchema());
+                        e.stopPropagation();
+                        if (session.currentSaveState === save.id) {
+                          dispatch(clearQB());
+                          dispatch(clearSchema());
+                        }
                         wsDeleteState(save.id);
+                        dispatch(deleteSaveState(save.id));
                       }}
                       title="Delete database connection"
                     >
diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
index 0597c1525..b54f506bc 100644
--- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
+++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
@@ -11,15 +11,14 @@ import {
   wsCreateState,
   nilUUID,
   DatabaseType,
+  useAuthorizationCache,
 } from '@graphpolaris/shared/lib/data-access';
 import { ErrorOutline } from '@mui/icons-material';
 import { Dialog } from '@graphpolaris/shared/lib/components/Dialog';
-import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice';
 import { Button } from '@graphpolaris/shared/lib/components/buttons';
-import Input from '@graphpolaris/shared/lib/components/inputs';
 import { useImmer } from 'use-immer';
 import Broker from '@graphpolaris/shared/lib/data-access/socket/broker';
-import { addSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice';
+import { addSaveState, testedSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice';
 import { DatabaseForm, INITIAL_SAVE_STATE } from './databaseForm';
 import { SampleDatabaseSelector } from './mockSaveStates';
 
@@ -32,7 +31,10 @@ type Connection = {
 export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update'; saveState: SaveStateI | null }) => {
   const dispatch = useAppDispatch();
   const ref = useRef<HTMLDialogElement>(null);
-  const [formData, setFormData] = useImmer(props.saveState && props.open === 'update' ? props.saveState : INITIAL_SAVE_STATE);
+  const auth = useAuthorizationCache();
+  const [formData, setFormData] = useImmer(
+    props.saveState && props.open === 'update' ? props.saveState : { ...INITIAL_SAVE_STATE, user_id: auth.userID || '' },
+  );
   const [hasError, setHasError] = useState(false);
   const [sampleDataPanel, setSampleDataPanel] = useState<boolean | null>(false);
   const [connection, setConnection] = useState<Connection>({
@@ -50,6 +52,10 @@ export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update'
         console.error('formDataRef.current is null');
         return;
       }
+      if (formDataRef.current.user_id !== auth.userID && auth.userID) {
+        console.error('formDataRef.current.user_id is not equal to auth.userID');
+        formDataRef.current.user_id = auth.userID;
+      }
       if (data.status === 'success') {
         setConnection((prevState) => ({
           updating: false,
@@ -73,6 +79,7 @@ export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update'
       let _data = JSON.parse(JSON.stringify(data));
       _data.db.status = DatabaseStatus.tested;
       dispatch(addSaveState(_data));
+      dispatch(testedSaveState(_data.id));
       formDataRef.current = null;
       closeDialog();
     },
diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx
index 76b72f2ba..2595b8038 100644
--- a/libs/shared/lib/components/inputs/index.tsx
+++ b/libs/shared/lib/components/inputs/index.tsx
@@ -89,7 +89,7 @@ const Input = (props: InputProps) => {
 
 export const SliderInput = ({ label, value, min, max, step, unit, showValue = true, onChange, tooltip }: SliderProps) => {
   return (
-    <div data-tip={tooltip} className={'tooltip ' + styles['slider']}>
+    <div data-tip={tooltip || null} className={styles['slider'] + (tooltip ? ' tooltip' : '')}>
       <label className="label flex flex-row justify-between items-end">
         <span className="label-text">{label}</span>
         {showValue ? (
@@ -132,7 +132,7 @@ export const TextInput = ({
   const [isValid, setIsValid] = React.useState<boolean>(true);
 
   return (
-    <div data-tip={tooltip || null} className="tooltip form-control w-full">
+    <div data-tip={tooltip || null} className={'form-control w-full' + (tooltip ? ' tooltip' : '')}>
       <label className="label">
         <span className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`}>
           {label}
@@ -163,7 +163,7 @@ export const TextInput = ({
 
 export const RadioInput = ({ label, value, options, onChange, tooltip }: RadioProps) => {
   return (
-    <div data-tip={tooltip || null} className="tooltip">
+    <div data-tip={tooltip || null} className={tooltip ? 'tooltip' : ''}>
       <label className="label">
         <span className="label-text">{label}</span>
       </label>
@@ -189,7 +189,7 @@ export const RadioInput = ({ label, value, options, onChange, tooltip }: RadioPr
 
 export const CheckboxInput = ({ label, value, options, onChange, tooltip }: CheckboxProps) => {
   return (
-    <div data-tip={tooltip || null} className="tooltip">
+    <div data-tip={tooltip || null} className={tooltip ? 'tooltip' : ''}>
       {label && (
         <label className="label">
           <span className="label-text">{label}</span>
@@ -218,7 +218,7 @@ export const CheckboxInput = ({ label, value, options, onChange, tooltip }: Chec
 
 export const BooleanInput = ({ label, value, onChange, tooltip }: BooleanProps) => {
   return (
-    <div data-tip={tooltip || null} className="tooltip">
+    <div data-tip={tooltip || null} className={tooltip ? 'tooltip' : ''}>
       <label className={`label cursor-pointer w-fit gap-2 px-0 py-1`}>
         <span className="label-text">{label}</span>
         <input
@@ -253,7 +253,7 @@ export const DropDownInput = ({ label, value, options, onChange, required = fals
   }, [isDropdownOpen]);
 
   return (
-    <div data-tip={tooltip || null} className="tooltip w-full">
+    <div data-tip={tooltip || null} className={'w-full' + (tooltip ? ' tooltip' : '')}>
       {label && (
         <label className="label">
           <span
diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx
index 9542c359b..acc34c2a8 100644
--- a/libs/shared/lib/data-access/api/eventBus.tsx
+++ b/libs/shared/lib/data-access/api/eventBus.tsx
@@ -22,17 +22,10 @@ import { allMLTypes, LinkPredictionInstance, setMLResult } from '@graphpolaris/s
 import { setQuerybuilderNodes } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 import { SchemaFromBackend } from '@graphpolaris/shared/lib/schema';
 import { useEffect } from 'react';
-import {
-  SaveStateI,
-  TestDatabaseConnectionResponse,
-  wsGetState,
-  wsGetStates,
-  wsUpdateState,
-  wsSelectState,
-} from './wsState';
+import { SaveStateI, TestDatabaseConnectionResponse, wsGetState, wsGetStates, wsUpdateState, wsSelectState, nilUUID } from './wsState';
 import { wsSchemaRequest } from './wsSchema';
 import { addSaveState, testedSaveState, selectSaveState, updateSaveStateList, updateSelectedSaveState } from '../store/sessionSlice';
-import { URLParams, getParam } from './url';
+import { URLParams, getParam, deleteParam } from './url';
 import { setVisualizationState } from '../store/visualizationSlice';
 import { isEqual } from 'lodash-es';
 
@@ -47,6 +40,17 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
   const visState = useVisualizationState();
   const queryBuilderSettings = useQuerybuilderSettings();
 
+  function loadSaveState(saveStateID: string | undefined, saveStates: Record<string, SaveStateI>) {
+    if (saveStateID && saveStates && saveStateID in saveStates) {
+      console.debug('Setting state from database', saveStateID, saveStates);
+      const state = saveStates[saveStateID];
+      if (state) {
+        dispatch(setQuerybuilderNodes(state.queryBuilder));
+        dispatch(setVisualizationState(state.visualization));
+      }
+    }
+  }
+
   useEffect(() => {
     Broker.instance().subscribe((data: SchemaFromBackend) => {
       dispatch(readInSchemaFromBackend(data));
@@ -66,17 +70,25 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
 
     Broker.instance().subscribe((data: SaveStateI[]) => {
       dispatch(updateSaveStateList(data));
+      console.debug('Save States updated', data);
+      const d = Object.fromEntries(data.map((x) => [x.id, x]));
+      loadSaveState(session.currentSaveState, d);
+      // useEffect(() => {
+
+      // }, [session.currentSaveState, session.saveStates]);
     }, 'save_states');
 
     Broker.instance().subscribe((data: any) => {}, 'save_state_status');
 
     Broker.instance().subscribe((data: SaveStateI) => {
-      dispatch(addSaveState(data));
+      if (data.id !== nilUUID) {
+        dispatch(addSaveState(data));
+        dispatch(selectSaveState(data.id));
+        loadSaveState(data.id, session.saveStates);
+      }
     }, 'save_state');
 
-    Broker.instance().subscribe((data: SaveStateI) => {
-      wsGetStates();
-    }, 'delete_save_state');
+    Broker.instance().subscribe((data: SaveStateI) => {}, 'delete_save_state');
 
     Broker.instance().subscribe((data: { saveStateID: string; success: boolean }) => {}, 'save_state_selected');
 
@@ -108,7 +120,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
     if (session.currentSaveState) {
       let state = { ...session.saveStates[session.currentSaveState] };
       if (!isEqual(state.queryBuilder, queryBuilder) && state.queryBuilder?.graph?.nodes) {
-        console.log('Updating queryBuilder state', state.queryBuilder, queryBuilder);
+        console.debug('Updating queryBuilder state', state.queryBuilder, queryBuilder);
         state.queryBuilder = { ...queryBuilder };
         dispatch(updateSelectedSaveState(state));
         wsUpdateState(state);
@@ -120,7 +132,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
     if (session.currentSaveState) {
       let state = { ...session.saveStates[session.currentSaveState] };
       if (!isEqual(state.visualization, visState)) {
-        console.log('Updating visState state', visState);
+        console.debug('Updating visState state', visState);
         state.visualization = { ...visState };
         dispatch(updateSelectedSaveState(state));
         wsUpdateState(state);
@@ -130,23 +142,13 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
 
   useEffect(() => {
     // New active database
-    if (session.currentSaveState) {
+    if (session.currentSaveState && session.currentSaveState !== nilUUID) {
       wsSchemaRequest(session.currentSaveState);
       wsSelectState(session.currentSaveState);
+      loadSaveState(session.currentSaveState, session.saveStates);
     }
   }, [session.currentSaveState]);
 
-  useEffect(() => {
-    if (session.currentSaveState && session.saveStates && session.currentSaveState in session.saveStates) {
-      const state = session.saveStates[session.currentSaveState];
-      console.log('Setting state from database', state, session.currentSaveState, session.saveStates);
-      if (state) {
-        dispatch(setQuerybuilderNodes(state.queryBuilder));
-        dispatch(setVisualizationState(state.visualization));
-      }
-    }
-  }, [session.currentSaveState, session.saveStates]);
-
   useEffect(() => {
     // Newly (un)authorized
     if (auth.authorized && auth.jwt) {
@@ -154,13 +156,14 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
       WebSocketHandler.instance()
         .useAuth(auth)
         .connect(() => {
-          console.log('WS connected', session.currentSaveState, window.location.search);
+          console.debug('WS connected', session.currentSaveState, window.location.search);
 
           // Process URL Params
           const paramSaveState = getParam(URLParams.saveState);
-          if (paramSaveState) {
+          if (paramSaveState && paramSaveState !== nilUUID) {
             wsGetState(paramSaveState);
-            dispatch(selectSaveState(paramSaveState));
+          } else {
+            deleteParam(URLParams.saveState);
           }
 
           wsGetStates();
diff --git a/libs/shared/lib/data-access/store/sessionSlice.ts b/libs/shared/lib/data-access/store/sessionSlice.ts
index 4b58a94d6..ca4b11e7b 100644
--- a/libs/shared/lib/data-access/store/sessionSlice.ts
+++ b/libs/shared/lib/data-access/store/sessionSlice.ts
@@ -1,6 +1,6 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import type { RootState } from './store';
-import { DatabaseStatus, SaveStateI, wsSelectState, wsTestSaveStateConnection } from '../api/wsState';
+import { DatabaseStatus, SaveStateI } from '../api/wsState';
 import { getParam, URLParams } from '../api/url';
 
 /** Message format of the error message from the backend */
@@ -28,7 +28,11 @@ export const sessionSlice = createSlice({
   initialState,
   reducers: {
     selectSaveState(state, action: PayloadAction<string | undefined>) {
-      state.currentSaveState = action.payload;
+      if (action.payload !== undefined && action.payload in state.saveStates) {
+        state.currentSaveState = action.payload;
+      } else {
+        state.currentSaveState = undefined;
+      }
     },
     updateSelectedSaveState(state, action: PayloadAction<SaveStateI>) {
       if (state.currentSaveState === action.payload.id)
@@ -77,14 +81,28 @@ export const sessionSlice = createSlice({
       state.saveStates[action.payload.id] = action.payload;
       state.currentSaveState = action.payload.id;
     },
+    deleteSaveState(state, action: PayloadAction<string>) {
+      delete state.saveStates[action.payload];
+      if (state.currentSaveState === action.payload) {
+        if (Object.keys(state.saveStates).length > 0) state.currentSaveState = Object.keys(state.saveStates)[0];
+        else state.currentSaveState = undefined;
+      }
+    },
     testedSaveState(state, action: PayloadAction<string>) {
       state.testedSaveState = { ...state.testedSaveState, [action.payload]: DatabaseStatus.tested };
     },
   },
 });
 
-export const { selectSaveState, updateSaveStateList, setSaveStateList, addSaveState, testedSaveState, updateSelectedSaveState } =
-  sessionSlice.actions;
+export const {
+  selectSaveState,
+  deleteSaveState,
+  updateSaveStateList,
+  setSaveStateList,
+  addSaveState,
+  testedSaveState,
+  updateSelectedSaveState,
+} = sessionSlice.actions;
 
 // Other code such as selectors can use the imported `RootState` type
 export const sessionCacheState = (state: RootState) => state.sessionCache;
diff --git a/libs/shared/lib/vis/visualizations/table_vis/components/Table.tsx b/libs/shared/lib/vis/visualizations/table_vis/components/Table.tsx
index 79906526e..65c10c0fb 100644
--- a/libs/shared/lib/vis/visualizations/table_vis/components/Table.tsx
+++ b/libs/shared/lib/vis/visualizations/table_vis/components/Table.tsx
@@ -224,7 +224,7 @@ export const Table = ({ data, itemsPerPage, showBarPlot }: TableProps) => {
                 </tr>
                 <tr>
                   {dataColumns.map((item, index) => (
-                    <th className="border-light bg-light">
+                    <th className="border-light bg-light" key={index}>
                       <div className="th" key={index + item}>
                         <div className="h-full w-full overflow-hidden">
                           {data2Render[index].showBarPlot &&
-- 
GitLab