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