From 300e7797d6039f69f5736fe9515b5f4d57ecfc7c Mon Sep 17 00:00:00 2001
From: Milho001 <l.milhomemfrancochristino@uu.nl>
Date: Thu, 20 Mar 2025 18:10:01 +0000
Subject: [PATCH] feat: abort query and split from count query

---
 src/App.tsx                                   |   7 +-
 src/lib/data-access/api/eventBus.tsx          |  55 ++++++--
 src/lib/data-access/broker/broker.tsx         |  43 +++---
 src/lib/data-access/broker/wsQuery.ts         |  16 ++-
 .../store/graphQueryResultSlice.ts            | 126 ++++++++++++------
 src/lib/data-access/store/hooks.ts            |   7 +-
 src/lib/data-access/store/sessionSlice.ts     |   2 +-
 src/lib/management/database/Databases.tsx     |  63 ++++-----
 .../querybuilder/panel/QueryBuilderNav.tsx    |  26 ++--
 src/lib/vis/components/VisualizationPanel.tsx |  17 ++-
 src/lib/vis/views/NoData.tsx                  |  19 ++-
 11 files changed, 246 insertions(+), 135 deletions(-)

diff --git a/src/App.tsx b/src/App.tsx
index 719242c41..5cb3ae631 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -13,7 +13,7 @@ import { InspectorPanel } from '@/lib/inspector';
 import { SearchBar } from '@/lib/sidebar/search/SearchBar';
 import { Schema } from '@/lib/schema/panel';
 import { InsightDialog } from '@/lib/insight-sharing';
-import { wsQueryRequest } from '@/lib/data-access/broker';
+import { Broker, wsQueryRequest } from '@/lib/data-access/broker';
 import { ErrorBoundary } from '@/lib/components/errorBoundary';
 import { Onboarding } from './app/onboarding/onboarding';
 import { Navbar } from './app/navbar/navbar';
@@ -36,8 +36,9 @@ export function App(props: App) {
         console.log('No query to run');
         dispatch(resetGraphQueryResults());
       } else {
-        dispatch(queryingBackend());
-        wsQueryRequest({ saveStateID: session.currentSaveState, ml, queryID: activeQuery.id, useCached });
+        const callID = Broker.instance().generateCallID();
+        dispatch(queryingBackend({ status: 'querying', callID }));
+        wsQueryRequest({ saveStateID: session.currentSaveState, ml, queryID: activeQuery.id, useCached, callID });
       }
     }
   };
diff --git a/src/lib/data-access/api/eventBus.tsx b/src/lib/data-access/api/eventBus.tsx
index 95857ef2a..63501e345 100644
--- a/src/lib/data-access/api/eventBus.tsx
+++ b/src/lib/data-access/api/eventBus.tsx
@@ -14,7 +14,7 @@ import {
   useActiveSaveState,
   resetGraphQueryResults,
 } from '@/lib/data-access';
-import { Broker, wsQueryErrorSubscription, wsQueryRequest, wsQuerySubscription } from '@/lib/data-access/broker';
+import { Broker, wsQueryCountSubscription, wsQueryErrorSubscription, wsQueryRequest, wsQuerySubscription } from '@/lib/data-access/broker';
 import { addInfo } from '@/lib/data-access/store/configSlice';
 import { setMLResult } from '@/lib/data-access/store/mlSlice';
 import { useEffect } from 'react';
@@ -40,6 +40,7 @@ import {
   setQueryState,
   setQuerybuilderNodes,
   selectSaveState,
+  setQueryNodeCounts,
 } from '../store/sessionSlice';
 import { URLParams, getParam, setParam } from './url';
 import { VisState, setVisualizationState } from '../store/visualizationSlice';
@@ -49,8 +50,16 @@ import { addError } from '@/lib/data-access/store/configSlice';
 import { unSelect } from '../store/interactionSlice';
 import { wsReconnectSubscription, wsUserGetPolicy } from '../broker/wsUser';
 import { authorized, setSessionID } from '../store/authSlice';
-import { asyncSetNewGraphQueryResult, queryingBackend } from '../store/graphQueryResultSlice';
-import { SaveState, allMLTypes, LinkPredictionInstance, GraphQueryTranslationResultMessage, wsReturnKey, Schema } from 'ts-common';
+import { abortedBackend, asyncSetNewGraphQueryResult, queryingBackend, setNewGraphQueryCountResult } from '../store/graphQueryResultSlice';
+import {
+  SaveState,
+  allMLTypes,
+  AbortedError,
+  LinkPredictionInstance,
+  GraphQueryTranslationResultMessage,
+  wsReturnKey,
+  Schema,
+} from 'ts-common';
 
 export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAuthorized: () => void }) => {
   const { login } = useAuthentication();
@@ -136,19 +145,45 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu
     );
 
     unsubs.push(
-      wsQuerySubscription(({ data, status }) => {
+      wsQuerySubscription(({ data, status, callID }) => {
+        if (status === 'aborted') {
+          dispatch(abortedBackend({ type: 'result', callID: callID }));
+          return;
+        }
         if (!data || status !== 'success') {
           dispatch(addError('Failed to fetch graph query result'));
           return;
         }
-        asyncSetNewGraphQueryResult(data, dispatch);
+        asyncSetNewGraphQueryResult({ data, callID }, dispatch);
         dispatch(addInfo('Query Executed!'));
         dispatch(unSelect());
       }),
     );
 
+    unsubs.push(
+      wsQueryCountSubscription(params => {
+        const { data, status } = params;
+        if (status === 'aborted') {
+          dispatch(abortedBackend({ type: 'count', callID: params.callID }));
+          return;
+        }
+        if (!data || status !== 'success') {
+          console.warn('Failed to fetch graph query counts', params);
+          dispatch(addError('Failed to fetch graph query counts'));
+          return;
+        }
+        // dispatch(setQueryNodeCounts(data)); // TODO needed?
+        dispatch(setNewGraphQueryCountResult({ data, callID: params.callID }));
+        dispatch(addInfo('Query Count Executed!'));
+      }),
+    );
+
     unsubs.push(
       wsQueryErrorSubscription(params => {
+        if (params.data?.name === new AbortedError().name) {
+          dispatch(resetGraphQueryResults());
+          return;
+        }
         dispatch(addError('Failed to fetch graph query result'));
         console.error('Query Error', params.data);
         dispatch(resetGraphQueryResults());
@@ -316,8 +351,9 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu
 
         if (data) {
           if (activeQuery?.id) {
-            dispatch(queryingBackend());
-            wsQueryRequest({ saveStateID: activeSS.id, ml, queryID: activeQuery.id, useCached: true });
+            const callID = Broker.instance().generateCallID();
+            dispatch(queryingBackend({ callID, status: 'querying' }));
+            wsQueryRequest({ saveStateID: activeSS.id, ml, queryID: activeQuery.id, useCached: true, callID });
           }
         }
       });
@@ -354,8 +390,9 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu
       session.testedSaveState[session.currentSaveState] === DatabaseStatus.online &&
       activeQuery?.id != null
     ) {
-      dispatch(queryingBackend());
-      wsQueryRequest({ saveStateID: session.currentSaveState, ml, queryID: activeQuery.id, useCached: true });
+      const callID = Broker.instance().generateCallID();
+      dispatch(queryingBackend({ callID, status: 'querying' }));
+      wsQueryRequest({ saveStateID: session.currentSaveState, ml, queryID: activeQuery.id, useCached: true, callID });
     }
   }, [session.currentSaveState, session.testedSaveState]);
 
diff --git a/src/lib/data-access/broker/broker.tsx b/src/lib/data-access/broker/broker.tsx
index e9d421799..04ae34075 100644
--- a/src/lib/data-access/broker/broker.tsx
+++ b/src/lib/data-access/broker/broker.tsx
@@ -27,9 +27,9 @@ let keepAlivePing: ReturnType<typeof setInterval>;
 export class Broker {
   private static singletonInstance: Broker;
 
-  private listeners: Record<string, Record<string, ResponseCallback<wsReturnKeyWithML>>> = {};
+  private listeners: Record<string, Record<string, ResponseCallback<any>>> = {};
   private catchAllListener: ResponseCallback<wsReturnKeyWithML> | undefined;
-  private callbackListeners: Record<string, ResponseCallback<wsReturnKeyWithML>> = {};
+  private callbackListeners: Record<string, ResponseCallback<any>> = {};
 
   private webSocket: WebSocket | undefined;
   private url: string;
@@ -68,6 +68,7 @@ export class Broker {
         newListener({
           data: this.mostRecentMessages[routingKey]!.value as ResponseCallbackParamsData<T>,
           status: this.mostRecentMessages[routingKey]!.status,
+          callID: this.mostRecentMessages[routingKey]!.callID,
           routingKey: routingKey,
           defaultCallback: () => true,
         });
@@ -83,7 +84,7 @@ export class Broker {
    * @param {string} routingKey The routingkey to subscribe to.
    * @param {boolean} consumeMostRecentMessage If true and there is a message for this routingkey available, notify the new listener. Default true.
    */
-  public subscribeDefault(newListener: ResponseCallback<wsReturnKeyWithML>): void {
+  public subscribeDefault(newListener: ResponseCallback<any>): void {
     this.catchAllListener = newListener;
   }
 
@@ -198,17 +199,24 @@ export class Broker {
     setTimeout(() => Broker.instance().attemptReconnect(), 5000);
   }
 
-  public sendMessage<R extends wsReturnKeyWithML>(message: Omit<WsMessageBody, 'callID'>, callback?: ResponseCallback<R>): void {
+  generateCallID() {
+    return uuidv4();
+  }
+
+  public sendMessage<R extends wsReturnKeyWithML>(
+    message: Omit<WsMessageBody, 'callID'> & { callID?: string },
+    callback?: ResponseCallback<R>,
+  ): void {
     console.debug('%cSending WS message: ', 'background: #222; color: #bada55', message);
 
-    const uuid = uuidv4();
+    message.callID = message.callID ?? this.generateCallID();
     const fullMessage = {
       ...message,
-      callID: uuid,
+      callID: message.callID,
     };
 
     if (callback) {
-      this.callbackListeners[uuid] = callback;
+      this.callbackListeners[message.callID] = callback;
     }
 
     if (message.body) {
@@ -233,22 +241,23 @@ export class Broker {
     const routingKey = jsonObject.type;
     const data = jsonObject.value;
     const status = jsonObject.status;
-    const uuid = jsonObject.callID;
+    const callID = jsonObject.callID;
 
     this.mostRecentMessages[routingKey] = jsonObject;
-    if (uuid in this.callbackListeners) {
+    if (callID in this.callbackListeners) {
       // If there is a callback listener, notify it, and not the others unless the callback listener says so
-      this.callbackListeners[uuid]({
+      this.callbackListeners[callID]({
         data,
         status,
         routingKey,
+        callID,
         defaultCallback: () => {
           if (this.listeners[routingKey] && Object.keys(this.listeners[routingKey]).length != 0) {
             if (this.catchAllListener) {
-              this.catchAllListener({ data, status, routingKey, defaultCallback: () => true });
+              this.catchAllListener({ data, status, routingKey, callID, defaultCallback: () => true });
             }
             Object.values(this.listeners[routingKey]).forEach(listener =>
-              listener({ data, status, routingKey, defaultCallback: () => true }),
+              listener({ data, status, routingKey, callID, defaultCallback: () => true }),
             );
             console.debug('%c' + routingKey + ` WS response`, 'background: #222; color: #DBAB2F', data);
           }
@@ -262,18 +271,20 @@ export class Broker {
         'stop=',
         stop,
       );
-      delete this.callbackListeners[uuid];
+      delete this.callbackListeners[callID];
     } else if (this.listeners[routingKey] && Object.keys(this.listeners[routingKey]).length != 0) {
       // If there are listeners, notify them
       if (this.catchAllListener) {
-        this.catchAllListener({ data, status, routingKey, defaultCallback: () => true });
+        this.catchAllListener({ data, status, routingKey, callID, defaultCallback: () => true });
       }
-      Object.values(this.listeners[routingKey]).forEach(listener => listener({ data, status, routingKey, defaultCallback: () => true }));
+      Object.values(this.listeners[routingKey]).forEach(listener =>
+        listener({ data, status, routingKey, callID, defaultCallback: () => true }),
+      );
       console.debug('%c' + routingKey + ` WS response`, 'background: #222; color: #DBAB2F', data);
     } else {
       // If there are no listeners, log the message
       if (this.catchAllListener) {
-        this.catchAllListener({ data, status, routingKey, defaultCallback: () => true });
+        this.catchAllListener({ data, status, routingKey, callID, defaultCallback: () => true });
         console.debug(routingKey, `catch all used for message with routing key`, data);
       } else {
         console.debug('%c' + routingKey + ` no listeners for message with routing key`, 'background: #663322; color: #DBAB2F', data);
diff --git a/src/lib/data-access/broker/wsQuery.ts b/src/lib/data-access/broker/wsQuery.ts
index e3f9f4202..880ccae4b 100644
--- a/src/lib/data-access/broker/wsQuery.ts
+++ b/src/lib/data-access/broker/wsQuery.ts
@@ -3,7 +3,13 @@
 import { Broker } from './broker';
 import { ML, Query, ResponseCallback, WsFrontendCall, wsKeys, wsReturnKey, wsSubKeys } from 'ts-common';
 
-export const wsQueryRequest: WsFrontendCall<{ saveStateID: string; ml: ML; queryID: number; useCached: boolean }> = params => {
+export const wsQueryRequest: WsFrontendCall<{
+  saveStateID: string;
+  ml: ML;
+  queryID: number;
+  useCached: boolean;
+  callID: string;
+}> = params => {
   const mlEnabled = Object.entries(params.ml)
     .filter(([_, value]) => value.enabled)
     .map(([, value]) => value);
@@ -16,6 +22,7 @@ export const wsQueryRequest: WsFrontendCall<{ saveStateID: string; ml: ML; query
       ml: mlEnabled,
       useCached: params.useCached,
     },
+    callID: params.callID,
   });
 };
 export const wsQueryErrorSubscription = (callback: ResponseCallback<wsReturnKey.queryStatusError>) => {
@@ -117,3 +124,10 @@ export function wsQuerySubscription(callback: ResponseCallback<wsReturnKey.query
     Broker.instance().unSubscribe(wsReturnKey.queryStatusResult, id);
   };
 }
+
+export function wsQueryCountSubscription(callback: ResponseCallback<wsReturnKey.queryCountResult>) {
+  const id = Broker.instance().subscribe(callback, wsReturnKey.queryCountResult);
+  return () => {
+    Broker.instance().unSubscribe(wsReturnKey.queryCountResult, id);
+  };
+}
diff --git a/src/lib/data-access/store/graphQueryResultSlice.ts b/src/lib/data-access/store/graphQueryResultSlice.ts
index cddd35bde..fb04f9948 100755
--- a/src/lib/data-access/store/graphQueryResultSlice.ts
+++ b/src/lib/data-access/store/graphQueryResultSlice.ts
@@ -1,7 +1,6 @@
 import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 import type { AppDispatch, RootState } from './store';
-import { GraphQueryResultMetaFromBackend, NodeAttributes, QueryStatusResult } from 'ts-common';
-import { setQueryNodeCounts } from './sessionSlice';
+import { GraphQueryCountResultFromBackend, GraphQueryResultMetaFromBackend, NodeAttributes, QueryStatusResult } from 'ts-common';
 export type UniqueEdge = {
   attributes: NodeAttributes;
   source: string;
@@ -9,24 +8,29 @@ export type UniqueEdge = {
   count: number;
 };
 
+export type GraphQueryResultStatus = 'idle' | 'error' | 'querying' | 'success' | 'aborted' | 'countAborted';
 // Define a type for the slice state
-export type GraphQueryResult = GraphQueryResultMetaFromBackend & {
-  queryingBackend: boolean;
-  error: boolean;
+export type GraphQueryResult = {
+  graph: GraphQueryResultMetaFromBackend;
+  graphCounts?: GraphQueryCountResultFromBackend;
+  status: GraphQueryResultStatus;
+  currentQueryCallID: string;
 };
 
 // Define the initial state using that type
 export const initialState: GraphQueryResult = {
-  metaData: {
-    nodes: { count: 0, labels: [], types: {} },
-    edges: { count: 0, labels: [], types: {} },
-    topological: { density: 0, self_loops: 0 },
+  graph: {
+    metaData: {
+      nodes: { count: 0, labels: [], types: {} },
+      edges: { count: 0, labels: [], types: {} },
+      topological: { density: 0, self_loops: 0 },
+    },
+    nodes: [],
+    edges: [],
   },
-  nodes: [],
-  edges: [],
-  queryingBackend: false,
-  error: false,
-  nodeCounts: { updatedAt: 0 },
+  graphCounts: undefined,
+  status: 'idle',
+  currentQueryCallID: '',
 };
 
 export function parseValue(value: number | string | Array<any> | object) {
@@ -48,36 +52,66 @@ export const graphQueryResultSlice = createSlice({
   name: 'graphQueryResult',
   initialState,
   reducers: {
-    setNewGraphQueryResult: (state, action: PayloadAction<QueryStatusResult>) => {
-      // Assign new state
-      state.metaData = action.payload.result.payload.metaData;
-      state.nodes = action.payload.result.payload.nodes;
-      state.edges = action.payload.result.payload.edges;
-      state.nodeCounts = action.payload.result.payload.nodeCounts;
-      state.queryingBackend = false;
-      state.error = false;
+    setNewGraphQueryResult: (
+      state,
+      action: PayloadAction<{ data: QueryStatusResult<GraphQueryResultMetaFromBackend>; callID: string }>,
+    ) => {
+      if (state.status !== 'querying') {
+        console.log('Set new graph query result called when not querying', state.status);
+        return;
+      }
+      if (state.currentQueryCallID !== action.payload.callID) {
+        console.log('Set new graph query result called with wrong callID', state.currentQueryCallID, action.payload.callID);
+        return;
+      }
+
+      state.graph = action.payload.data.result.payload;
+      state.status = 'success';
+      state.graphCounts = undefined;
+    },
+    setNewGraphQueryCountResult: (
+      state,
+      action: PayloadAction<{ data: QueryStatusResult<GraphQueryCountResultFromBackend>; callID: string }>,
+    ) => {
+      if (state.status !== 'success') {
+        console.log("Set new count graph query result called when the normal result set isn't there yet", state.status);
+        return;
+      }
+      if (state.currentQueryCallID !== action.payload.callID) {
+        console.log('Set new count graph query result called with wrong callID', state.currentQueryCallID, action.payload.callID);
+        return;
+      }
+
+      state.graphCounts = action.payload.data.result.payload;
+    },
+    resetGraphQueryResults: (state, action: PayloadAction<undefined | { status: GraphQueryResultStatus }>) => {
+      state.graph = initialState.graph;
+      state.graphCounts = undefined;
+      state.status = action.payload?.status ?? 'success';
     },
-    resetGraphQueryResults: (state, action: PayloadAction<undefined | { error: boolean }>) => {
-      // Assign new state
-      state.metaData = {
-        nodes: { count: 0, labels: [], types: {} },
-        edges: { count: 0, labels: [], types: {} },
-        topological: { density: 0, self_loops: 0 },
-      };
-      state.nodes = [];
-      state.edges = [];
-      state.nodeCounts = { updatedAt: 0 };
-      state.queryingBackend = false;
-      state.error = action.payload?.error ?? false;
+    queryingBackend: (state, action: PayloadAction<{ status: GraphQueryResultStatus; callID: string }>) => {
+      state.status = action.payload.status ?? 'querying';
+      state.currentQueryCallID = action.payload.callID;
     },
-    queryingBackend: (state, action: PayloadAction<boolean | undefined>) => {
-      state.queryingBackend = action.payload ?? true;
-      state.error = false;
+    abortedBackend: (state, action: PayloadAction<{ type: 'result' | 'count'; callID: string }>) => {
+      if (state.status !== 'querying' || state.currentQueryCallID !== action.payload.callID) {
+        console.debug('Aborted backend call does not match current call', state.status, state.currentQueryCallID, action.payload.callID);
+        return;
+      }
+      if (action.payload.type === 'result') {
+        state.graph = initialState.graph;
+        state.graphCounts = undefined;
+        state.status = 'aborted';
+      }
+      if (action.payload.type === 'count') {
+        state.status = 'countAborted';
+        state.graphCounts = undefined;
+      }
     },
   },
 });
 
-export const { resetGraphQueryResults, queryingBackend } = graphQueryResultSlice.actions;
+export const { resetGraphQueryResults, queryingBackend, setNewGraphQueryCountResult, abortedBackend } = graphQueryResultSlice.actions;
 
 /**
  * Asynchronously sets a new graph query result.
@@ -90,15 +124,19 @@ export const { resetGraphQueryResults, queryingBackend } = graphQueryResultSlice
  * @param payload - The result of the graph query to be set.
  * @param dispatcher - The dispatch function to trigger the action.
  */
-export const asyncSetNewGraphQueryResult = async (payload: QueryStatusResult, dispatcher: AppDispatch) => {
-  dispatcher(setQueryNodeCounts(payload.result.payload.nodeCounts));
+export const asyncSetNewGraphQueryResult = async (
+  payload: { data: QueryStatusResult<GraphQueryResultMetaFromBackend>; callID: string },
+  dispatcher: AppDispatch,
+) => {
   dispatcher(graphQueryResultSlice.actions.setNewGraphQueryResult(payload));
 };
 
 // Other code such as selectors can use the imported `RootState` type
-export const selectGraphQueryResult = (state: RootState) => state.graphQueryResult;
-export const selectGraphQueryResultNodes = (state: RootState) => state.graphQueryResult.nodes;
-export const selectGraphQueryResultLinks = (state: RootState) => state.graphQueryResult.edges;
-export const selectGraphQueryResultMetaData = (state: RootState) => state.graphQueryResult.metaData;
+export const selectGraphQuery = (state: RootState) => state.graphQueryResult;
+export const selectGraphQueryResult = (state: RootState) => state.graphQueryResult.graph;
+export const selectGraphQueryResultNodes = (state: RootState) => state.graphQueryResult.graph.nodes;
+export const selectGraphQueryResultLinks = (state: RootState) => state.graphQueryResult.graph.edges;
+export const selectGraphQueryResultMetaData = (state: RootState) => state.graphQueryResult.graph.metaData;
+export const selectGraphQueryCounts = (state: RootState) => state.graphQueryResult.graphCounts;
 
 export default graphQueryResultSlice.reducer;
diff --git a/src/lib/data-access/store/hooks.ts b/src/lib/data-access/store/hooks.ts
index 6141da472..fab5de314 100644
--- a/src/lib/data-access/store/hooks.ts
+++ b/src/lib/data-access/store/hooks.ts
@@ -1,5 +1,5 @@
 import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
-import { GraphQueryResult, selectGraphQueryResult, selectGraphQueryResultMetaData } from './graphQueryResultSlice';
+import { GraphQueryResult, selectGraphQuery, selectGraphQueryResult, selectGraphQueryResultMetaData } from './graphQueryResultSlice';
 import { schemaGraph, selectSchemaLayout, schemaSettingsState, schemaStatsState, SchemaSliceI, schema } from './schemaSlice';
 import type { RootState, AppDispatch } from './store';
 import { ConfigStateI, configState } from '@/lib/data-access/store/configSlice';
@@ -32,9 +32,9 @@ import {
   SaveStateAuthorizationHeaders,
   Query,
   SchemaSettings,
-  SchemaStatsFromBackend,
   SchemaGraphStats,
   SaveStateWithAuthorization,
+  GraphQueryResultMetaFromBackend,
 } from 'ts-common';
 import { ProjectState, selectProject } from './projectSlice';
 import { AllLayoutAlgorithms } from 'ts-common/src/model/layouts';
@@ -45,7 +45,8 @@ export const useAppDispatch: () => AppDispatch = useDispatch;
 export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
 
 /** Gives the graphQueryResult from the store */
-export const useGraphQueryResult: () => GraphQueryResult = () => useAppSelector(selectGraphQueryResult);
+export const useGraphQuery: () => GraphQueryResult = () => useAppSelector(selectGraphQuery);
+export const useGraphQueryResult: () => GraphQueryResultMetaFromBackend = () => useAppSelector(selectGraphQueryResult);
 export const useGraphQueryResultMeta: () => GraphStatistics | undefined = () => useAppSelector(selectGraphQueryResultMetaData);
 
 // Gives the schema
diff --git a/src/lib/data-access/store/sessionSlice.ts b/src/lib/data-access/store/sessionSlice.ts
index cf7d308b0..1888e3f12 100644
--- a/src/lib/data-access/store/sessionSlice.ts
+++ b/src/lib/data-access/store/sessionSlice.ts
@@ -128,8 +128,8 @@ export const sessionSlice = createSlice({
       if (activeQuery) {
         activeQuery.graph = {
           ...action.payload,
-          nodeCounts: activeQuery.graph.nodeCounts, // ensure visual query building does not get rid of statistics information from the backend
         };
+        activeQuery.graphCounts = { nodeCounts: { updatedAt: 0 } };
       }
     },
     setQuerybuilderSettings: (state: SessionCacheI, action: PayloadAction<QueryBuilderSettings>) => {
diff --git a/src/lib/management/database/Databases.tsx b/src/lib/management/database/Databases.tsx
index 78b3b14e3..2f5c71f81 100644
--- a/src/lib/management/database/Databases.tsx
+++ b/src/lib/management/database/Databases.tsx
@@ -252,37 +252,38 @@ export function Databases({ onClose, saveStates, changeActive, setSelectedSaveSt
               onSelect={() => {
                 if (key !== session.currentSaveState) {
                   onClose();
-                }
-              }}
-              onUpdate={() => {
-                changeActive('update');
-                setSelectedSaveState(saveStates[key]);
-              }}
-              onClone={() => {
-                databaseHandler.submitDatabaseChange(
-                  { ...saveStates[key], name: saveStates[key].name + ' (copy)', id: nilUUID },
-                  'add',
-                  true,
-                  () => {},
-                );
-                setSelectedSaveState(saveStates[key]);
-              }}
-              onDelete={() => {
-                if (session.currentSaveState === key) {
-                  dispatch(clearQB());
-                  dispatch(clearSchema());
-                }
-                wsDeleteState({ saveStateID: key });
-                dispatch(deleteSaveState(key));
-              }}
-              onShare={async () => {
-                setSharing(true);
-                await auth.shareLink();
-                setSharing(false);
-              }}
-              showDifferentiation={showDifferentiation}
-              showQueries={showQueries}
-            />
+                  }
+                }}
+                onUpdate={() => {
+                  changeActive('update');
+                  setSelectedSaveState(saveStates[key]);
+                }}
+                onClone={() => {
+                  databaseHandler.submitDatabaseChange(
+                    { ...saveStates[key], name: saveStates[key].name + ' (copy)', id: nilUUID },
+                    'add',
+                    true,
+                    () => {},
+                  );
+                  setSelectedSaveState(saveStates[key]);
+                }}
+                onDelete={() => {
+                  if (session.currentSaveState === key) {
+                    dispatch(clearQB());
+                    dispatch(clearSchema());
+                  }
+                  wsDeleteState({ saveStateID: key });
+                  dispatch(deleteSaveState(key));
+                }}
+                onShare={async () => {
+                  setSharing(true);
+                  await auth.shareLink();
+                  setSharing(false);
+                }}
+                showDifferentiation={showDifferentiation}
+                showQueries={showQueries}
+              />
+
           ))}
         </tbody>
       </table>
diff --git a/src/lib/querybuilder/panel/QueryBuilderNav.tsx b/src/lib/querybuilder/panel/QueryBuilderNav.tsx
index f8bc37f9b..6f313f0d6 100644
--- a/src/lib/querybuilder/panel/QueryBuilderNav.tsx
+++ b/src/lib/querybuilder/panel/QueryBuilderNav.tsx
@@ -1,7 +1,12 @@
 import { useEffect, useMemo, useRef, useState } from 'react';
 import { ControlContainer, TooltipProvider, Tooltip, TooltipTrigger, Button, TooltipContent, Input } from '@/lib/components';
 import { Popover, PopoverTrigger, PopoverContent } from '@/lib/components/popover';
-import { useActiveQuery, useActiveSaveState, useAppDispatch, useGraphQueryResult, useML } from '../../data-access';
+import { useActiveQuery, useActiveSaveState,
+  useAppDispatch,
+  useGraphQuery,
+  useGraphQueryCounts,
+  useGraphQueryResult,
+  useML} from '../../data-access';
 import {
   clearQB,
   setQuerybuilderSettings,
@@ -34,20 +39,20 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => {
   const dispatch = useAppDispatch();
   const activeQuery = useActiveQuery();
   const ss = useActiveSaveState();
-  const result = useGraphQueryResult();
+  const graphQuery = useGraphQuery();
   const resultSize = useMemo(() => {
-    if (!result) return 0;
-    return result.nodes.length;
-  }, [result]);
+    if (!graphQuery) return 0;
+    return graphQuery.graph.nodes.length;
+  }, [graphQuery]);
   const totalSize = useMemo(() => {
-    if (!activeQuery || !result || !ss) return 0;
+    if (!activeQuery || !graphQuery || !ss) return 0;
 
     const nodeCounts = activeQuery.graph.nodes
       .filter(x => x.attributes.type == 'entity')
-      .map(x => result.nodeCounts[`${x.key}_count`] ?? 0);
+      .map(x => graphQuery.graphCounts?.nodeCounts[`${x.key}_count`] ?? 0);
 
     return nodeCounts.reduce((a, b) => a + b, 0);
-  }, [result]);
+  }, [graphQuery]);
   const ml = useML();
   const [editingIdx, setEditingIdx] = useState<{ idx: number; text: string } | null>(null);
 
@@ -155,10 +160,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => {
           </Tooltip>
         </TooltipProvider>
       </div>
-      <Tabs
-        ref={tabsRef}
-        className={`-my-px overflow-x-auto overflow-y-hidden no-scrollbar divide-x divide-secondary-200 border-x ${result.queryingBackend ? 'pointer-events-none' : ''}`}
-      >
+      <Tabs ref={tabsRef} className={`-my-px overflow-x-auto overflow-y-hidden no-scrollbar divide-x divide-secondary-200 border-x`}>
         {ss.queryStates.openQueryArray
           .filter(query => query.id != null)
           .sort((a, b) => {
diff --git a/src/lib/vis/components/VisualizationPanel.tsx b/src/lib/vis/components/VisualizationPanel.tsx
index 049ec1417..b668570ab 100644
--- a/src/lib/vis/components/VisualizationPanel.tsx
+++ b/src/lib/vis/components/VisualizationPanel.tsx
@@ -3,6 +3,7 @@ import {
   useActiveQuery,
   useActiveSaveState,
   useAppDispatch,
+  useGraphQuery,
   useGraphQueryResult,
   useGraphQueryResultMeta,
   useML,
@@ -38,14 +39,12 @@ export const Visualizations: Record<string, PromiseFunc> = {
 export const VISUALIZATION_TYPES: string[] = Object.keys(Visualizations);
 
 export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => {
-  const activeQuery = useActiveQuery();
-  const graphQueryResult = useGraphQueryResult();
+  const graphQuery = useGraphQuery();
   const activeSaveState = useActiveSaveState();
   const dispatch = useAppDispatch();
   const { activeVisualizationIndex, openVisualizationArray } = useVisualization();
   const ml = useML();
   const schema = useSchemaGraph();
-  const graphMetadata = useGraphQueryResultMeta();
   const [viz, setViz] = useState<{ component: React.FC<VisualizationPropTypes>; id: string; exportImage: () => void } | undefined>(
     undefined,
   );
@@ -98,10 +97,10 @@ export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => {
 
   return (
     <div className="relative pt-7 vis-panel h-full w-full flex flex-col border bg-light">
-      <div className="grow overflow-y-auto" style={graphQueryResult.nodes.length === 0 ? { overflow: 'hidden' } : {}}>
-        {graphQueryResult.queryingBackend ? (
+      <div className="grow overflow-y-auto" style={graphQuery.graph.nodes.length === 0 ? { overflow: 'hidden' } : {}}>
+        {graphQuery.status === 'querying' ? (
           <Querying />
-        ) : graphQueryResult.nodes.length === 0 ? (
+        ) : graphQuery.graph.nodes.length === 0 ? (
           <NoData />
         ) : openVisualizationArray.length === 0 ? (
           <Recommender />
@@ -118,15 +117,15 @@ export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => {
                 activeVisualizationIndex !== -1 &&
                 openVisualizationArray?.[activeVisualizationIndex] &&
                 viz.id === openVisualizationArray[activeVisualizationIndex].id &&
-                graphMetadata && (
+                graphQuery.graph.metaData && (
                   <viz.component
-                    data={graphQueryResult}
+                    data={graphQuery.graph}
                     schema={schema}
                     ml={ml}
                     settings={openVisualizationArray[activeVisualizationIndex]}
                     dispatch={dispatch}
                     handleSelect={handleSelect}
-                    graphMetadata={graphMetadata}
+                    graphMetadata={graphQuery.graph.metaData}
                     updateSettings={updateSettings}
                     handleHover={() => {}}
                   />
diff --git a/src/lib/vis/views/NoData.tsx b/src/lib/vis/views/NoData.tsx
index 0454d47aa..8d30fdcb7 100644
--- a/src/lib/vis/views/NoData.tsx
+++ b/src/lib/vis/views/NoData.tsx
@@ -1,31 +1,38 @@
-import React from 'react';
 import { Button } from '../../components';
-import { useActiveQuery, useGraphQueryResult, useSessionCache } from '@/lib/data-access';
+import { useActiveQuery, useGraphQuery, useSessionCache } from '@/lib/data-access';
 
 export function NoData() {
   const activeQuery = useActiveQuery();
   const session = useSessionCache();
-  const graphResult = useGraphQueryResult();
+  const graph = useGraphQuery();
   const dataAvailable = !activeQuery || activeQuery.graph.nodes.length > 0;
 
   return (
     <div className="flex justify-center items-center h-full">
       <div className="max-w-lg mx-auto text-left">
-        <p className="text-xl font-normal text-secondary-600">No data available to be shown</p>
-        {graphResult.error ? (
+        {graph.status === 'error' ? (
           <div className="m-3 self-center text-center flex h-full flex-col justify-center">
             <p className="text-xl font-bold text-error">An error occurred while fetching data!</p>
             <p className="">Please retry or contact your Database's Administrator</p>
           </div>
+        ) : graph.status === 'aborted' ? (
+          <div className="m-3 self-center text-center flex h-full flex-col justify-center">
+            <p className="text-xl font-bold text-error">Your query was aborted</p>
+            <p className="">Please retry or contact your Database's Administrator</p>
+          </div>
         ) : !session.currentSaveState ? (
           <p>
             <span>No database selected. </span>
             <span>Please select a database using the database selector on the top left of your screen.</span>
           </p>
         ) : dataAvailable ? (
-          <p>Query resulted in empty dataset</p>
+          <div>
+            <p className="text-xl font-normal text-secondary-600">No data available to be shown</p>
+            <p>Query resulted in empty dataset</p>
+          </div>
         ) : (
           <div>
+            <p className="text-xl font-normal text-secondary-600">No data available to be shown</p>
             <p>Query for data to visualize</p>
             <Button
               variantType="primary"
-- 
GitLab