From 3a92f0c4b52bfa1656421643ef9487d21b3f1a27 Mon Sep 17 00:00:00 2001 From: Leonardo Christino <leomilho@gmail.com> Date: Wed, 7 Feb 2024 12:02:00 +0100 Subject: [PATCH] fix: infinite loop, delete, and empty tooltip --- .../dbConnectionSelector.tsx | 19 +++--- .../DatabaseManagement/forms/settings.tsx | 15 +++-- libs/shared/lib/components/inputs/index.tsx | 12 ++-- libs/shared/lib/data-access/api/eventBus.tsx | 63 ++++++++++--------- .../lib/data-access/store/sessionSlice.ts | 26 ++++++-- .../table_vis/components/Table.tsx | 2 +- 6 files changed, 81 insertions(+), 56 deletions(-) diff --git a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx index d8013f96c..4301840ad 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { Add, Delete, Settings } from '@mui/icons-material'; import { useAppDispatch, useSchemaGraph, useSessionCache, useAuthorizationCache } from '@graphpolaris/shared/lib/data-access'; -import { selectSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; +import { deleteSaveState, selectSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; import { SettingsForm } from './forms/settings'; import { LoadingSpinner } from '@graphpolaris/shared/lib/components/LoadingSpinner'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; @@ -67,18 +67,12 @@ export default function DatabaseSelector({}) { }} /> )} - {/* <NewDatabaseForm - open={addDbConnectionFormOpen} - onClose={() => { - setAddDbConnectionFormOpen(false); - }} - /> */} <DropdownContainer ref={dbSelectionMenuRef} className="w-[20rem]"> <DropdownButton disabled={connecting || authCache.authorized === false || !!authCache.roomID} title={ <div className="flex items-center"> - {connecting && session.currentSaveState ? ( + {connecting && session.currentSaveState && session.currentSaveState in session.saveStates ? ( <> <LoadingSpinner /> <p className="ml-2 truncate">Connecting to {session.saveStates[session.currentSaveState].name}</p> @@ -188,10 +182,13 @@ export default function DatabaseSelector({}) { className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" onClick={(e) => { e.preventDefault(); - dispatch(selectSaveState(undefined)); - dispatch(clearQB()); - dispatch(clearSchema()); + e.stopPropagation(); + if (session.currentSaveState === save.id) { + dispatch(clearQB()); + dispatch(clearSchema()); + } wsDeleteState(save.id); + dispatch(deleteSaveState(save.id)); }} title="Delete database connection" > diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx index 0597c1525..b54f506bc 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx @@ -11,15 +11,14 @@ import { wsCreateState, nilUUID, DatabaseType, + useAuthorizationCache, } from '@graphpolaris/shared/lib/data-access'; import { ErrorOutline } from '@mui/icons-material'; import { Dialog } from '@graphpolaris/shared/lib/components/Dialog'; -import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { Button } from '@graphpolaris/shared/lib/components/buttons'; -import Input from '@graphpolaris/shared/lib/components/inputs'; import { useImmer } from 'use-immer'; import Broker from '@graphpolaris/shared/lib/data-access/socket/broker'; -import { addSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; +import { addSaveState, testedSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; import { DatabaseForm, INITIAL_SAVE_STATE } from './databaseForm'; import { SampleDatabaseSelector } from './mockSaveStates'; @@ -32,7 +31,10 @@ type Connection = { export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update'; saveState: SaveStateI | null }) => { const dispatch = useAppDispatch(); const ref = useRef<HTMLDialogElement>(null); - const [formData, setFormData] = useImmer(props.saveState && props.open === 'update' ? props.saveState : INITIAL_SAVE_STATE); + const auth = useAuthorizationCache(); + const [formData, setFormData] = useImmer( + props.saveState && props.open === 'update' ? props.saveState : { ...INITIAL_SAVE_STATE, user_id: auth.userID || '' }, + ); const [hasError, setHasError] = useState(false); const [sampleDataPanel, setSampleDataPanel] = useState<boolean | null>(false); const [connection, setConnection] = useState<Connection>({ @@ -50,6 +52,10 @@ export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update' console.error('formDataRef.current is null'); return; } + if (formDataRef.current.user_id !== auth.userID && auth.userID) { + console.error('formDataRef.current.user_id is not equal to auth.userID'); + formDataRef.current.user_id = auth.userID; + } if (data.status === 'success') { setConnection((prevState) => ({ updating: false, @@ -73,6 +79,7 @@ export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update' let _data = JSON.parse(JSON.stringify(data)); _data.db.status = DatabaseStatus.tested; dispatch(addSaveState(_data)); + dispatch(testedSaveState(_data.id)); formDataRef.current = null; closeDialog(); }, diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx index 76b72f2ba..2595b8038 100644 --- a/libs/shared/lib/components/inputs/index.tsx +++ b/libs/shared/lib/components/inputs/index.tsx @@ -89,7 +89,7 @@ const Input = (props: InputProps) => { export const SliderInput = ({ label, value, min, max, step, unit, showValue = true, onChange, tooltip }: SliderProps) => { return ( - <div data-tip={tooltip} className={'tooltip ' + styles['slider']}> + <div data-tip={tooltip || null} className={styles['slider'] + (tooltip ? ' tooltip' : '')}> <label className="label flex flex-row justify-between items-end"> <span className="label-text">{label}</span> {showValue ? ( @@ -132,7 +132,7 @@ export const TextInput = ({ const [isValid, setIsValid] = React.useState<boolean>(true); return ( - <div data-tip={tooltip || null} className="tooltip form-control w-full"> + <div data-tip={tooltip || null} className={'form-control w-full' + (tooltip ? ' tooltip' : '')}> <label className="label"> <span className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`}> {label} @@ -163,7 +163,7 @@ export const TextInput = ({ export const RadioInput = ({ label, value, options, onChange, tooltip }: RadioProps) => { return ( - <div data-tip={tooltip || null} className="tooltip"> + <div data-tip={tooltip || null} className={tooltip ? 'tooltip' : ''}> <label className="label"> <span className="label-text">{label}</span> </label> @@ -189,7 +189,7 @@ export const RadioInput = ({ label, value, options, onChange, tooltip }: RadioPr export const CheckboxInput = ({ label, value, options, onChange, tooltip }: CheckboxProps) => { return ( - <div data-tip={tooltip || null} className="tooltip"> + <div data-tip={tooltip || null} className={tooltip ? 'tooltip' : ''}> {label && ( <label className="label"> <span className="label-text">{label}</span> @@ -218,7 +218,7 @@ export const CheckboxInput = ({ label, value, options, onChange, tooltip }: Chec export const BooleanInput = ({ label, value, onChange, tooltip }: BooleanProps) => { return ( - <div data-tip={tooltip || null} className="tooltip"> + <div data-tip={tooltip || null} className={tooltip ? 'tooltip' : ''}> <label className={`label cursor-pointer w-fit gap-2 px-0 py-1`}> <span className="label-text">{label}</span> <input @@ -253,7 +253,7 @@ export const DropDownInput = ({ label, value, options, onChange, required = fals }, [isDropdownOpen]); return ( - <div data-tip={tooltip || null} className="tooltip w-full"> + <div data-tip={tooltip || null} className={'w-full' + (tooltip ? ' tooltip' : '')}> {label && ( <label className="label"> <span diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx index 9542c359b..acc34c2a8 100644 --- a/libs/shared/lib/data-access/api/eventBus.tsx +++ b/libs/shared/lib/data-access/api/eventBus.tsx @@ -22,17 +22,10 @@ import { allMLTypes, LinkPredictionInstance, setMLResult } from '@graphpolaris/s import { setQuerybuilderNodes } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { SchemaFromBackend } from '@graphpolaris/shared/lib/schema'; import { useEffect } from 'react'; -import { - SaveStateI, - TestDatabaseConnectionResponse, - wsGetState, - wsGetStates, - wsUpdateState, - wsSelectState, -} from './wsState'; +import { SaveStateI, TestDatabaseConnectionResponse, wsGetState, wsGetStates, wsUpdateState, wsSelectState, nilUUID } from './wsState'; import { wsSchemaRequest } from './wsSchema'; import { addSaveState, testedSaveState, selectSaveState, updateSaveStateList, updateSelectedSaveState } from '../store/sessionSlice'; -import { URLParams, getParam } from './url'; +import { URLParams, getParam, deleteParam } from './url'; import { setVisualizationState } from '../store/visualizationSlice'; import { isEqual } from 'lodash-es'; @@ -47,6 +40,17 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function } const visState = useVisualizationState(); const queryBuilderSettings = useQuerybuilderSettings(); + function loadSaveState(saveStateID: string | undefined, saveStates: Record<string, SaveStateI>) { + if (saveStateID && saveStates && saveStateID in saveStates) { + console.debug('Setting state from database', saveStateID, saveStates); + const state = saveStates[saveStateID]; + if (state) { + dispatch(setQuerybuilderNodes(state.queryBuilder)); + dispatch(setVisualizationState(state.visualization)); + } + } + } + useEffect(() => { Broker.instance().subscribe((data: SchemaFromBackend) => { dispatch(readInSchemaFromBackend(data)); @@ -66,17 +70,25 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function } Broker.instance().subscribe((data: SaveStateI[]) => { dispatch(updateSaveStateList(data)); + console.debug('Save States updated', data); + const d = Object.fromEntries(data.map((x) => [x.id, x])); + loadSaveState(session.currentSaveState, d); + // useEffect(() => { + + // }, [session.currentSaveState, session.saveStates]); }, 'save_states'); Broker.instance().subscribe((data: any) => {}, 'save_state_status'); Broker.instance().subscribe((data: SaveStateI) => { - dispatch(addSaveState(data)); + if (data.id !== nilUUID) { + dispatch(addSaveState(data)); + dispatch(selectSaveState(data.id)); + loadSaveState(data.id, session.saveStates); + } }, 'save_state'); - Broker.instance().subscribe((data: SaveStateI) => { - wsGetStates(); - }, 'delete_save_state'); + Broker.instance().subscribe((data: SaveStateI) => {}, 'delete_save_state'); Broker.instance().subscribe((data: { saveStateID: string; success: boolean }) => {}, 'save_state_selected'); @@ -108,7 +120,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function } if (session.currentSaveState) { let state = { ...session.saveStates[session.currentSaveState] }; if (!isEqual(state.queryBuilder, queryBuilder) && state.queryBuilder?.graph?.nodes) { - console.log('Updating queryBuilder state', state.queryBuilder, queryBuilder); + console.debug('Updating queryBuilder state', state.queryBuilder, queryBuilder); state.queryBuilder = { ...queryBuilder }; dispatch(updateSelectedSaveState(state)); wsUpdateState(state); @@ -120,7 +132,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function } if (session.currentSaveState) { let state = { ...session.saveStates[session.currentSaveState] }; if (!isEqual(state.visualization, visState)) { - console.log('Updating visState state', visState); + console.debug('Updating visState state', visState); state.visualization = { ...visState }; dispatch(updateSelectedSaveState(state)); wsUpdateState(state); @@ -130,23 +142,13 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function } useEffect(() => { // New active database - if (session.currentSaveState) { + if (session.currentSaveState && session.currentSaveState !== nilUUID) { wsSchemaRequest(session.currentSaveState); wsSelectState(session.currentSaveState); + loadSaveState(session.currentSaveState, session.saveStates); } }, [session.currentSaveState]); - useEffect(() => { - if (session.currentSaveState && session.saveStates && session.currentSaveState in session.saveStates) { - const state = session.saveStates[session.currentSaveState]; - console.log('Setting state from database', state, session.currentSaveState, session.saveStates); - if (state) { - dispatch(setQuerybuilderNodes(state.queryBuilder)); - dispatch(setVisualizationState(state.visualization)); - } - } - }, [session.currentSaveState, session.saveStates]); - useEffect(() => { // Newly (un)authorized if (auth.authorized && auth.jwt) { @@ -154,13 +156,14 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function } WebSocketHandler.instance() .useAuth(auth) .connect(() => { - console.log('WS connected', session.currentSaveState, window.location.search); + console.debug('WS connected', session.currentSaveState, window.location.search); // Process URL Params const paramSaveState = getParam(URLParams.saveState); - if (paramSaveState) { + if (paramSaveState && paramSaveState !== nilUUID) { wsGetState(paramSaveState); - dispatch(selectSaveState(paramSaveState)); + } else { + deleteParam(URLParams.saveState); } wsGetStates(); diff --git a/libs/shared/lib/data-access/store/sessionSlice.ts b/libs/shared/lib/data-access/store/sessionSlice.ts index 4b58a94d6..ca4b11e7b 100644 --- a/libs/shared/lib/data-access/store/sessionSlice.ts +++ b/libs/shared/lib/data-access/store/sessionSlice.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; -import { DatabaseStatus, SaveStateI, wsSelectState, wsTestSaveStateConnection } from '../api/wsState'; +import { DatabaseStatus, SaveStateI } from '../api/wsState'; import { getParam, URLParams } from '../api/url'; /** Message format of the error message from the backend */ @@ -28,7 +28,11 @@ export const sessionSlice = createSlice({ initialState, reducers: { selectSaveState(state, action: PayloadAction<string | undefined>) { - state.currentSaveState = action.payload; + if (action.payload !== undefined && action.payload in state.saveStates) { + state.currentSaveState = action.payload; + } else { + state.currentSaveState = undefined; + } }, updateSelectedSaveState(state, action: PayloadAction<SaveStateI>) { if (state.currentSaveState === action.payload.id) @@ -77,14 +81,28 @@ export const sessionSlice = createSlice({ state.saveStates[action.payload.id] = action.payload; state.currentSaveState = action.payload.id; }, + deleteSaveState(state, action: PayloadAction<string>) { + delete state.saveStates[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; + } + }, testedSaveState(state, action: PayloadAction<string>) { state.testedSaveState = { ...state.testedSaveState, [action.payload]: DatabaseStatus.tested }; }, }, }); -export const { selectSaveState, updateSaveStateList, setSaveStateList, addSaveState, testedSaveState, updateSelectedSaveState } = - sessionSlice.actions; +export const { + selectSaveState, + deleteSaveState, + updateSaveStateList, + setSaveStateList, + addSaveState, + testedSaveState, + updateSelectedSaveState, +} = sessionSlice.actions; // Other code such as selectors can use the imported `RootState` type export const sessionCacheState = (state: RootState) => state.sessionCache; diff --git a/libs/shared/lib/vis/visualizations/table_vis/components/Table.tsx b/libs/shared/lib/vis/visualizations/table_vis/components/Table.tsx index 79906526e..65c10c0fb 100644 --- a/libs/shared/lib/vis/visualizations/table_vis/components/Table.tsx +++ b/libs/shared/lib/vis/visualizations/table_vis/components/Table.tsx @@ -224,7 +224,7 @@ export const Table = ({ data, itemsPerPage, showBarPlot }: TableProps) => { </tr> <tr> {dataColumns.map((item, index) => ( - <th className="border-light bg-light"> + <th className="border-light bg-light" key={index}> <div className="th" key={index + item}> <div className="h-full w-full overflow-hidden"> {data2Render[index].showBarPlot && -- GitLab