diff --git a/src/app/navbar/navbar.tsx b/src/app/navbar/navbar.tsx index 0859d9bb49e1e67d9b9849a80c7aae86999084b1..230411c2ca029ed5fc68f6497e5e092008b3459e 100644 --- a/src/app/navbar/navbar.tsx +++ b/src/app/navbar/navbar.tsx @@ -13,7 +13,7 @@ import React, { useState, useRef, useEffect } from 'react'; import GpLogo from './gp-logo'; import { getEnvVariable } from '@/config'; -import { useAuthentication, useAuthCache, useActiveSaveStateAuthorization, Button, DropdownItem, useAppDispatch } from '@/lib'; +import { useAuthentication, useAuthCache, Button, DropdownItem, useAppDispatch, useActiveSaveState } from '@/lib'; import { FeatureEnabled } from '@/lib/components/featureFlags'; import { Popover, PopoverTrigger, PopoverContent } from '@/lib/components/popover'; import { ManagementViews, ManagementTrigger } from '@/lib/management'; @@ -24,7 +24,7 @@ export const Navbar = () => { const dropdownRef = useRef<HTMLDivElement>(null); const auth = useAuthentication(); const authCache = useAuthCache(); - const authorization = useActiveSaveStateAuthorization(); + const ss = useActiveSaveState(); const [menuOpen, setMenuOpen] = useState(false); const buildInfo = getEnvVariable('GRAPHPOLARIS_VERSION'); const [managementOpen, setManagementOpen] = useState<boolean>(false); @@ -106,7 +106,7 @@ export const Navbar = () => { /> </FeatureEnabled> <FeatureEnabled featureFlag="VIEWER_PERMISSIONS"> - {authCache.authorization?.savestate?.W && authorization.database?.W && ( + {authCache.authorization?.savestate?.W && ss?.authorization.database?.W && ( <DropdownItem value="Viewer Permissions" onClick={() => { diff --git a/src/lib/components/dropdowns/index.tsx b/src/lib/components/dropdowns/index.tsx index b3d667f8030bc1db28a206e1888faabfb0dad88c..d18577ba209558540cc76c1a43e677d228c60c95 100644 --- a/src/lib/components/dropdowns/index.tsx +++ b/src/lib/components/dropdowns/index.tsx @@ -63,7 +63,14 @@ export function DropdownTrigger({ onClick(); } }; - if (popover) { + + if (disabled) { + return ( + <button className={`w-full ${disabled ? 'cursor-not-allowed opacity-50' : ''}`} disabled={disabled}> + {inner} + </button> + ); + } else if (popover) { return ( <PopoverTrigger className={`${disabled ? 'cursor-not-allowed opacity-50' : ''}`} onClick={handleClick}> {inner} diff --git a/src/lib/data-access/api/eventBus.tsx b/src/lib/data-access/api/eventBus.tsx index 9c15efe9f3b30c45cb768ecb1fb333b41d623d17..95857ef2afa589063862f80937f96cd9dc412ff6 100644 --- a/src/lib/data-access/api/eventBus.tsx +++ b/src/lib/data-access/api/eventBus.tsx @@ -27,23 +27,21 @@ import { wsDeleteStateSubscription, wsGetStateSubscription, wsTestSaveStateConnectionSubscription, - wsStateGetPolicy, DatabaseStatus, wsTestSaveStateConnection, } from '../broker/wsState'; import { addSaveState, testedSaveState, - selectSaveState, updateSaveStateList, updateSelectedSaveState, setFetchingSaveStates, - setStateAuthorization, setActiveQueryID, setQueryState, setQuerybuilderNodes, + selectSaveState, } from '../store/sessionSlice'; -import { URLParams, getParam } from './url'; +import { URLParams, getParam, setParam } from './url'; import { VisState, setVisualizationState } from '../store/visualizationSlice'; import { isEqual } from 'lodash-es'; import { clearSchema, setSchemaAttributeInformation, setSchemaLoading, setSchemaState } from '../store/schemaSlice'; @@ -52,15 +50,7 @@ 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, - SchemaGraphStats, - allMLTypes, - LinkPredictionInstance, - GraphQueryTranslationResultMessage, - wsReturnKey, - Schema, -} from 'ts-common'; +import { SaveState, allMLTypes, LinkPredictionInstance, GraphQueryTranslationResultMessage, wsReturnKey, Schema } from 'ts-common'; export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAuthorized: () => void }) => { const { login } = useAuthentication(); @@ -129,7 +119,7 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu const unsubs: (() => void)[] = []; unsubs.push( - wsSchemaSubscription((data, status) => { + wsSchemaSubscription(({ data, status }) => { if (status !== 'success') { dispatch(addError('Failed to fetch schema')); dispatch(clearSchema({ error: true })); @@ -146,8 +136,8 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu ); unsubs.push( - wsQuerySubscription(data => { - if (!data) { + wsQuerySubscription(({ data, status }) => { + if (!data || status !== 'success') { dispatch(addError('Failed to fetch graph query result')); return; } @@ -158,20 +148,21 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu ); unsubs.push( - wsQueryErrorSubscription(data => { + wsQueryErrorSubscription(params => { dispatch(addError('Failed to fetch graph query result')); - console.error('Query Error', data); + console.error('Query Error', params.data); dispatch(resetGraphQueryResults()); }), ); unsubs.push( - wsTestSaveStateConnectionSubscription(data => { + wsTestSaveStateConnectionSubscription(params => { + const data = params.data; if (data?.status === 'success' && data.saveStateID) { dispatch( testedSaveState({ saveStateID: data.saveStateID, - status: data.status == 'success' ? DatabaseStatus.online : DatabaseStatus.offline, + status: data.status === 'success' ? DatabaseStatus.online : DatabaseStatus.offline, }), ); } else { @@ -181,7 +172,8 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu ); unsubs.push( - wsReconnectSubscription(data => { + wsReconnectSubscription(params => { + const data = params.data; if (data == null) return; if (!Broker.instance().checkSessionID(data.sessionID)) { @@ -196,13 +188,15 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu dispatch(setFetchingSaveStates(true)); // check authorizations - wsUserGetPolicy(null, res => { - if (res) dispatch(authorized(res)); + wsUserGetPolicy(null, params => { + if (params.data) dispatch(authorized(params.data)); }); - wsGetStates(null, states => { + wsGetStates(null, params => { + const states = params.data; if (states) { dispatch(setFetchingSaveStates(false)); + params.defaultCallback(); // Process URL Params after states are fetched const paramSaveState = getParam(URLParams.saveState); @@ -211,13 +205,26 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu const stateExists = states.some(state => state.id === paramSaveState); if (!stateExists) { console.log('Shared save state not found in user states. Fetching:', paramSaveState); - wsGetState({ saveStateID: paramSaveState }); + wsGetState({ saveStateID: paramSaveState }, ({ data, status }) => { + if (!['success', 'unchanged'].includes(status) || !data) { + dispatch(addError('Failed to fetch state')); + return; + } + if (data.id !== nilUUID) { + if (isEqual(data, session.saveStates[data.id])) { + console.log('State unchanged, not updating'); + return; + } + console.log('State fetched', data, status); + dispatch(addSaveState({ ss: data, select: true })); + } + }); + } else { + dispatch(selectSaveState(paramSaveState)); } } - return true; } else { dispatch(addError('Failed to fetch states')); - return false; } }); @@ -229,47 +236,39 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu }), ); - Broker.instance().subscribe((data: SchemaGraphStats) => { - dispatch(setSchemaAttributeInformation(data)); + Broker.instance().subscribe(params => { + if (!params.data) { + dispatch(addError('Failed to fetch schema stats')); + return; + } + dispatch(setSchemaAttributeInformation(params.data)); dispatch(addInfo('Received attribute information')); }, wsReturnKey.schemaStatsResult); // Broker.instance().subscribe((data: QueryBuilderState) => dispatch(setQuerybuilderNodes(data)), 'query_builder_state'); allMLTypes.forEach(mlType => { - const id = Broker.instance().subscribe((data: LinkPredictionInstance[]) => { + const id = Broker.instance().subscribe(params => { + const data = params.data as unknown as LinkPredictionInstance[]; dispatch(setMLResult({ type: mlType, result: data })); }, mlType); unsubs.push(() => Broker.instance().unSubscribe(mlType, id)); }); unsubs.push( - wsGetStatesSubscription((data, status) => { - console.debug('Save States updated', data, status); - if (status !== 'success' || !data) { + wsGetStatesSubscription(params => { + console.debug('Save States updated', params.data, params.status); + if (params.status !== 'success' || !params.data) { dispatch(addError('Failed to fetch states')); return; } - dispatch(updateSaveStateList(data)); - const d = Object.fromEntries(data.map(x => [x.id, x])); - loadSaveState(session.currentSaveState, d); - data.forEach(ss => { - if (session.saveStatesAuthorization?.[ss.id] === undefined) { - wsStateGetPolicy({ saveStateID: ss.id }, ret => { - if (!ret) { - dispatch(addError('Failed to fetch state authorization')); - return; - } - dispatch(setStateAuthorization({ id: ss.id, authorization: ret })); - }); - } - }); + dispatch(updateSaveStateList(params.data)); }), ); unsubs.push( - wsGetStateSubscription((data, status) => { + wsGetStateSubscription(({ data, status }) => { if (!['success', 'unchanged'].includes(status) || !data) { dispatch(addError('Failed to fetch state')); return; @@ -279,16 +278,8 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu console.log('State unchanged, not updating'); return; } - dispatch(addSaveState(data)); - dispatch(selectSaveState(data.id)); - loadSaveState(data.id, session.saveStates); - wsStateGetPolicy({ saveStateID: data.id }, ret => { - if (!ret) { - dispatch(addError('Failed to fetch state authorization')); - return; - } - dispatch(setStateAuthorization({ id: data.id, authorization: ret })); - }); + console.log('State fetched', data, status); + dispatch(addSaveState({ ss: data, select: false })); } }), ); @@ -316,7 +307,7 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu useEffect(() => { if (activeSS) { console.log('Query State Hash Updated', queryHash, activeSS.id); - wsUpdateState(activeSS, (data, status) => { + wsUpdateState(activeSS, ({ data, status }) => { // if (status === 'unchanged') { // TODO: check if this is needed, but for now removed so that the query is always sent, solving issue with changing ss // console.log('State unchanged, not updating'); diff --git a/src/lib/data-access/broker/broker.tsx b/src/lib/data-access/broker/broker.tsx index 42afae9ed1a4d3a3adcb68548891b1bce9573b2f..e9d42179991ec2dab440b3585741db0c6ff27426 100644 --- a/src/lib/data-access/broker/broker.tsx +++ b/src/lib/data-access/broker/broker.tsx @@ -6,11 +6,9 @@ import { getEnvVariable } from '@/config'; import { UseIsAuthorizedState } from '../store/authSlice'; -import { WsMessageBackend2Frontend, WsMessageBody, wsReturnKeyWithML } from 'ts-common'; +import { ResponseCallback, ResponseCallbackParamsData, WsMessageBackend2Frontend, WsMessageBody, wsReturnKeyWithML } from 'ts-common'; import { v4 as uuidv4 } from 'uuid'; -export type BrokerCallbackFunction = (data: any, status: string, routingKey?: string) => boolean | void; - let keepAlivePing: ReturnType<typeof setInterval>; /** @@ -29,9 +27,9 @@ let keepAlivePing: ReturnType<typeof setInterval>; export class Broker { private static singletonInstance: Broker; - private listeners: Record<string, Record<string, BrokerCallbackFunction>> = {}; - private catchAllListener: BrokerCallbackFunction | undefined; - private callbackListeners: Record<string, BrokerCallbackFunction> = {}; + private listeners: Record<string, Record<string, ResponseCallback<wsReturnKeyWithML>>> = {}; + private catchAllListener: ResponseCallback<wsReturnKeyWithML> | undefined; + private callbackListeners: Record<string, ResponseCallback<wsReturnKeyWithML>> = {}; private webSocket: WebSocket | undefined; private url: string; @@ -53,9 +51,9 @@ 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 subscribe( - newListener: BrokerCallbackFunction, - routingKey: wsReturnKeyWithML, + public subscribe<T extends wsReturnKeyWithML>( + newListener: ResponseCallback<T>, + routingKey: T, key: string = (Date.now() + Math.floor(Math.random() * 100)).toString(), consumeMostRecentMessage: boolean = false, ): string { @@ -67,7 +65,12 @@ export class Broker { // Consume the most recent message if (consumeMostRecentMessage && routingKey in this.mostRecentMessages) { - newListener(this.mostRecentMessages[routingKey]!.value, this.mostRecentMessages[routingKey]!.status, routingKey); + newListener({ + data: this.mostRecentMessages[routingKey]!.value as ResponseCallbackParamsData<T>, + status: this.mostRecentMessages[routingKey]!.status, + routingKey: routingKey, + defaultCallback: () => true, + }); } } @@ -80,7 +83,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: BrokerCallbackFunction): void { + public subscribeDefault(newListener: ResponseCallback<wsReturnKeyWithML>): void { this.catchAllListener = newListener; } @@ -195,7 +198,7 @@ export class Broker { setTimeout(() => Broker.instance().attemptReconnect(), 5000); } - public sendMessage(message: Omit<WsMessageBody, 'callID'>, callback?: BrokerCallbackFunction): void { + public sendMessage<R extends wsReturnKeyWithML>(message: Omit<WsMessageBody, 'callID'>, callback?: ResponseCallback<R>): void { console.debug('%cSending WS message: ', 'background: #222; color: #bada55', message); const uuid = uuidv4(); @@ -232,10 +235,25 @@ export class Broker { const status = jsonObject.status; const uuid = jsonObject.callID; - let stop = false; // check in case there is a specific callback listener and, if its response is true, also call the subscriptions this.mostRecentMessages[routingKey] = jsonObject; if (uuid in this.callbackListeners) { - stop = this.callbackListeners[uuid](data, status) !== true; + // If there is a callback listener, notify it, and not the others unless the callback listener says so + this.callbackListeners[uuid]({ + data, + status, + routingKey, + defaultCallback: () => { + if (this.listeners[routingKey] && Object.keys(this.listeners[routingKey]).length != 0) { + if (this.catchAllListener) { + this.catchAllListener({ data, status, routingKey, defaultCallback: () => true }); + } + Object.values(this.listeners[routingKey]).forEach(listener => + listener({ data, status, routingKey, defaultCallback: () => true }), + ); + console.debug('%c' + routingKey + ` WS response`, 'background: #222; color: #DBAB2F', data); + } + }, + }); console.debug( '%c' + routingKey + ` WS response WITH CALLBACK`, 'background: #222; color: #DBAB2F', @@ -245,19 +263,17 @@ export class Broker { stop, ); delete this.callbackListeners[uuid]; - } - - if (!stop && this.listeners[routingKey] && Object.keys(this.listeners[routingKey]).length != 0) { + } 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); + this.catchAllListener({ data, status, routingKey, defaultCallback: () => true }); } - Object.values(this.listeners[routingKey]).forEach(listener => listener(data, status, routingKey)); + Object.values(this.listeners[routingKey]).forEach(listener => listener({ data, status, routingKey, defaultCallback: () => true })); console.debug('%c' + routingKey + ` WS response`, 'background: #222; color: #DBAB2F', data); - } - // If there are no listeners, log the message - else if (!stop) { + } else { + // If there are no listeners, log the message if (this.catchAllListener) { - this.catchAllListener(data, status, routingKey); + this.catchAllListener({ data, status, routingKey, 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/wsInsightSharing.ts b/src/lib/data-access/broker/wsInsightSharing.ts index 42dbc5471e12820110869ff24b1bc329194d8d66..b857b731eb4b2a16c3fd7992b4854c77ff691044 100644 --- a/src/lib/data-access/broker/wsInsightSharing.ts +++ b/src/lib/data-access/broker/wsInsightSharing.ts @@ -1,4 +1,4 @@ -import { InsightModel, InsightRequest, ResponseCallback, WsFrontendCall, wsKeys, wsReturnKey, wsSubKeys } from 'ts-common'; +import { InsightRequest, ResponseCallback, WsFrontendCall, wsKeys, wsReturnKey, wsSubKeys } from 'ts-common'; import { Broker } from './broker'; export const wsGetInsights: WsFrontendCall<{ saveStateId: string }, wsReturnKey.insightResults> = (params, callback) => { @@ -13,32 +13,30 @@ export const wsGetInsights: WsFrontendCall<{ saveStateId: string }, wsReturnKey. }; export const wsCreateInsight: WsFrontendCall<InsightRequest, wsReturnKey.insightResult> = (params, callback) => { - Broker.instance().sendMessage( + Broker.instance().sendMessage<wsReturnKey.insightResult>( { key: wsKeys.insight, subKey: wsSubKeys.create, body: params, }, - (data: InsightModel, status: string) => { - if (status === 'Bad Request') { - console.error('Failed to create insight:', data); - if (callback) callback(data, status); + params => { + if (params.status === 'Bad Request') { + console.error('Failed to create insight:', params.data); + if (callback) callback(params); return; } - if (!data || typeof data !== 'object') { - console.error('Invalid response data', data); - if (callback) callback(null, 'error'); + if (!params.data || typeof params.data !== 'object') { + console.error('Invalid response data', params.data); return; } - if (!data.type || !data.id) { - console.error('Missing fields in response', data); - if (callback) callback(null, 'error'); + if (!params.data.type || !params.data.id) { + console.error('Missing fields in response', params.data); return; } - if (callback) callback(data, status); + if (callback) callback(params); }, ); }; @@ -47,33 +45,33 @@ export const wsUpdateInsight: WsFrontendCall<{ id: number; insight: InsightReque params, callback, ) => { - Broker.instance().sendMessage( + Broker.instance().sendMessage<wsReturnKey.insightResult>( { key: wsKeys.insight, subKey: wsSubKeys.update, body: { id: params.id, insight: params.insight, generateEmail: params.generateEmail }, }, - (data: any, status: string) => { - if (status === 'Bad Request') { - console.error('Failed to update insight:', data); + params => { + if (params.status === 'Bad Request') { + console.error('Failed to update insight:', params.data); } - if (callback) callback(data, status); + if (callback) callback(params); }, ); }; export const wsDeleteInsight: WsFrontendCall<{ id: number }, wsReturnKey.insightResult> = (params, callback) => { - Broker.instance().sendMessage( + Broker.instance().sendMessage<wsReturnKey.insightResult>( { key: wsKeys.insight, subKey: wsSubKeys.delete, body: { id: params.id }, }, - (data: any, status: string) => { - if (status === 'Bad Request') { - console.error('Failed to delete insight:', data); + params => { + if (params.status === 'Bad Request') { + console.error('Failed to delete insight:', params.data); } - if (callback) callback(data, status); + if (callback) callback(params); }, ); }; diff --git a/src/lib/data-access/broker/wsProject.ts b/src/lib/data-access/broker/wsProject.ts index 6331bbff98c2f52d68b45a32cf70fc620a8c15c7..c74de8ee07c83702fe42b7595f93c6eb77ff6dbb 100644 --- a/src/lib/data-access/broker/wsProject.ts +++ b/src/lib/data-access/broker/wsProject.ts @@ -1,6 +1,6 @@ import { ProjectModel, ProjectRequest } from 'ts-common/src/model/webSocket/project'; import { Broker } from './broker'; -import { SaveState, wsKeys, wsReturnKey, wsSubKeys } from 'ts-common'; +import { ResponseCallback, SaveState, WsFrontendCall, wsKeys, wsReturnKey, wsSubKeys } from 'ts-common'; type GetProjectResponse = (data: ProjectModel, status: string) => void; type GetProjectsResponse = (data: ProjectModel[], status: string) => void; @@ -22,96 +22,64 @@ function transformToProjectModel(saveStates: SaveState[]): ProjectModel[] { }) as ProjectModel, ); } - -export function wsGetProject(userID: number, parentID: number, callback?: GetProjectResponse) { +export const wsGetProject: WsFrontendCall<{ userID: number; parentID: number }, wsReturnKey.project> = (params, callback) => { Broker.instance().sendMessage( { key: wsKeys.project, subKey: wsSubKeys.get, - body: { userID: userID, parentID: parentID }, - }, - (data: ProjectModel, status: string) => { - if (callback) { - callback(data, status); - } + body: { userID: params.userID, parentID: params.parentID }, }, + callback, ); -} +}; -export function wsGetProjects(userID: number, parentID: number | null, callback?: GetProjectsResponse) { +export const wsGetProjects: WsFrontendCall<{ userID: number; parentID: number | null }, wsReturnKey.projects> = (params, callback) => { Broker.instance().sendMessage( { key: wsKeys.project, subKey: wsSubKeys.getAll, - body: { parentID: parentID }, - }, - (data: ProjectModel[], status: string) => { - if (callback) callback(data, status); + body: { parentID: params.parentID }, }, + callback, ); -} +}; -export function wsCreateProject(project: ProjectRequest, callback?: GetProjectResponse) { +export const wsCreateProject: WsFrontendCall<{ project: ProjectRequest }, wsReturnKey.project> = (params, callback) => { Broker.instance().sendMessage( { key: wsKeys.project, - subKey: wsSubKeys.create, - body: project, - }, - (data: ProjectModel, status: string) => { - if (callback) { - callback(data, status); - } + subKey: wsSubKeys.getAll, + body: params.project, }, + callback, ); -} +}; -export function wsUpdateProject(id: number, project: ProjectRequest, callback?: GetProjectResponse) { +export const wsUpdateProject: WsFrontendCall<{ id: number; project: ProjectRequest }, wsReturnKey.project> = (params, callback) => { Broker.instance().sendMessage( { key: wsKeys.project, subKey: wsSubKeys.update, - body: { queryID: id, ...project }, - }, - (data: SaveState, status: string) => { - const projects = transformToProjectModel([data]); - if (callback && projects.length > 0) { - callback(projects[0], status); - } else if (callback) { - callback( - { - id: -1, - name: '', - parentId: null, - createdAt: '', - updatedAt: '', - }, - 'error', - ); - } + body: { queryID: params.id, ...params.project }, }, + callback, ); -} +}; -export function wsDeleteProject(id: number, callback?: (data: { success: boolean }, status: string) => void) { +export const wsDeleteProject: WsFrontendCall<{ id: number }, wsReturnKey.project> = (params, callback) => { Broker.instance().sendMessage( { key: wsKeys.project, subKey: wsSubKeys.delete, - body: { id: id }, + body: { id: params.id }, }, callback, ); -} +}; -export function wsProjectSubscription(callback: (data: ProjectModel, status: string) => void) { - const id = Broker.instance().subscribe((data: SaveState, status: string) => { - const projects = transformToProjectModel([data]); - if (projects.length > 0) { - callback(projects[0], status); - } - }, wsReturnKey.project); +export const wsProjectSubscription = (callback: ResponseCallback<wsReturnKey.project>) => { + const id = Broker.instance().subscribe(callback, wsReturnKey.project); return () => { Broker.instance().unSubscribe(wsReturnKey.project, id); }; -} +}; diff --git a/src/lib/data-access/broker/wsState.ts b/src/lib/data-access/broker/wsState.ts index be16f46350f73839db7ee03e2bff60c4a486f72a..10028798d9967408e17a1b956095a2d841ac5fe9 100644 --- a/src/lib/data-access/broker/wsState.ts +++ b/src/lib/data-access/broker/wsState.ts @@ -118,7 +118,7 @@ export const wsUpdateState: WsFrontendCall<SaveState, wsReturnKey.saveState> = ( export const wsTestDatabaseConnection: WsFrontendCall<DatabaseInfo, wsReturnKey.testedConnection> = (dbConnection, callback) => { if (!dbConnection) { console.warn('dbConnection is undefined on wsTestDatabaseConnection'); - if (callback) callback({ status: 'fail', saveStateID: '' }, 'error'); + if (callback) callback({ data: { status: 'fail', saveStateID: '' }, status: 'error', defaultCallback: () => {} }); return; } Broker.instance().sendMessage( @@ -131,17 +131,6 @@ export const wsTestDatabaseConnection: WsFrontendCall<DatabaseInfo, wsReturnKey. ); }; -export const wsStateGetPolicy: WsFrontendCall<{ saveStateID: string }, wsReturnKey.saveStatesModels> = (params, callback) => { - Broker.instance().sendMessage( - { - key: wsKeys.state, - subKey: wsSubKeys.getPolicy, - body: { saveStateID: params.saveStateID }, - }, - callback, - ); -}; - export const wsShareSaveState: WsFrontendCall<StateSetPolicyRequest, wsReturnKey.shareSaveState> = (request, callback) => { Broker.instance().sendMessage( { diff --git a/src/lib/data-access/store/hooks.ts b/src/lib/data-access/store/hooks.ts index 07b22cd3b06d6e4cca534e5470e1533725ae7718..6141da472e1f46381059f584d04702a851d474b3 100644 --- a/src/lib/data-access/store/hooks.ts +++ b/src/lib/data-access/store/hooks.ts @@ -4,14 +4,7 @@ import { schemaGraph, selectSchemaLayout, schemaSettingsState, schemaStatsState, import type { RootState, AppDispatch } from './store'; import { ConfigStateI, configState } from '@/lib/data-access/store/configSlice'; -import { - activeSaveState, - activeSaveStateAuthorization, - activeSaveStateQuery, - selectQuerybuilderHash, - SessionCacheI, - sessionCacheState, -} from './sessionSlice'; +import { activeSaveState, activeSaveStateQuery, selectQuerybuilderHash, SessionCacheI, sessionCacheState } from './sessionSlice'; import { AuthSliceState, authState } from './authSlice'; import { visualizationState, VisState, visualizationActive } from './visualizationSlice'; import { allMLEnabled, selectML } from './mlSlice'; @@ -24,7 +17,6 @@ import { SearchCategoryMapI, CategoryDataI, } from './searchResultSlice'; -import { AllLayoutAlgorithms } from '../../graph-layout'; import { SchemaGraph } from '../../schema'; import { SelectionStateI, FocusStateI, focusState, selectionState } from './interactionSlice'; import { VisualizationSettingsType } from '../../vis/common'; @@ -32,19 +24,20 @@ import { PolicyUsersState, selectPolicyState } from './authorizationUsersSlice'; import { selectResourcesPolicyState } from './authorizationResourcesSlice'; import { selectInsights } from './insightSharingSlice'; import { - SchemaGraphStats, - QueryGraphEdgeHandle, PolicyResourcesState, InsightModel, - QueryBuilderSettings, ML, GraphStatistics, SaveState, SaveStateAuthorizationHeaders, Query, SchemaSettings, + SchemaStatsFromBackend, + SchemaGraphStats, + SaveStateWithAuthorization, } from 'ts-common'; import { ProjectState, selectProject } from './projectSlice'; +import { AllLayoutAlgorithms } from 'ts-common/src/model/layouts'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch; @@ -73,8 +66,7 @@ export const useQuerybuilderHash: () => string = () => useAppSelector(selectQuer // Overall Configuration of the app export const useConfig: () => ConfigStateI = () => useAppSelector(configState); export const useSessionCache: () => SessionCacheI = () => useAppSelector(sessionCacheState); -export const useActiveSaveState: () => SaveState | undefined = () => useAppSelector(activeSaveState); -export const useActiveSaveStateAuthorization: () => SaveStateAuthorizationHeaders = () => useAppSelector(activeSaveStateAuthorization); +export const useActiveSaveState: () => SaveStateWithAuthorization | undefined = () => useAppSelector(activeSaveState); export const useAuthCache: () => AuthSliceState = () => useAppSelector(authState); // Machine Learning Slices diff --git a/src/lib/data-access/store/sessionSlice.ts b/src/lib/data-access/store/sessionSlice.ts index 5ac64ef655b0e937a76e1f23d2d7263dfcb03cfb..cf7d308b0d7638852a209f270952781ae3b8d7a2 100644 --- a/src/lib/data-access/store/sessionSlice.ts +++ b/src/lib/data-access/store/sessionSlice.ts @@ -14,6 +14,7 @@ import { QueryUnionType, SaveState, SaveStateAuthorizationHeaders, + SaveStateWithAuthorization, } from 'ts-common'; import { QueryMultiGraphology } from '@/lib/querybuilder'; import { MultiGraph } from 'graphology'; @@ -29,8 +30,7 @@ export type ErrorMessage = { /** Cache type */ export type SessionCacheI = { currentSaveState?: string; // id of the current save state - saveStates: Record<string, SaveState>; - saveStatesAuthorization: Record<string, SaveStateAuthorizationHeaders>; + saveStates: Record<string, SaveStateWithAuthorization>; fetchingSaveStates: boolean; testedSaveState: Record<string, DatabaseStatus>; }; @@ -47,7 +47,6 @@ const newStateAuthorizationHeaders: SaveStateAuthorizationHeaders = { export const initialState: SessionCacheI = { currentSaveState: undefined, saveStates: {}, - saveStatesAuthorization: {}, fetchingSaveStates: true, // default to true to prevent flashing of the UI testedSaveState: {}, }; @@ -68,29 +67,21 @@ export const sessionSlice = createSlice({ } setParam(URLParams.saveState, action.payload); }, - updateSelectedSaveState: (state: SessionCacheI, action: PayloadAction<SaveState>) => { + updateSelectedSaveState: (state: SessionCacheI, action: PayloadAction<SaveStateWithAuthorization>) => { if (state.currentSaveState === action.payload.id && state.currentSaveState) state.saveStates[state.currentSaveState] = action.payload; }, - updateSaveStateList: (state: SessionCacheI, action: PayloadAction<SaveState[]>) => { + updateSaveStateList: (state: SessionCacheI, action: PayloadAction<SaveStateWithAuthorization[]>) => { // Does NOT clear the states, just adds in new data - const newState: Record<string, SaveState> = {}; + const newState: Record<string, SaveStateWithAuthorization> = {}; if (!action.payload) return; - action.payload.forEach((ss: SaveState) => { + action.payload.forEach(ss => { newState[ss.id] = ss; }); state.saveStates = { ...state.saveStates, ...newState }; - const paramSaveState = getParam(URLParams.saveState); - if (!state.currentSaveState) { - if (paramSaveState && paramSaveState in state.saveStates) { - state.currentSaveState = paramSaveState; - } else if (Object.keys(state.saveStates).length > 0) { - state.currentSaveState = Object.keys(state.saveStates)[0]; - } else state.currentSaveState = undefined; - } }, - setSaveStateList: (state: SessionCacheI, action: PayloadAction<SaveState[]>) => { + setSaveStateList: (state: SessionCacheI, action: PayloadAction<SaveStateWithAuthorization[]>) => { // Clears the states and puts in new data - const newState: Record<string, SaveState> = {}; + const newState: Record<string, SaveStateWithAuthorization> = {}; action.payload.forEach(ss => { newState[ss.id] = ss; }); @@ -99,36 +90,39 @@ export const sessionSlice = createSlice({ if (!state.currentSaveState) { if (paramSaveState && paramSaveState in state.saveStates) { state.currentSaveState = paramSaveState; + setParam(URLParams.saveState, paramSaveState); } else if (Object.keys(state.saveStates).length > 0) { state.currentSaveState = Object.keys(state.saveStates)[0]; - } else state.currentSaveState = undefined; + setParam(URLParams.saveState, state.currentSaveState); + } else { + state.currentSaveState = undefined; + setParam(URLParams.saveState, undefined); + } } }, - addSaveState: (state: SessionCacheI, action: PayloadAction<SaveState>) => { + addSaveState: (state: SessionCacheI, action: PayloadAction<{ ss: SaveStateWithAuthorization; select: boolean }>) => { if (state.saveStates === undefined) state.saveStates = {}; - state.saveStates[action.payload.id] = action.payload; - state.currentSaveState = action.payload.id; - if (!state.saveStatesAuthorization[action.payload.id]) { - state.saveStatesAuthorization[action.payload.id] = cloneDeep(newStateAuthorizationHeaders); + state.saveStates[action.payload.ss.id] = action.payload.ss; + if (action.payload.select === true) { + state.currentSaveState = action.payload.ss.id; + setParam(URLParams.saveState, action.payload.ss.id); } }, deleteSaveState: (state: SessionCacheI, action: PayloadAction<string>) => { delete state.saveStates[action.payload]; - delete state.saveStatesAuthorization[action.payload]; if (state.currentSaveState === action.payload) { - if (Object.keys(state.saveStates).length > 0) state.currentSaveState = Object.keys(state.saveStates)[0]; - else state.currentSaveState = undefined; + if (Object.keys(state.saveStates).length > 0) { + state.currentSaveState = Object.keys(state.saveStates)[0]; + setParam(URLParams.saveState, state.currentSaveState); + } else { + state.currentSaveState = undefined; + setParam(URLParams.saveState, undefined); + } } }, testedSaveState: (state: SessionCacheI, action: PayloadAction<{ saveStateID: string; status: DatabaseStatus }>) => { state.testedSaveState = { ...state.testedSaveState, [action.payload.saveStateID]: action.payload.status }; }, - setStateAuthorization: (state: SessionCacheI, action: PayloadAction<{ id: string; authorization: SaveStateAuthorizationHeaders }>) => { - state.saveStatesAuthorization[action.payload.id] = action.payload.authorization; - }, - deleteAuthorization: (state: SessionCacheI, action: PayloadAction<string>) => { - delete state.saveStatesAuthorization[action.payload]; - }, setQuerybuilderGraph: (state: SessionCacheI, action: PayloadAction<QueryMultiGraph>) => { const activeQuery = getActiveQuery(state); if (activeQuery) { @@ -300,8 +294,6 @@ export const { testedSaveState, setFetchingSaveStates, updateSelectedSaveState, - setStateAuthorization, - deleteAuthorization, setQuerybuilderGraph, setQuerybuilderSettings, setActiveQueryID, @@ -317,9 +309,10 @@ export const { setQueryUnionType, } = sessionSlice.actions; -function getActiveSaveState(sessionCache: SessionCacheI): SaveState | undefined { +function getActiveSaveState(sessionCache: SessionCacheI): SaveStateWithAuthorization | undefined { return sessionCache.saveStates?.[sessionCache.currentSaveState!]; } + function getActiveQuery(state: SessionCacheI): Query | undefined { const ss = getActiveSaveState(state); if (!ss || !ss.queryStates || !ss.queryStates.activeQueryId || ss.queryStates.activeQueryId === -1) { @@ -351,9 +344,7 @@ export const toQuerybuilderGraphology = (graph: QueryMultiGraph): QueryMultiGrap // Other code such as selectors can use the imported `RootState` type export const sessionCacheState = (state: RootState) => state.sessionCache; -export const activeSaveState = (state: RootState): SaveState | undefined => getActiveSaveState(state.sessionCache); -export const activeSaveStateAuthorization = (state: RootState): SaveStateAuthorizationHeaders => - state.sessionCache.saveStatesAuthorization?.[state.sessionCache.currentSaveState!] || newStateAuthorizationHeaders; +export const activeSaveState = (state: RootState): SaveStateWithAuthorization | undefined => getActiveSaveState(state.sessionCache); export default sessionSlice.reducer; diff --git a/src/lib/graph-layout/layoutCreatorUsecase.ts b/src/lib/graph-layout/layoutCreatorUsecase.ts index aaef9951b2dadeb35a50c1ea4e4f50d8f20ecae8..36128f63ce1a5da26c28f9102533ab1a48e87135 100644 --- a/src/lib/graph-layout/layoutCreatorUsecase.ts +++ b/src/lib/graph-layout/layoutCreatorUsecase.ts @@ -1,14 +1,14 @@ -import { CytoscapeFactory } from './cytoscapeLayouts'; -import { DagreFactory } from './dagreLayout'; -import { GraphologyFactory } from './graphologyLayouts'; -import { ListLayoutFactory } from './listLayouts'; -import { AlgorithmToLayoutProvider } from './types'; import { AllLayoutAlgorithms, - CytoscapeLayoutAlgorithms, GraphologyLayoutAlgorithms, + CytoscapeLayoutAlgorithms, ListLayoutAlgorithms, } from 'ts-common/src/model/layouts'; +import { CytoscapeFactory } from './cytoscapeLayouts'; +import { DagreFactory, DagreLayouts } from './dagreLayout'; +import { GraphologyFactory } from './graphologyLayouts'; +import { ListLayoutFactory } from './listLayouts'; +import { AlgorithmToLayoutProvider } from './types'; export interface ILayoutFactory<Algorithm extends AllLayoutAlgorithms> { createLayout: (Algorithm: Algorithm) => AlgorithmToLayoutProvider<Algorithm> | null; @@ -34,6 +34,10 @@ export class LayoutFactory implements ILayoutFactory<AllLayoutAlgorithms> { createLayout<Algorithm extends AllLayoutAlgorithms = AllLayoutAlgorithms>( layoutAlgorithm: Algorithm, ): AlgorithmToLayoutProvider<Algorithm> { + if (layoutAlgorithm == null) { + layoutAlgorithm = 'Dagre_Dagre' as Algorithm; + } + if (this.isSpecificAlgorithm<GraphologyLayoutAlgorithms>(layoutAlgorithm, 'Graphology')) { return this.graphologyFactory.createLayout(layoutAlgorithm) as AlgorithmToLayoutProvider<Algorithm>; } diff --git a/src/lib/insight-sharing/InsightDialog.tsx b/src/lib/insight-sharing/InsightDialog.tsx index 6f9c4a1cf70b89033f401df287558575397e1e7f..0b5dbeb1ca911fed177092302224103d79206da0 100644 --- a/src/lib/insight-sharing/InsightDialog.tsx +++ b/src/lib/insight-sharing/InsightDialog.tsx @@ -30,7 +30,7 @@ export function InsightDialog(props: Props) { return; } - wsDeleteInsight({ id: insight.id }, (data, status) => { + wsDeleteInsight({ id: insight.id }, ({ data, status }) => { if (status === 'success') { dispatch(deleteInsight({ id: insight.id })); if (active === insight.id) { @@ -48,7 +48,7 @@ export function InsightDialog(props: Props) { const handleSave = (insight: InsightModel | InsightRequest, generateEmail: boolean) => { if (Object.prototype.hasOwnProperty.call(insight, 'id')) { const updatedInsight = insight as InsightModel; - wsUpdateInsight({ id: updatedInsight.id, insight: updatedInsight, generateEmail }, (data, status) => { + wsUpdateInsight({ id: updatedInsight.id, insight: updatedInsight, generateEmail }, ({ data, status }) => { if (status === 'success' && data) { dispatch(updateInsight(data)); dispatch(addSuccess(`${insight.type} updated successfully`)); @@ -60,7 +60,7 @@ export function InsightDialog(props: Props) { }); } else { const newInsight = insight as InsightRequest; - wsCreateInsight(newInsight, (data, status) => { + wsCreateInsight(newInsight, ({ data, status }) => { if (status === 'success' && data) { dispatch(addInsight(data)); dispatch(addSuccess(`${insight.type} created successfully`)); diff --git a/src/lib/insight-sharing/components/Sidebar.tsx b/src/lib/insight-sharing/components/Sidebar.tsx index 75d418a9b0ee6f39bd3614feee97c32519849144..3cdefb473430ea292322d8160c794dc6c09acf04 100644 --- a/src/lib/insight-sharing/components/Sidebar.tsx +++ b/src/lib/insight-sharing/components/Sidebar.tsx @@ -5,6 +5,7 @@ import { useAppDispatch, useInsights } from '../../data-access'; import { setInsights } from '../../data-access/store/insightSharingSlice'; import { useSessionCache } from '../../data-access'; import { wsGetInsights } from '../../data-access/broker/wsInsightSharing'; +import { addError } from '@/lib/data-access/store/configSlice'; export type MonitorType = 'report' | 'alert'; @@ -20,7 +21,13 @@ export function Sidebar(props: SidebarProps) { useEffect(() => { if (session.currentSaveState && session.currentSaveState !== '') { - wsGetInsights({ saveStateId: session.currentSaveState }, (data: any) => { + wsGetInsights({ saveStateId: session.currentSaveState }, ({ data, status }) => { + if (status !== 'success') { + console.error('Failed to get insights:', data); + dispatch(addError('Failed to get insights')); + return; + } + dispatch(setInsights(data)); }); } diff --git a/src/lib/management/database/Databases.tsx b/src/lib/management/database/Databases.tsx index a5db1e43832134c9a21a2300b5be532c0ece8a6d..78b3b14e3003556847f95e112706e07687cb512e 100644 --- a/src/lib/management/database/Databases.tsx +++ b/src/lib/management/database/Databases.tsx @@ -12,10 +12,10 @@ import { useSessionCache, wsDeleteState, } from '../..'; -import { clearQB, deleteSaveState, selectSaveState } from '../../data-access/store/sessionSlice'; +import { clearQB, deleteSaveState } from '../../data-access/store/sessionSlice'; import { clearSchema } from '../../data-access/store/schemaSlice'; import { useHandleDatabase } from './useHandleDatabase'; -import { CountQueryResultFromBackend, SaveState } from 'ts-common'; +import { SaveState } from 'ts-common'; // --- Add the imports for project functionality --- import { addProject, setCurrentProject, setProjects, deleteProject, addToPath } from '../../data-access/store/projectSlice'; @@ -65,8 +65,6 @@ export function Databases({ onClose, saveStates, changeActive, setSelectedSaveSt const databaseHandler = useHandleDatabase(); const [orderBy, setOrderBy] = useState<[DatabaseTableHeaderTypes, 'asc' | 'desc']>(['name', 'desc']); const [sharing, setSharing] = useState<boolean>(false); - const activeQuery = useActiveQuery(); - const queryResult = useGraphQueryResult(); const projectState = useProjects(); const [showNewProjectDialog, setShowNewProjectDialog] = useState(false); @@ -74,16 +72,10 @@ export function Databases({ onClose, saveStates, changeActive, setSelectedSaveSt const [showDifferentiation, setShowDifferentiation] = useState(false); const [showQueries, setShowQueries] = useState(false); - const configVisuals = { - classNameVisuals: 'h-10', - marginPercentage: { top: 0.1, right: 0.07, left: 0.07, bottom: 0.1 }, - maxValue: 1, - }; - // load projects from DB on path change useEffect(() => { - wsGetProjects(authCache.authentication?.userID || -1, projectState.currentProject?.id ?? null, projects => { - dispatch(setProjects(projects)); + wsGetProjects({ userID: authCache.authentication?.userID || -1, parentID: projectState.currentProject?.id ?? null }, ({ data }) => { + dispatch(setProjects(data)); }); }, [projectState.currentPath]); @@ -141,68 +133,30 @@ export function Databases({ onClose, saveStates, changeActive, setSelectedSaveSt if (newProjectName) { wsCreateProject( { - name: newProjectName, - parent: projectState.currentProject?.id ?? null, - userID: authCache.authentication?.userID || -1, + project: { + name: newProjectName, + parent: projectState.currentProject?.id ?? null, + userID: authCache.authentication?.userID || -1, + }, }, - (data, status) => { + ({ data, status }) => { if (status === 'success' && data) { dispatch(addProject(data)); setShowNewProjectDialog(false); setNewProjectName(''); - wsGetProjects(authCache.authentication?.userID || -1, projectState.currentProject?.id ?? null, projects => { - dispatch(setProjects(projects)); - }); + wsGetProjects( + { userID: authCache.authentication?.userID || -1, parentID: projectState.currentProject?.id ?? null }, + ({ data }) => { + dispatch(setProjects(data)); + }, + ); } }, ); } }; - function differentiationFor(saveStateId: string, queryIndex: number = 0) { - const saveState = session.saveStates[saveStateId]; - const query = saveState?.queryStates.openQueryArray[queryIndex]; - - // For the current query, use the most up to date results from queryresultslice - let nodeCountsObj: CountQueryResultFromBackend | undefined; - if (activeQuery != null && activeQuery.id == query.id) { - nodeCountsObj = queryResult.nodeCounts ?? query.graph?.nodeCounts; - } else { - nodeCountsObj = query.graph?.nodeCounts; - } - - if (nodeCountsObj == null || saveState?.schemas[0]?.stats == null) return 0; - - const nodeCounts = query.graph.nodes - .filter(x => x.attributes.type == 'entity') - .map(x => ({ name: x.attributes.name, count: nodeCountsObj[`${x.key}_count`] ?? 0 })); - - const totalCounts = query.graph.nodes - .filter(x => x.attributes.type == 'entity') - .map(x => ({ name: x.attributes.name, count: saveState?.schemas[0].stats?.nodes?.stats[x.attributes.name]?.count ?? 0 })); - - const diffs = nodeCounts.map((x, i) => x.count / totalCounts[i].count); - const score = diffs.reduce((a, b) => a + b) / diffs.length; - - return Math.round(score * 100) / 100; - } - - function timestampFor(saveStateId: string, queryIndex: number = 0) { - const saveState = session.saveStates[saveStateId]; - const query = saveState?.queryStates.openQueryArray[queryIndex]; - - if (query.graph?.nodeCounts == null || query.graph.nodeCounts.updatedAt == null) return 'unknown'; - - return new Date(query.graph.nodeCounts.updatedAt).toLocaleString('nl-NL', { - month: '2-digit', - day: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - } - return ( <div className="flex flex-col gap-4"> <div className="flex justify-between items-start"> @@ -282,7 +236,7 @@ export function Databases({ onClose, saveStates, changeActive, setSelectedSaveSt iconComponent="icon-[mdi--trash-outline]" variant="ghost" onClick={() => { - wsDeleteProject(project.id); + wsDeleteProject({ id: project.id }); dispatch(deleteProject(project.id)); }} /> @@ -292,46 +246,43 @@ export function Databases({ onClose, saveStates, changeActive, setSelectedSaveSt ))} {orderedSaveStates.map(key => ( - <> - <DatabaseLine - saveState={saveStates[key]} - onSelect={() => { - if (key !== session.currentSaveState) { - dispatch(clearSchema()); - dispatch(selectSaveState(key)); - 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} - /> - </> + <DatabaseLine + key={key} + saveState={saveStates[key]} + 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} + /> ))} </tbody> </table> diff --git a/src/lib/management/database/useHandleDatabase.ts b/src/lib/management/database/useHandleDatabase.ts index 3bc79f09286ac7d917ce11e352be9b83265587f3..7f9512ce9e468f940851903f88334400c32b1b74 100644 --- a/src/lib/management/database/useHandleDatabase.ts +++ b/src/lib/management/database/useHandleDatabase.ts @@ -10,7 +10,7 @@ import { import { useState } from 'react'; import { addSaveState, selectSaveState, testedSaveState } from '../../data-access/store/sessionSlice'; import { setSchemaLoading } from '../../data-access/store/schemaSlice'; -import { PartialSaveState, SaveState } from 'ts-common'; +import { PartialSaveState, SaveState, SaveStateWithAuthorization } from 'ts-common'; import { addError } from '@/lib/data-access/store/configSlice'; import { wsAddQuery, wsUpdateQuery } from '@/lib/data-access/broker'; @@ -50,7 +50,8 @@ export const useHandleDatabase = () => { newSaveStateData.project = null; } - wsTestDatabaseConnection(newSaveStateData.dbConnections?.[0], data => { + wsTestDatabaseConnection(newSaveStateData.dbConnections?.[0], params => { + const data = params.data; if (!newSaveStateData) { console.error('formData is null'); return; @@ -72,66 +73,70 @@ export const useHandleDatabase = () => { const queryStateToClone = JSON.parse(JSON.stringify(newSaveStateData.queryStates ?? {})); delete newSaveStateData.queryStates; - wsCreateState(newSaveStateData, async newSaveState => { - if (!newSaveState) { - dispatch(addError('Failed to create new save state.')); - return; - } - - // Only if we are adding a save state that is cloned and contains a queryState - if (newSaveStateData.queryStates != null) { - // Clone query state - for (const i in queryStateToClone.openQueryArray) { - const query = queryStateToClone.openQueryArray[i]; - delete query.id; + wsCreateState(newSaveStateData, params => { + (async (newSaveState: SaveStateWithAuthorization) => { + if (!newSaveState) { + dispatch(addError('Failed to create new save state.')); + return; + } - await new Promise<void>((resolve, reject) => { - if (Number(i) == 0) { - query.id = newSaveState.queryStates.openQueryArray[0].id; - queryStateToClone.activeQuery = query.id; - wsUpdateQuery({ saveStateID: newSaveState.id, query: query }, query => { - if (query == null) return reject(); - return resolve(); - }); - } else { - wsAddQuery({ saveStateID: newSaveState.id }, newQuery => { - if (newQuery == null) { - return reject('Failed to create new save state (query state).'); - } - query.id = newQuery.id; - queryStateToClone.activeQueryId = query.id; + // Only if we are adding a save state that is cloned and contains a queryState + if (newSaveStateData.queryStates != null) { + // Clone query state + for (const i in queryStateToClone.openQueryArray) { + const query = queryStateToClone.openQueryArray[i]; + delete query.id; - wsUpdateQuery({ saveStateID: newSaveState.id, query: query }, query => { - if (query == null) reject(); + await new Promise<void>((resolve, reject) => { + if (Number(i) == 0) { + query.id = newSaveState.queryStates.openQueryArray[0].id; + queryStateToClone.activeQuery = query.id; + wsUpdateQuery({ saveStateID: newSaveState.id, query: query }, params => { + if (params.data == null) return reject(); return resolve(); }); - }); - } - }); - } + } else { + wsAddQuery({ saveStateID: newSaveState.id }, params => { + const newQuery = params.data; + if (newQuery == null) { + return reject('Failed to create new save state (query state).'); + } + query.id = newQuery.id; + queryStateToClone.activeQueryId = query.id; - newSaveState.queryStates = queryStateToClone; - } + wsUpdateQuery({ saveStateID: newSaveState.id, query: query }, params => { + if (params.data == null) reject(); + return resolve(); + }); + }); + } + }); + } - dispatch(addSaveState(newSaveState)); - dispatch(testedSaveState({ saveStateID: newSaveState.id, status: DatabaseStatus.untested })); - setConnectionStatus({ - updating: false, - status: null, - verified: null, - }); - concludedCallback(); + newSaveState.queryStates = queryStateToClone; + } + + dispatch(addSaveState({ ss: newSaveState, select: true })); + setConnectionStatus({ + updating: false, + status: null, + verified: null, + }); + concludedCallback(); + })(params.data); }); } else { if (data.saveStateID) { dispatch(testedSaveState({ saveStateID: data.saveStateID, status: DatabaseStatus.untested })); } - wsUpdateState(newSaveStateData as SaveState, updatedSaveState => { + wsUpdateState(newSaveStateData as SaveState, params => { + const updatedSaveState = params.data; + if (!updatedSaveState) { dispatch(addError('Failed to update save state.')); return; } - dispatch(addSaveState(updatedSaveState)); + dispatch(addSaveState({ ss: updatedSaveState, select: true })); setConnectionStatus({ updating: false, status: null, diff --git a/src/lib/querybuilder/model/graphology/utils.ts b/src/lib/querybuilder/model/graphology/utils.ts index ae91d32469639a37892e15b4a3ee7382444beda8..5e830e6f40ef9797e153da0e25f86766fb0f4271 100644 --- a/src/lib/querybuilder/model/graphology/utils.ts +++ b/src/lib/querybuilder/model/graphology/utils.ts @@ -51,7 +51,7 @@ export class QueryMultiGraphology extends MultiGraph<QueryGraphNodes, QueryGraph attributes.width = width; attributes.height = height; - if (!attributes.id) attributes.id = 'id_' + (Date.now() + Math.floor(Math.random() * 1000)).toString(); + if (!attributes.id) attributes.id = 'id_' + this.nodes().length; // Add to the beginning the meta attributes, such as (# Connection) attributes.attributes = [...checkForMetaAttributes(attributes).map(a => ({ handleData: a })), ...attributes.attributes]; diff --git a/src/lib/querybuilder/panel/QueryBuilder.tsx b/src/lib/querybuilder/panel/QueryBuilder.tsx index cb885cfe7abc6b13c5b761ea344372570bf102a2..2cc8c16338a67acde136b2fa1c60786af2c1cb6a 100644 --- a/src/lib/querybuilder/panel/QueryBuilder.tsx +++ b/src/lib/querybuilder/panel/QueryBuilder.tsx @@ -1,7 +1,6 @@ import { useActiveQuery, useActiveSaveState, - useActiveSaveStateAuthorization, useAppDispatch, useConfig, useQuerybuilderHash, @@ -64,9 +63,8 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { const [toggleSettings, setToggleSettings] = useState<QueryBuilderToggleSettings>(); const reactFlowWrapper = useRef<HTMLDivElement>(null); const reactFlowRef = useRef<HTMLDivElement>(null); - const activeSS = useActiveSaveState(); + const ss = useActiveSaveState(); const activeQuery = useActiveQuery(); - const saveStateAuthorization = useActiveSaveStateAuthorization(); const schemaGraph = useSchemaGraph(); const schema = useMemo(() => toSchemaGraphology(schemaGraph), [schemaGraph]); @@ -116,7 +114,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { * TODO: only works if the node is clicked and not moved (maybe use onSelectionChange) */ function onNodesDelete(nodes: Node[]) { - if (!saveStateAuthorization.query.W) return; + if (!ss?.authorization.query.W) return; nodes.forEach(n => { graphologyGraph.dropNode(n.id); }); @@ -641,19 +639,19 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { reactFlowInstanceRef.current = reactFlowInstance; onInit(reactFlowInstance); }} - onNodesChange={saveStateAuthorization.query.W ? onNodesChange : () => {}} - onDragOver={saveStateAuthorization.query.W ? onDragOver : () => {}} - onConnect={saveStateAuthorization.query.W ? onConnect : () => {}} - onConnectStart={saveStateAuthorization.query.W ? onConnectStart : () => {}} - onConnectEnd={saveStateAuthorization.query.W ? onConnectEnd : () => {}} + onNodesChange={ss?.authorization.query.W ? onNodesChange : () => {}} + onDragOver={ss?.authorization.query.W ? onDragOver : () => {}} + onConnect={ss?.authorization.query.W ? onConnect : () => {}} + onConnectStart={ss?.authorization.query.W ? onConnectStart : () => {}} + onConnectEnd={ss?.authorization.query.W ? onConnectEnd : () => {}} // onNodeMouseEnter={onNodeMouseEnter} // onNodeMouseLeave={onNodeMouseLeave} - onEdgeUpdate={saveStateAuthorization.query.W ? onEdgeUpdate : () => {}} - onEdgeUpdateStart={saveStateAuthorization.query.W ? onEdgeUpdateStart : () => {}} - onEdgeUpdateEnd={saveStateAuthorization.query.W ? onEdgeUpdateEnd : () => {}} - onDrop={saveStateAuthorization.query.W ? onDrop : () => {}} + onEdgeUpdate={ss?.authorization.query.W ? onEdgeUpdate : () => {}} + onEdgeUpdateStart={ss?.authorization.query.W ? onEdgeUpdateStart : () => {}} + onEdgeUpdateEnd={ss?.authorization.query.W ? onEdgeUpdateEnd : () => {}} + onDrop={ss?.authorization.query.W ? onDrop : () => {}} // onContextMenu={onContextMenu} - onNodeContextMenu={saveStateAuthorization.query.W ? onNodeContextMenu : () => {}} + onNodeContextMenu={ss?.authorization.query.W ? onNodeContextMenu : () => {}} // onNodesDelete={onNodesDelete} // onNodesChange={onNodesChange} deleteKeyCode="Backspace" diff --git a/src/lib/querybuilder/panel/QueryBuilderNav.tsx b/src/lib/querybuilder/panel/QueryBuilderNav.tsx index f5b6804eba8aebbe24def8b61a5675393b2d17f6..f8bc37f9bb185a28d15eb190d3ccc50db8d4d52f 100644 --- a/src/lib/querybuilder/panel/QueryBuilderNav.tsx +++ b/src/lib/querybuilder/panel/QueryBuilderNav.tsx @@ -1,14 +1,7 @@ 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, - useActiveSaveStateAuthorization, - useAppDispatch, - useGraphQueryResult, - useML, -} from '../../data-access'; +import { useActiveQuery, useActiveSaveState, useAppDispatch, useGraphQueryResult, useML } from '../../data-access'; import { clearQB, setQuerybuilderSettings, @@ -25,7 +18,6 @@ import { wsAddQuery, wsDeleteQuery, wsManualQueryRequest, wsUpdateQuery } from ' import { Tabs, Tab } from '@/lib/components/tabs'; import { addError } from '@/lib/data-access/store/configSlice'; import Sortable from 'sortablejs'; -import { Query } from 'ts-common'; import objectHash from 'object-hash'; export type QueryBuilderToggleSettings = 'settings' | 'ml' | 'logic' | 'relatedNodes' | undefined; @@ -41,14 +33,14 @@ export type QueryBuilderNavProps = { export const QueryBuilderNav = (props: QueryBuilderNavProps) => { const dispatch = useAppDispatch(); const activeQuery = useActiveQuery(); - const activeSS = useActiveSaveState(); + const ss = useActiveSaveState(); const result = useGraphQueryResult(); const resultSize = useMemo(() => { if (!result) return 0; return result.nodes.length; }, [result]); const totalSize = useMemo(() => { - if (!activeQuery || !result || !activeSS) return 0; + if (!activeQuery || !result || !ss) return 0; const nodeCounts = activeQuery.graph.nodes .filter(x => x.attributes.type == 'entity') @@ -57,7 +49,6 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { return nodeCounts.reduce((a, b) => a + b, 0); }, [result]); const ml = useML(); - const saveStateAuthorization = useActiveSaveStateAuthorization(); const [editingIdx, setEditingIdx] = useState<{ idx: number; text: string } | null>(null); /** @@ -69,7 +60,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { const tabsRef = useRef<HTMLDivElement | null>(null); useEffect(() => { - if (!activeSS || !tabsRef.current) return; + if (!ss || !tabsRef.current) return; const sortable = new Sortable(tabsRef.current, { animation: 150, draggable: '[data-type="tab"]', @@ -87,7 +78,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { }, }); - const sortedQueries = activeSS.queryStates.openQueryArray + const sortedQueries = ss.queryStates.openQueryArray .filter(query => query.id != null) .sort((a, b) => { return a.order < b.order ? -1 : 1; @@ -97,7 +88,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { return () => { sortable.destroy(); }; - }, [activeSS ? objectHash(Object.fromEntries(activeSS?.queryStates.openQueryArray.map(x => [x.id, x.order]))) : null]); + }, [ss ? objectHash(Object.fromEntries(ss?.queryStates.openQueryArray.map(x => [x.id, x.order]))) : null]); const mlEnabled = ml.linkPrediction.enabled || ml.centrality.enabled || ml.communityDetection.enabled || ml.shortestPath.enabled; @@ -105,20 +96,20 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { wsManualQueryRequest({ query: query }); }; - if (!activeSS || !activeQuery) { + if (!ss || !activeQuery) { console.debug('No active query found in query nav'); return null; } function updateQueryName(text: string) { - if (!activeSS || !activeQuery) return; + if (!ss || !activeQuery) return; wsUpdateQuery( { - saveStateID: activeSS.id, + saveStateID: ss.id, query: { ...activeQuery, name: text }, }, - (data, state) => { - if (state !== 'success') { + ({ status }) => { + if (status !== 'success') { addError('Failed to update query'); } @@ -143,16 +134,17 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { variant="ghost" size="xs" iconComponent="icon-[ic--baseline-add]" + disabled={!ss?.authorization.database?.W} onClick={async () => { - wsAddQuery({ saveStateID: activeSS.id }, (query, status) => { - if (status !== 'success' || query == null || !query.id || query.id < 0) { + wsAddQuery({ saveStateID: ss.id }, ({ data, status }) => { + if (status !== 'success' || data == null || !data.id || data.id < 0) { console.error('Failed to add query'); addError('Failed to add query'); return; } - console.log('Query added', query); - dispatch(addNewQuery(query)); + console.log('Query added', data); + dispatch(addNewQuery(data)); }); }} /> @@ -167,7 +159,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { 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' : ''}`} > - {activeSS.queryStates.openQueryArray + {ss.queryStates.openQueryArray .filter(query => query.id != null) .sort((a, b) => { return a.order < b.order ? -1 : 1; @@ -220,18 +212,18 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { {query.name ?? 'Query'} </div> )} - {activeSS.queryStates.openQueryArray.filter(query => query.id != null).length > 1 && ( + {ss.queryStates.openQueryArray.filter(query => query.id != null).length > 1 && ( <Button variantType="secondary" variant="ghost" - disabled={!saveStateAuthorization.database?.W} + disabled={!ss?.authorization.database?.W} rounded size="3xs" iconComponent="icon-[ic--baseline-close]" onClick={e => { e.stopPropagation(); if (query.id !== undefined) { - wsDeleteQuery({ saveStateID: activeSS.id, queryID: query.id }); + wsDeleteQuery({ saveStateID: ss.id, queryID: query.id }); dispatch(removeQueryByID(query.id)); } }} @@ -264,10 +256,10 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { variantType="secondary" variant="ghost" size="xs" - disabled={!saveStateAuthorization.query.W} + disabled={!ss?.authorization.query.W} iconComponent="icon-[ic--baseline-delete]" onClick={() => { - if (saveStateAuthorization.query.W) clearAllNodes(); + if (ss?.authorization.query.W) clearAllNodes(); }} /> </TooltipTrigger> @@ -297,7 +289,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { variantType="secondary" variant="ghost" size="xs" - disabled={!saveStateAuthorization.query.W} + disabled={!ss?.authorization.query.W} iconComponent="icon-[ic--baseline-settings]" className="query-settings" /> @@ -332,7 +324,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { variantType="secondary" variant="ghost" size="xs" - disabled={!saveStateAuthorization.query.W} + disabled={!ss?.authorization.query.W} iconComponent="icon-[ic--baseline-difference]" onClick={props.onLogic} /> @@ -349,7 +341,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { variantType={mlEnabled ? 'primary' : 'secondary'} variant={mlEnabled ? 'outline' : 'ghost'} size="xs" - disabled={!saveStateAuthorization.query.W} + disabled={!ss?.authorization.query.W} iconComponent="icon-[ic--baseline-lightbulb]" /> </TooltipTrigger> @@ -412,7 +404,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { variantType={activeQuery.settings.limit <= resultSize ? 'primary' : 'secondary'} variant={activeQuery.settings.limit <= resultSize ? 'outline' : 'ghost'} size="xs" - disabled={!saveStateAuthorization.query.W} + disabled={!ss?.authorization.query.W} iconComponent="icon-[ic--baseline-filter-alt]" /> </TooltipTrigger> @@ -442,7 +434,7 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => { </PopoverContent> </Popover> <Popover> - <PopoverTrigger disabled={!saveStateAuthorization.query.W}> + <PopoverTrigger disabled={!ss?.authorization.query.W}> <Tooltip> <TooltipTrigger> <Button variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-search]" /> diff --git a/src/lib/schema/panel/Schema.tsx b/src/lib/schema/panel/Schema.tsx index f687967385c46a09a361330f34a895f86b41b15b..814466fac3cef94ef948d85071554dc78bfd02b0 100644 --- a/src/lib/schema/panel/Schema.tsx +++ b/src/lib/schema/panel/Schema.tsx @@ -16,7 +16,7 @@ import { wsSchemaRequest, } from '../../data-access'; import { setSchemaLoading, toSchemaGraphology } from '../../data-access/store/schemaSlice'; -import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '../../graph-layout'; +import { AlgorithmToLayoutProvider, LayoutFactory } from '../../graph-layout'; import { ConnectionDragLine, ConnectionLine } from '../../querybuilder'; import { SchemaEntityPill } from '../pills/nodes/entity/SchemaEntityPill'; import { SchemaListEntityPill } from '../pills/nodes/entity/SchemaListEntityPill'; @@ -27,6 +27,7 @@ import { SchemaSettings } from './SchemaSettings'; import { NodeEdge } from '../pills/edges/NodeEdge'; import { SelfEdge } from '../pills/edges/SelfEdge'; import { SchemaLayoutConfig } from './LayoutDescription/SchemaLayoutConfig'; +import { AllLayoutAlgorithms } from 'ts-common/src/model/layouts'; interface Props { content?: string; @@ -55,7 +56,7 @@ const edgeTypes = { export const Schema = (props: Props) => { const session = useSessionCache(); const settings = useSchemaSettings(); - const activeSaveState = useActiveSaveState(); + const ss = useActiveSaveState(); const searchResults = useSearchResultSchema(); const dispatch = useDispatch(); const [nodes, setNodes, onNodesChange] = useNodesState([] as Node[]); @@ -89,9 +90,9 @@ export const Schema = (props: Props) => { }; const refreshSchema = (useCached: boolean) => { - if (!activeSaveState) return; + if (!ss) return; dispatch(setSchemaLoading(true)); - wsSchemaRequest({ saveStateID: activeSaveState.id, useCached: useCached }); // No callback, this would override global behavior + wsSchemaRequest({ saveStateID: ss.id, useCached: useCached }); // No callback, this would override global behavior }; useEffect(() => { @@ -343,13 +344,14 @@ export const Schema = (props: Props) => { <Popover> <PopoverTrigger> <Tooltip> - <TooltipTrigger> + <TooltipTrigger disabled={!ss?.authorization.schema.W}> <Button variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-settings]" className="schema-settings" + disabled={!ss?.authorization.schema.W} /> </TooltipTrigger> <TooltipContent> diff --git a/src/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx b/src/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx index 0e5b5550770db41c7a47c4312c09b746723f607e..94681bbea81a222b1141e8b8d5a8ba5fb26c3763 100644 --- a/src/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx +++ b/src/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx @@ -1,7 +1,7 @@ import { EntityPill } from '@/lib/components'; import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; import { NodeDetails } from '@/lib/components/nodeDetails'; -import { useSchemaStats } from '@/lib/data-access'; +import { useActiveSaveState, useSchemaStats } from '@/lib/data-access'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Handle, NodeProps, Position, useViewport } from 'reactflow'; import { SchemaReactflowNodeWithFunctions } from '../../../model/reactflow'; @@ -13,6 +13,7 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc const viewport = useViewport(); const schemaStats = useSchemaStats(); + const ss = useActiveSaveState(); const ref = useRef<HTMLDivElement>(null); /** * adds drag functionality in order to be able to drag the entityNode to the schema @@ -62,7 +63,7 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc setOpenPopupLocation(ref.current.getBoundingClientRect()); }} - draggable + draggable={ss?.authorization.query.W} ref={ref} > {openPopupLocation !== null && ( @@ -88,7 +89,7 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc )} <EntityPill - draggable + draggable={ss?.authorization.query.W} title={id} withHandles="vertical" handleUp={ diff --git a/src/lib/schema/pills/nodes/entity/SchemaListEntityPill.tsx b/src/lib/schema/pills/nodes/entity/SchemaListEntityPill.tsx index f036c7026bb0b99f272a568fad2f3770b97d74b6..e2e1edda990d000ca376e1645b5f06bca4079667 100644 --- a/src/lib/schema/pills/nodes/entity/SchemaListEntityPill.tsx +++ b/src/lib/schema/pills/nodes/entity/SchemaListEntityPill.tsx @@ -1,7 +1,7 @@ import { EntityPill } from '@/lib/components'; import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; import { NodeDetails } from '@/lib/components/nodeDetails'; -import { useSchemaStats } from '@/lib/data-access'; +import { useActiveSaveState, useSchemaStats } from '@/lib/data-access'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Handle, NodeProps, Position, useViewport } from 'reactflow'; import { SchemaReactflowNodeWithFunctions } from '../../../model/reactflow'; @@ -12,6 +12,7 @@ export const SchemaListEntityPill = React.memo(({ id, selected, data }: NodeProp const [openPopupLocation, setOpenPopupLocation] = useState<{ x: number; y: number } | null>(null); const viewport = useViewport(); + const ss = useActiveSaveState(); const schemaStats = useSchemaStats(); const ref = useRef<HTMLDivElement>(null); @@ -63,7 +64,7 @@ export const SchemaListEntityPill = React.memo(({ id, selected, data }: NodeProp setOpenPopupLocation(ref.current.getBoundingClientRect()); }} - draggable + draggable={ss?.authorization.query.W} ref={ref} > {openPopupLocation !== null && ( @@ -89,7 +90,7 @@ export const SchemaListEntityPill = React.memo(({ id, selected, data }: NodeProp )} <EntityPill - draggable + draggable={ss?.authorization.query.W} title={ <div className="flex flex-row justify-between items-center"> <span className="line-clamp-1">{id || ''}</span> diff --git a/src/lib/schema/pills/nodes/relation/SchemaListRelationPill.tsx b/src/lib/schema/pills/nodes/relation/SchemaListRelationPill.tsx index c64e4016830570aa6d87ac4dbc8cdf2e7c7de915..c0d8aaf5c9341baf71f32ff6620a0cdb0697a163 100644 --- a/src/lib/schema/pills/nodes/relation/SchemaListRelationPill.tsx +++ b/src/lib/schema/pills/nodes/relation/SchemaListRelationPill.tsx @@ -5,7 +5,7 @@ import { SchemaReactflowRelationWithFunctions } from '../../../model/reactflow'; import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; import { NodeDetails } from '@/lib/components/nodeDetails'; -import { useSchemaStats } from '@/lib/data-access'; +import { useActiveSaveState, useSchemaStats } from '@/lib/data-access'; import { SchemaPopUp } from '../SchemaPopUp/SchemaPopUp'; import { SchemaEdge, QueryElementTypes } from 'ts-common'; @@ -13,6 +13,7 @@ export const SchemaListRelationPill = React.memo(({ id, selected, data, ...props const [openPopupLocation, setOpenPopupLocation] = useState<{ x: number; y: number } | null>(null); const viewport = useViewport(); + const ss = useActiveSaveState(); const schemaStats = useSchemaStats(); const ref = useRef<HTMLDivElement>(null); @@ -70,7 +71,7 @@ export const SchemaListRelationPill = React.memo(({ id, selected, data, ...props setOpenPopupLocation(ref.current.getBoundingClientRect()); }} - draggable + draggable={ss?.authorization.query.W} ref={ref} > {openPopupLocation !== null && ( @@ -100,7 +101,7 @@ export const SchemaListRelationPill = React.memo(({ id, selected, data, ...props </Popover> )} <RelationPill - draggable + draggable={ss?.authorization.query.W} title={ <div className="flex flex-row justify-between items-center"> <span className="line-clamp-1">{data.collection || ''}</span> diff --git a/src/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx b/src/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx index 316c5d5d6916c2037304b41a22fd9989cf56976d..74a092707a0928c82ead183c578095437b7bd77a 100644 --- a/src/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx +++ b/src/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx @@ -6,12 +6,13 @@ import { RelationPill } from '@/lib/components'; import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover'; import { NodeDetails } from '@/lib/components/nodeDetails'; import { SchemaPopUp } from '../SchemaPopUp/SchemaPopUp'; -import { useSchemaStats } from '@/lib/data-access'; +import { useActiveSaveState, useSchemaStats } from '@/lib/data-access'; export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: NodeProps<SchemaReactflowRelationWithFunctions>) => { const [openPopupLocation, setOpenPopupLocation] = useState<{ x: number; y: number } | null>(null); const viewport = useViewport(); + const ss = useActiveSaveState(); const schemaStats = useSchemaStats(); const ref = useRef<HTMLDivElement>(null); @@ -69,7 +70,7 @@ export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: setOpenPopupLocation(ref.current.getBoundingClientRect()); }} - draggable + draggable={ss?.authorization.query.W} ref={ref} > {openPopupLocation !== null && ( @@ -99,7 +100,7 @@ export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: </Popover> )} <RelationPill - draggable + draggable={ss?.authorization.query.W} title={data.collection} withHandles="vertical" handleUp={ diff --git a/src/lib/sidebar/index.tsx b/src/lib/sidebar/index.tsx index b1639ef801721cc6ad7cc538f1fb3a51fd543c10..341e16e64bdaa225d08a9955b6bc672a9e5b5cc6 100644 --- a/src/lib/sidebar/index.tsx +++ b/src/lib/sidebar/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../components'; import ColorMode from '../components/color-mode'; import { FeatureEnabled } from '@/lib/components/featureFlags'; +import { useActiveSaveState } from '..'; export type SideNavTab = 'Schema' | 'Search' | undefined; @@ -30,6 +31,8 @@ export function Sidebar({ tab: SideNavTab; openMonitoringDialog: () => void; }) { + const ss = useActiveSaveState(); + return ( <div className="side-bar w-fit h-full flex shrink"> <TooltipProvider> @@ -65,6 +68,7 @@ export function Sidebar({ size="lg" iconComponent="icon-[ic--outline-analytics]" onClick={openMonitoringDialog} + disabled={!ss?.authorization.database.W} /> </TooltipTrigger> <TooltipContent>Insight Sharing</TooltipContent> diff --git a/src/lib/vis/components/VisualizationPanel.tsx b/src/lib/vis/components/VisualizationPanel.tsx index c42b3857f7020f51d2fe65f9f60440759e1aa302..049ec1417395d5581c05a55433bcd879bfa5c548 100644 --- a/src/lib/vis/components/VisualizationPanel.tsx +++ b/src/lib/vis/components/VisualizationPanel.tsx @@ -102,7 +102,7 @@ export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => { {graphQueryResult.queryingBackend ? ( <Querying /> ) : graphQueryResult.nodes.length === 0 ? ( - <NoData dataAvailable={!activeQuery || activeQuery.graph.nodes.length > 0} /> + <NoData /> ) : openVisualizationArray.length === 0 ? ( <Recommender /> ) : ( diff --git a/src/lib/vis/components/VisualizationTabBar.tsx b/src/lib/vis/components/VisualizationTabBar.tsx index e66341543d3d43332fef3a2f7dd9650a9d542084..d916fab9371a494a6ca8affec9a3361d1f048c30 100644 --- a/src/lib/vis/components/VisualizationTabBar.tsx +++ b/src/lib/vis/components/VisualizationTabBar.tsx @@ -3,7 +3,7 @@ import { Button, DropdownContainer, DropdownItem, DropdownItemContainer, Dropdow import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip'; import { ControlContainer } from '../../components/controls'; import { Tabs, Tab } from '@/lib/components'; -import { useActiveSaveStateAuthorization, useAppDispatch, useVisualization } from '../../data-access'; +import { useActiveSaveState, useAppDispatch, useVisualization } from '../../data-access'; import { addVisualization, removeVisualization, @@ -19,9 +19,10 @@ import { resultSetFocus } from '@/lib/data-access/store/interactionSlice'; export default function VisualizationTabBar(props: { fullSize: () => void; exportImage: () => void; handleSelect: () => void }) { const { activeVisualizationIndex, openVisualizationArray } = useVisualization(); - const saveStateAuthorization = useActiveSaveStateAuthorization(); + const [open, setOpen] = useState(false); const dispatch = useAppDispatch(); + const ss = useActiveSaveState(); const [editingIdx, setEditingIdx] = useState<{ idx: number; text: string } | null>(null); const tabsRef = useRef<HTMLDivElement | null>(null); @@ -96,8 +97,15 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor <Tooltip> <TooltipTrigger> <DropdownContainer open={open} onOpenChange={setOpen}> - <DropdownTrigger disabled={!saveStateAuthorization.database?.W} onClick={() => setOpen(v => !v)}> - <Button as="a" variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-add]" /> + <DropdownTrigger disabled={!ss?.authorization.database?.W} onClick={() => setOpen(v => !v)}> + <Button + as="a" + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-add]" + disabled={!ss?.authorization.database?.W} + /> </DropdownTrigger> <DropdownItemContainer className="max-h-none"> {Object.values(VisualizationsConfig).map(({ id, displayName, icons }) => ( @@ -175,7 +183,7 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor <Button variantType="secondary" variant="ghost" - disabled={!saveStateAuthorization.database?.W} + disabled={!ss?.authorization.database?.W} rounded size="3xs" iconComponent="icon-[ic--baseline-close]" diff --git a/src/lib/vis/views/NoData.tsx b/src/lib/vis/views/NoData.tsx index f9c9cbc69081e275d10092c73ea153da5c3b1cc0..0454d47aa21d8765806089b90ab50977710b01fb 100644 --- a/src/lib/vis/views/NoData.tsx +++ b/src/lib/vis/views/NoData.tsx @@ -1,18 +1,27 @@ import React from 'react'; import { Button } from '../../components'; +import { useActiveQuery, useGraphQueryResult, useSessionCache } from '@/lib/data-access'; -type Props = { dataAvailable: boolean; error: boolean }; +export function NoData() { + const activeQuery = useActiveQuery(); + const session = useSessionCache(); + const graphResult = useGraphQueryResult(); + const dataAvailable = !activeQuery || activeQuery.graph.nodes.length > 0; -export function NoData({ dataAvailable, error }: Props) { 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> - {error ? ( + {graphResult.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> + ) : !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> ) : ( diff --git a/src/lib/vis/views/Recommender.tsx b/src/lib/vis/views/Recommender.tsx index 209a3adda961dc6a695bf070f04c77513d317ea0..f1c6f5045b6ed825834b7dd4d22bd97fccf91b9d 100644 --- a/src/lib/vis/views/Recommender.tsx +++ b/src/lib/vis/views/Recommender.tsx @@ -1,17 +1,17 @@ import { useState, useRef } from 'react'; import Info from '../../components/info'; import { addVisualization } from '../../data-access/store/visualizationSlice'; -import { useActiveSaveStateAuthorization, useAppDispatch } from '../../data-access'; +import { useActiveSaveState, useAppDispatch } from '../../data-access'; import { Visualizations } from '../components/VisualizationPanel'; import { VisualizationsConfig } from '../components/config/VisualizationConfig'; import { resultSetFocus } from '@/lib/data-access/store/interactionSlice'; export function Recommender() { const dispatch = useAppDispatch(); - const saveStateAuthorization = useActiveSaveStateAuthorization(); const [visualizationDescriptions] = useState(Object.values(VisualizationsConfig)); + const ss = useActiveSaveState(); - const ref = useRef<HTMLDivElement>(); + const ref = useRef<HTMLDivElement>(null); return ( <div className="p-4"> <span className="text-md">Select a visualization</span> @@ -22,13 +22,13 @@ export function Recommender() { return ( <div key={id} - className={`group flex flex-row gap-1.5 items-start rounded-md relative p-2 border h-18 ${saveStateAuthorization.visualization.W ? 'cursor-pointer hover:bg-secondary-100' : 'cursor-not-allowed opacity-50'}`} + className={`group flex flex-row gap-1.5 items-start rounded-md relative p-2 border h-18 ${ss?.authorization.visualization.W ? 'cursor-pointer hover:bg-secondary-100' : 'cursor-not-allowed opacity-50'}`} onClick={async e => { e.preventDefault(); // Ensure no new pointer events are passed, preventing doubleclick to open multiple visualizations. - ref.current.classList.add('pointer-events-none'); + ref?.current?.classList.add('pointer-events-none'); - if (!saveStateAuthorization.visualization.W) { + if (!ss?.authorization.visualization.W) { console.debug('User blocked from editing query due to being a viewer'); return; } @@ -36,7 +36,7 @@ export function Recommender() { const component = await Visualizations[id](); dispatch(addVisualization({ ...component.default.settings, name: displayName, id })); - ref.current.classList.remove('pointer-events-none'); + ref?.current?.classList.remove('pointer-events-none'); }} > <div className="text-secondary-500 group-hover:text-secondary-700">