From c3b8e1452dbc242947757a873cb6e4ed109dfe24 Mon Sep 17 00:00:00 2001
From: Leonardo Christino <leomilho@gmail.com>
Date: Tue, 6 Feb 2024 18:49:32 +0100
Subject: [PATCH] feat: recall saveState from memory on load

---
 .../dbConnectionSelector.tsx                  | 12 +--
 .../DatabaseManagement/forms/databaseForm.tsx |  6 ++
 .../forms/mockSaveStates.tsx                  | 31 ++++++++
 .../DatabaseManagement/forms/settings.tsx     | 16 +---
 libs/shared/lib/data-access/api/eventBus.tsx  | 73 ++++++++++++++++---
 libs/shared/lib/data-access/api/wsState.tsx   |  8 +-
 .../data-access/store/querybuilderSlice.ts    | 10 ++-
 .../lib/data-access/store/sessionSlice.ts     | 39 ++++------
 .../data-access/store/visualizationSlice.ts   |  8 ++
 .../lib/querybuilder/panel/querybuilder.tsx   |  6 +-
 10 files changed, 146 insertions(+), 63 deletions(-)

diff --git a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx
index 5c2a554dc..d8013f96c 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 { updateCurrentSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice';
+import { 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';
@@ -45,7 +45,7 @@ export default function DatabaseSelector({}) {
       timeoutId = setTimeout(() => {
         dispatch(addError("Couldn't establish connection"));
         setConnecting(false);
-        dispatch(updateCurrentSaveState(undefined));
+        dispatch(selectSaveState(undefined));
         dispatch(clearQB());
         dispatch(clearSchema());
       }, 10000);
@@ -87,7 +87,7 @@ export default function DatabaseSelector({}) {
                 <>
                   <div
                     className={`h-2 w-2 rounded-full ${
-                      session.saveStates[session.currentSaveState].db.status === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500'
+                      session.testedSaveState[session.currentSaveState] === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500'
                     }`}
                   />
                   <p className="ml-2 truncate">Connected DB: {session.saveStates[session.currentSaveState].name}</p>
@@ -148,7 +148,7 @@ export default function DatabaseSelector({}) {
                     e.preventDefault();
                     setDbSelectionMenuOpen(false);
                     setConnecting(true);
-                    dispatch(updateCurrentSaveState(save.id));
+                    dispatch(selectSaveState(save.id));
                     dispatch(clearQB());
                     dispatch(clearSchema());
                   } else {
@@ -161,7 +161,7 @@ export default function DatabaseSelector({}) {
               >
                 <div
                   className={`h-[8px] w-[8px] rounded-full shrink-0 ${
-                    save.db.status === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500'
+                    session.testedSaveState[save.id] === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500'
                   }`}
                 />
                 <div className="w-full shrink min-w-0 flex flex-col">
@@ -188,7 +188,7 @@ export default function DatabaseSelector({}) {
                       className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300"
                       onClick={(e) => {
                         e.preventDefault();
-                        dispatch(updateCurrentSaveState(undefined));
+                        dispatch(selectSaveState(undefined));
                         dispatch(clearQB());
                         dispatch(clearSchema());
                         wsDeleteState(save.id);
diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx
index 59beb8bca..4cd8e8fef 100644
--- a/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx
+++ b/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx
@@ -10,6 +10,7 @@ import {
 import { sampleSaveStates } from './mockSaveStates';
 import Input from '@graphpolaris/shared/lib/components/inputs';
 import { useImmer } from 'use-immer';
+import { initialState as qbInitialState } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 
 export const INITIAL_SAVE_STATE: SaveStateI = {
   id: nilUUID,
@@ -23,6 +24,11 @@ export const INITIAL_SAVE_STATE: SaveStateI = {
     internalDatabaseName: 'neo4j',
     type: DatabaseType.Neo4j,
   },
+  schema: {},
+  queryBuilder: qbInitialState,
+  visualization: {},
+  share_state: {},
+  user_id: '',
 };
 
 export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveStateI, error: boolean) => void }) => {
diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx
index ffe4c41be..61f4a3d69 100644
--- a/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx
+++ b/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import { DatabaseInfo } from '@graphpolaris/shared/lib/data-access';
 import { DatabaseType, SaveStateI, nilUUID } from '@graphpolaris/shared/lib/data-access/api/wsState';
+import { initialState as qbInitialState } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 
 export type SaveStateSampleI = SaveStateI & {
   subtitle: string;
@@ -20,6 +21,11 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [
       internalDatabaseName: 'recommendations',
       type: DatabaseType.Neo4j,
     },
+    schema: {},
+    queryBuilder: qbInitialState,
+    visualization: {},
+    share_state: {},
+    user_id: '',
   },
   {
     id: nilUUID,
@@ -34,6 +40,11 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [
       internalDatabaseName: 'movies',
       type: DatabaseType.Neo4j,
     },
+    schema: {},
+    queryBuilder: qbInitialState,
+    visualization: {},
+    share_state: {},
+    user_id: '',
   },
   {
     id: nilUUID,
@@ -48,6 +59,11 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [
       internalDatabaseName: 'northwind',
       type: DatabaseType.Neo4j,
     },
+    schema: {},
+    queryBuilder: qbInitialState,
+    visualization: {},
+    share_state: {},
+    user_id: '',
   },
   {
     id: nilUUID,
@@ -62,6 +78,11 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [
       internalDatabaseName: 'fincen',
       type: DatabaseType.Neo4j,
     },
+    schema: {},
+    queryBuilder: qbInitialState,
+    visualization: {},
+    share_state: {},
+    user_id: '',
   },
   {
     id: nilUUID,
@@ -76,6 +97,11 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [
       internalDatabaseName: 'slack',
       type: DatabaseType.Neo4j,
     },
+    schema: {},
+    queryBuilder: qbInitialState,
+    visualization: {},
+    share_state: {},
+    user_id: '',
   },
   {
     id: nilUUID,
@@ -90,6 +116,11 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [
       internalDatabaseName: 'gameofthrones',
       type: DatabaseType.Neo4j,
     },
+    schema: {},
+    queryBuilder: qbInitialState,
+    visualization: {},
+    share_state: {},
+    user_id: '',
   },
 ];
 
diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
index 859a6fd90..0597c1525 100644
--- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
+++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx
@@ -20,23 +20,9 @@ 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 { DatabaseForm } from './databaseForm';
+import { DatabaseForm, INITIAL_SAVE_STATE } from './databaseForm';
 import { SampleDatabaseSelector } from './mockSaveStates';
 
-export const INITIAL_SAVE_STATE: SaveStateI = {
-  id: nilUUID,
-  name: 'Untitled',
-  db: {
-    username: 'neo4j',
-    password: 'DevOnlyPass',
-    url: 'localhost',
-    port: 7687,
-    protocol: 'neo4j://',
-    internalDatabaseName: 'neo4j',
-    type: DatabaseType.Neo4j,
-  },
-};
-
 type Connection = {
   updating: boolean;
   status: null | string;
diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx
index 235fe3656..9542c359b 100644
--- a/libs/shared/lib/data-access/api/eventBus.tsx
+++ b/libs/shared/lib/data-access/api/eventBus.tsx
@@ -11,23 +11,30 @@ import {
   useQuerybuilderSettings,
   readInSchemaFromBackend,
   assignNewGraphQueryResult,
-  setQuerybuilderNodes,
-  resetGraphQueryResults,
   useQuerybuilder,
+  useVisualizationState,
 } from '@graphpolaris/shared/lib/data-access';
 import { WebSocketHandler } from '@graphpolaris/shared/lib/data-access/socket';
 import Broker from '@graphpolaris/shared/lib/data-access/socket/broker';
-import { addError, addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice';
+import { addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice';
 import { GraphQueryResultFromBackendPayload, queryingBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
 import { allMLTypes, LinkPredictionInstance, setMLResult } from '@graphpolaris/shared/lib/data-access/store/mlSlice';
-import { QueryBuilderState } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
-import { QueryMultiGraph, Query2BackendQuery } from '@graphpolaris/shared/lib/querybuilder';
+import { setQuerybuilderNodes } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 import { SchemaFromBackend } from '@graphpolaris/shared/lib/schema';
-import { useRef, useState, useEffect } from 'react';
-import { DatabaseInfo, DatabaseStatus, SaveStateI, TestDatabaseConnectionResponse, wsGetState, wsGetStates } from './wsState';
+import { useEffect } from 'react';
+import {
+  SaveStateI,
+  TestDatabaseConnectionResponse,
+  wsGetState,
+  wsGetStates,
+  wsUpdateState,
+  wsSelectState,
+} from './wsState';
 import { wsSchemaRequest } from './wsSchema';
-import { addSaveState, testedSaveState, updateCurrentSaveState, updateSaveStateList } from '../store/sessionSlice';
+import { addSaveState, testedSaveState, selectSaveState, updateSaveStateList, updateSelectedSaveState } from '../store/sessionSlice';
 import { URLParams, getParam } from './url';
+import { setVisualizationState } from '../store/visualizationSlice';
+import { isEqual } from 'lodash-es';
 
 export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }) => {
   const { login } = useAuth();
@@ -37,19 +44,22 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
   const queryHash = useQuerybuilderHash();
   const queryBuilder = useQuerybuilder();
   const mlHash = useMLEnabledHash();
+  const visState = useVisualizationState();
   const queryBuilderSettings = useQuerybuilderSettings();
 
   useEffect(() => {
-    // Default
     Broker.instance().subscribe((data: SchemaFromBackend) => {
       dispatch(readInSchemaFromBackend(data));
       dispatch(addInfo('Schema graph updated'));
     }, 'schema_result');
+
     Broker.instance().subscribe((data: GraphQueryResultFromBackendPayload) => {
       dispatch(assignNewGraphQueryResult(data));
       dispatch(addInfo('Query Executed!'));
     }, 'query_result');
+
     // Broker.instance().subscribe((data: QueryBuilderState) => dispatch(setQuerybuilderNodes(data)), 'query_builder_state');
+
     allMLTypes.forEach((mlType) => {
       Broker.instance().subscribe((data: LinkPredictionInstance[]) => dispatch(setMLResult({ type: mlType, result: data })), mlType);
     });
@@ -57,13 +67,19 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
     Broker.instance().subscribe((data: SaveStateI[]) => {
       dispatch(updateSaveStateList(data));
     }, 'save_states');
+
     Broker.instance().subscribe((data: any) => {}, 'save_state_status');
+
     Broker.instance().subscribe((data: SaveStateI) => {
       dispatch(addSaveState(data));
     }, 'save_state');
+
     Broker.instance().subscribe((data: SaveStateI) => {
       wsGetStates();
     }, 'delete_save_state');
+
+    Broker.instance().subscribe((data: { saveStateID: string; success: boolean }) => {}, 'save_state_selected');
+
     Broker.instance().subscribe((response: TestDatabaseConnectionResponse) => {
       if (response && response.status === 'success') dispatch(testedSaveState(response.saveStateID));
     }, 'tested_connection');
@@ -80,6 +96,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
       Broker.instance().unSubscribeAll('save_state_status');
       Broker.instance().unSubscribeAll('delete_save_state');
       Broker.instance().unSubscribeAll('tested_connection');
+      Broker.instance().unSubscribeAll('save_state_selected');
       // Broker.instance().unSubscribeAll('query_builder_state');
       allMLTypes.forEach((mlType) => {
         Broker.instance().unSubscribeAll(mlType);
@@ -87,13 +104,49 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
     };
   }, []);
 
+  useEffect(() => {
+    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);
+        state.queryBuilder = { ...queryBuilder };
+        dispatch(updateSelectedSaveState(state));
+        wsUpdateState(state);
+      }
+    }
+  }, [queryBuilderSettings, queryHash]);
+
+  useEffect(() => {
+    if (session.currentSaveState) {
+      let state = { ...session.saveStates[session.currentSaveState] };
+      if (!isEqual(state.visualization, visState)) {
+        console.log('Updating visState state', visState);
+        state.visualization = { ...visState };
+        dispatch(updateSelectedSaveState(state));
+        wsUpdateState(state);
+      }
+    }
+  }, [visState]);
+
   useEffect(() => {
     // New active database
     if (session.currentSaveState) {
       wsSchemaRequest(session.currentSaveState);
+      wsSelectState(session.currentSaveState);
     }
   }, [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) {
@@ -107,7 +160,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
           const paramSaveState = getParam(URLParams.saveState);
           if (paramSaveState) {
             wsGetState(paramSaveState);
-            dispatch(updateCurrentSaveState(paramSaveState));
+            dispatch(selectSaveState(paramSaveState));
           }
 
           wsGetStates();
diff --git a/libs/shared/lib/data-access/api/wsState.tsx b/libs/shared/lib/data-access/api/wsState.tsx
index b33e8b36b..481caf8c2 100644
--- a/libs/shared/lib/data-access/api/wsState.tsx
+++ b/libs/shared/lib/data-access/api/wsState.tsx
@@ -1,4 +1,5 @@
 import { WebSocketHandler } from '../socket';
+import { QueryBuilderState } from '../store/querybuilderSlice';
 import { URLParams, setParam } from './url';
 
 // export function wsGetState() {
@@ -28,15 +29,19 @@ export type DatabaseInfo = {
   username: string;
   password: string;
   type: number;
-  status?: DatabaseStatus;
 };
 
 export const nilUUID = '00000000-0000-0000-0000-000000000000';
 
 export type SaveStateI = {
   id: string;
+  user_id: string;
   name: string;
   db: DatabaseInfo;
+  schema: any;
+  queryBuilder: QueryBuilderState;
+  visualization: any;
+  share_state: any;
 };
 
 export function wsGetState(saveStateId: string) {
@@ -61,7 +66,6 @@ export function wsSelectState(saveStateId: string | undefined) {
     subKey: 'select',
     body: { saveStateId: saveStateId }, //messageTypeGetSaveState
   });
-  console.log('saveStateId', saveStateId);
 
   WebSocketHandler.instance().useSaveStateID(saveStateId);
   setParam(URLParams.saveState, saveStateId);
diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts
index 9c5fe1fc6..03cce603d 100644
--- a/libs/shared/lib/data-access/store/querybuilderSlice.ts
+++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts
@@ -43,9 +43,11 @@ export const querybuilderSlice = createSlice({
       state.ignoreReactivity = false;
     },
     setQuerybuilderNodes: (state, action: PayloadAction<QueryBuilderState>) => {
-      state.graph = action.payload.graph;
-      state.settings = action.payload.settings;
-      state.ignoreReactivity = true;
+      if (action.payload.graph?.nodes && action.payload.graph?.edges) {
+        state.graph = action.payload.graph;
+        state.settings = action.payload.settings;
+        // state.ignoreReactivity = true;
+      }
     },
     clearQB: (state) => {
       state.graph = defaultGraph();
@@ -77,7 +79,7 @@ export const selectQuerybuilderGraph = (state: RootState): QueryMultiGraph => {
 };
 
 /** Select the querybuilder nodes and convert it to a graphology object */
-export const selectQuerybuilderHash = (state: RootState): any => {
+export const selectQuerybuilderHash = (state: RootState): string => {
   const hashedNodes = state.querybuilder.graph.nodes.map((n) => {
     let node = { ...n };
     if (n?.attributes) {
diff --git a/libs/shared/lib/data-access/store/sessionSlice.ts b/libs/shared/lib/data-access/store/sessionSlice.ts
index 07818ab5f..4b58a94d6 100644
--- a/libs/shared/lib/data-access/store/sessionSlice.ts
+++ b/libs/shared/lib/data-access/store/sessionSlice.ts
@@ -13,31 +13,36 @@ export type ErrorMessage = {
 export type SessionCacheI = {
   currentSaveState?: string; // id of the current save state
   saveStates: Record<string, SaveStateI>;
+  testedSaveState: Record<string, DatabaseStatus>;
 };
 
 // Define the initial state using that type
 export const initialState: SessionCacheI = {
   currentSaveState: undefined,
   saveStates: {},
+  testedSaveState: {},
 };
 
 export const sessionSlice = createSlice({
   name: 'session',
   initialState,
   reducers: {
-    updateCurrentSaveState(state, action: PayloadAction<string | undefined>) {
+    selectSaveState(state, action: PayloadAction<string | undefined>) {
       state.currentSaveState = action.payload;
-      wsSelectState(state.currentSaveState);
+    },
+    updateSelectedSaveState(state, action: PayloadAction<SaveStateI>) {
+      if (state.currentSaveState === action.payload.id)
+        //@ts-ignore
+        state.saveStates = {
+          ...state.saveStates,
+          [state.currentSaveState]: action.payload,
+        };
     },
     updateSaveStateList(state, action: PayloadAction<SaveStateI[]>) {
       // Does NOT clear the states, just adds in new data
       let newState: Record<string, SaveStateI> = {};
       action.payload.forEach((ss) => {
         newState[ss.id] = ss;
-        // Keep db status (e.g. tested) if already tested
-        if (ss.id in state.saveStates) {
-          newState[ss.id].db.status = state.saveStates[ss.id].db.status || newState[ss.id].db.status;
-        }
       });
       state.saveStates = { ...state.saveStates, ...newState };
 
@@ -48,7 +53,6 @@ export const sessionSlice = createSlice({
         } else if (Object.keys(state.saveStates).length > 0) {
           state.currentSaveState = Object.keys(state.saveStates)[0];
         } else state.currentSaveState = undefined;
-        wsSelectState(state.currentSaveState);
       }
     },
     setSaveStateList(state, action: PayloadAction<SaveStateI[]>) {
@@ -56,9 +60,6 @@ export const sessionSlice = createSlice({
       let newState: Record<string, SaveStateI> = {};
       action.payload.forEach((ss) => {
         newState[ss.id] = ss;
-        if (ss.id in state.saveStates) {
-          newState[ss.id].db.status = state.saveStates[ss.id].db.status || newState[ss.id].db.status;
-        }
       });
       state.saveStates = newState;
 
@@ -69,31 +70,21 @@ export const sessionSlice = createSlice({
         } else if (Object.keys(state.saveStates).length > 0) {
           state.currentSaveState = Object.keys(state.saveStates)[0];
         } else state.currentSaveState = undefined;
-        wsSelectState(state.currentSaveState);
       }
     },
     addSaveState(state, action: PayloadAction<SaveStateI>) {
       if (state.saveStates === undefined) state.saveStates = {};
-      if (action.payload.id in state.saveStates) {
-        // Keep db status tested if was already tested
-        const status = state.saveStates[action.payload.id].db.status || action.payload.db.status;
-        state.saveStates[action.payload.id] = action.payload;
-        state.saveStates[action.payload.id].db.status = status;
-      } else {
-        state.saveStates[action.payload.id] = action.payload;
-      }
+      state.saveStates[action.payload.id] = action.payload;
       state.currentSaveState = action.payload.id;
-      wsSelectState(state.currentSaveState);
     },
     testedSaveState(state, action: PayloadAction<string>) {
-      if (action.payload in state.saveStates) {
-        state.saveStates[action.payload].db.status = DatabaseStatus.tested;
-      }
+      state.testedSaveState = { ...state.testedSaveState, [action.payload]: DatabaseStatus.tested };
     },
   },
 });
 
-export const { updateCurrentSaveState, updateSaveStateList, setSaveStateList, addSaveState, testedSaveState } = sessionSlice.actions;
+export const { selectSaveState, 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/data-access/store/visualizationSlice.ts b/libs/shared/lib/data-access/store/visualizationSlice.ts
index d149f2f63..9213b9e68 100644
--- a/libs/shared/lib/data-access/store/visualizationSlice.ts
+++ b/libs/shared/lib/data-access/store/visualizationSlice.ts
@@ -1,6 +1,7 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import type { RootState } from './store';
 import { globalConfigSchemaTypes, localConfigSchemaType } from '../../vis/Types';
+import { isEqual } from 'lodash-es';
 
 export enum Visualizations {
   NodeLink = 'NodeLinkVis',
@@ -57,6 +58,12 @@ export const visualizationSlice = createSlice({
     updateSettings: (state, action: PayloadAction<any>) => {
       state.settings.visualization = action.payload;
     },
+    setVisualizationState: (state, action: PayloadAction<VisState>) => {
+      if (action.payload.activeVisualization && !isEqual(action.payload, state)) {
+        state.activeVisualization = action.payload.activeVisualization;
+        state.settings = action.payload.settings;
+      }
+    },
   },
 });
 
@@ -67,6 +74,7 @@ export const {
   updateGeneralSettings,
   setActiveVisualization,
   updateSettings,
+  setVisualizationState,
 } = visualizationSlice.actions;
 
 export const visualizationState = (state: RootState) => state.visualize;
diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx
index 0bab30e6b..65838b391 100644
--- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx
+++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx
@@ -1,6 +1,7 @@
 import {
   useConfig,
   useQuerybuilderGraph,
+  useQuerybuilderHash,
   useQuerybuilderSettings,
   useSchemaGraph,
   useSearchResultQB,
@@ -68,6 +69,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
   const schemaGraph = useSchemaGraph();
   const schema = useMemo(() => toSchemaGraphology(schemaGraph), [schemaGraph]);
   const graph = useQuerybuilderGraph();
+  const qbHash = useQuerybuilderHash();
   const config = useConfig();
   const dispatch = useDispatch();
   const isDraggingPill = useRef(false);
@@ -75,8 +77,8 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
   const reactFlow = useReactFlow();
   const isEdgeUpdating = useRef(false);
   const isOnConnect = useRef(false);
-  const graphologyGraph = useMemo(() => toQuerybuilderGraphology(graph), [graph]);
-  const elements = useMemo(() => createReactFlowElements(graphologyGraph), [graph]);
+  const graphologyGraph = useMemo(() => toQuerybuilderGraphology(graph), [graph, qbHash]);
+  const elements = useMemo(() => createReactFlowElements(graphologyGraph), [graph, qbHash]);
   const searchResults = useSearchResultQB();
   const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null);
 
-- 
GitLab