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