diff --git a/apps/web/package.json b/apps/web/package.json index 6878b52021fb29f16430ac98e7dbe7c34e744fb5..5ddc367352fc1d018268b076f93bc5165a9a29c1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,7 +26,8 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.8.1", "reactflow": "11.4.0-next.1", - "styled-components": "^5.3.6" + "styled-components": "^5.3.6", + "use-immer": "^0.9.0" }, "devDependencies": { "@import-meta-env/cli": "^0.6.5", diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx index 9ddedfcbd9132394b21f9f7fcc1216752c4ec0b2..f8bc379ddfe639efbefd8ff8358636112a6011b9 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/app.tsx @@ -1,27 +1,15 @@ import React, { useEffect, useRef, useState } from 'react'; import { - readInSchemaFromBackend, - useAuth, useAuthorizationCache, - useDatabaseAPI, - useQueryAPI, useQuerybuilderGraph, - useQuerybuilderHash, - useSchemaAPI, useSessionCache, } from '@graphpolaris/shared/lib/data-access'; -import { WebSocketHandler } from '@graphpolaris/shared/lib/data-access/socket'; -import Broker from '@graphpolaris/shared/lib/data-access/socket/broker'; import { - assignNewGraphQueryResult, useAppDispatch, - useConfig, useML, - useMLEnabledHash, useQuerybuilderSettings, } from '@graphpolaris/shared/lib/data-access/store'; import { - GraphQueryResultFromBackendPayload, resetGraphQueryResults, queryingBackend, } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; @@ -29,95 +17,43 @@ import { Query2BackendQuery, QueryBuilder, QueryMultiGraph } from '@graphpolaris import { Schema } from '@graphpolaris/shared/lib/schema/panel'; import { Navbar } from '../components/navbar/navbar'; import { VisualizationPanel } from '@graphpolaris/shared/lib/vis/panel'; -import { SchemaFromBackend } from '@graphpolaris/shared/lib/schema'; -import { LinkPredictionInstance, setMLResult, allMLTypes } from '@graphpolaris/shared/lib/data-access/store/mlSlice'; import { Resizable } from '@graphpolaris/shared/lib/components/Resizable'; import { DashboardAlerts } from '@graphpolaris/shared/lib/data-access/authorization/dashboardAlerts'; -import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import { EventBus } from '@graphpolaris/shared/lib/data-access/api/eventBus'; import Onboarding from '../components/onboarding/onboarding'; +import { wsQueryRequest } from '@graphpolaris/shared/lib/data-access/api/wsQuery'; export interface App {} export function App(props: App) { - const { login } = useAuth(); const auth = useAuthorizationCache(); - const api = useDatabaseAPI(); - const api_schema = useSchemaAPI(); - const api_query = useQueryAPI(); - const dispatch = useAppDispatch(); - const session = useSessionCache(); const query = useQuerybuilderGraph() as QueryMultiGraph; - const queryHash = useQuerybuilderHash(); - const ws = useRef(new WebSocketHandler(import.meta.env.BACKEND_WSS_URL)); - const [authCheck, setAuthCheck] = useState(false); const ml = useML(); - const mlHash = useMLEnabledHash(); - const config = useConfig(); + const session = useSessionCache(); + const dispatch = useAppDispatch(); const queryBuilderSettings = useQuerybuilderSettings(); - // for testing purposes - // useEffect(() => { - // console.info('Authentification changed', auth) - // }, [auth]); - - useEffect(() => { - // Default - Broker.instance().subscribe((data: SchemaFromBackend) => dispatch(readInSchemaFromBackend(data)), 'schema_result'); - Broker.instance().subscribe((data: GraphQueryResultFromBackendPayload) => dispatch(assignNewGraphQueryResult(data)), 'query_result'); - allMLTypes.forEach((mlType) => { - Broker.instance().subscribe((data: LinkPredictionInstance[]) => dispatch(setMLResult({ type: mlType, result: data })), mlType); - }); - - login(); - - return () => { - Broker.instance().unSubscribeAll('schema_result'); - Broker.instance().unSubscribeAll('query_result'); - allMLTypes.forEach((mlType) => { - Broker.instance().unSubscribeAll(mlType); - }); - }; - }, []); - - useEffect(() => { - // New active database - if (session.currentDatabase) { - api_schema.RequestSchema(session.currentDatabase); - } - }, [session.currentDatabase]); - - useEffect(() => { - // Newly (un)authorized - if (auth.authorized && auth.jwt) { - console.debug('App is authorized; Getting Databases', auth.authorized); - setAuthCheck(true); - ws.current.useAuth(auth).connect(() => { - api.GetAllDatabases({ updateSessionCache: true }).catch((e) => { - dispatch(addError(e.message)); - }); - }); - } else { - // dispatch(logout()); - } - }, [auth]); - const runQuery = () => { - if (session?.currentDatabase && query) { + if (session?.currentSaveState && query) { if (query.nodes.length === 0) { dispatch(resetGraphQueryResults()); } else { dispatch(queryingBackend()); - api_query.execute(Query2BackendQuery(session.currentDatabase, query, queryBuilderSettings, ml)); + wsQueryRequest(Query2BackendQuery(session.currentSaveState, query, queryBuilderSettings, ml)); } } }; - useEffect(() => { - runQuery(); - }, [queryHash, mlHash, queryBuilderSettings]); + const [authCheck, setAuthCheck] = useState(false); return ( <div className="h-screen w-screen overflow-clip"> + <EventBus + onRunQuery={runQuery} + onAuthorized={() => { + setAuthCheck(true); + }} + /> <Onboarding /> <DashboardAlerts /> <div className={'h-screen w-screen ' + (!auth.authorized ? 'blur-sm pointer-events-none ' : '')}> @@ -136,12 +72,7 @@ export function App(props: App) { <VisualizationPanel /> </div> <div className="w-full h-full panel"> - <QueryBuilder - onRunQuery={() => { - console.log('Run Query'); - runQuery(); - }} - /> + <QueryBuilder onRunQuery={runQuery} /> </div> </Resizable> </div> diff --git a/apps/web/src/components/navbar/DatabaseManagement/DatabaseSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx similarity index 64% rename from apps/web/src/components/navbar/DatabaseManagement/DatabaseSelector.tsx rename to apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx index 47016885ba40132f90b13a2ff95c7a16dc62321d..d0309b0370b3f7d07d3bf5dde4252d0ec4e34980 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/DatabaseSelector.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx @@ -1,27 +1,27 @@ import React, { useEffect, useState } from 'react'; import { Add, Delete, Settings } from '@mui/icons-material'; -import { DatabaseInfo, useAppDispatch, useDatabaseAPI, useSchemaGraph, useSessionCache } from '@graphpolaris/shared/lib/data-access'; -import { updateCurrentDatabase } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; +import { useAppDispatch, useSchemaGraph, useSessionCache, useAuthorizationCache } from '@graphpolaris/shared/lib/data-access'; +import { updateCurrentSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; import { SettingsForm } from './forms/settings'; -import { NewDatabaseForm } from './forms/AddDatabase/newdatabase'; import { LoadingSpinner } from '@graphpolaris/shared/lib/components/LoadingSpinner'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { DropdownButton, DropdownContainer, DropdownItemContainer } from '@graphpolaris/shared/lib/components/dropdowns'; import { clearQB } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { clearSchema } from '@graphpolaris/shared/lib/data-access/store/schemaSlice'; +import { DatabaseStatus, SaveStateI, wsDeleteState } from '@graphpolaris/shared/lib/data-access/api/wsState'; export default function DatabaseSelector({}) { const dispatch = useAppDispatch(); - const api = useDatabaseAPI(); const session = useSessionCache(); const schemaGraph = useSchemaGraph(); + const authCache = useAuthorizationCache(); const dbSelectionMenuRef = React.useRef<HTMLDivElement>(null); const [hovered, setHovered] = useState<string | null>(null); const [connecting, setConnecting] = useState<boolean>(false); const [dbSelectionMenuOpen, setDbSelectionMenuOpen] = useState<boolean>(false); - const [settingsMenuOpen, setSettingsMenuOpen] = useState<boolean>(false); - const [selectedDatabase, setSelectedDatabase] = useState<DatabaseInfo | null>(null); - const [addDatabaseFormOpen, setAddDatabaseFormOpen] = useState<boolean>(false); + const [settingsMenuOpen, setSettingsMenuOpen] = useState<'create' | 'update' | undefined>(undefined); + const [selectedSaveState, setSelectedSaveState] = useState<SaveStateI | null>(null); + // const [addDbConnectionFormOpen, setAddDbConnectionFormOpen] = useState<boolean>(false); useEffect(() => { const handleClickOutside = ({ target }: MouseEvent) => { @@ -45,7 +45,7 @@ export default function DatabaseSelector({}) { timeoutId = setTimeout(() => { dispatch(addError("Couldn't establish connection")); setConnecting(false); - dispatch(updateCurrentDatabase(undefined)); + dispatch(updateCurrentSaveState(undefined)); dispatch(clearQB()); dispatch(clearSchema()); }, 10000); @@ -58,39 +58,46 @@ export default function DatabaseSelector({}) { return ( <> - <SettingsForm - open={settingsMenuOpen} - database={selectedDatabase} - onClose={() => { - setSettingsMenuOpen(false); - }} - /> - <NewDatabaseForm - open={addDatabaseFormOpen} + {settingsMenuOpen !== undefined && ( + <SettingsForm + open={settingsMenuOpen} + saveState={settingsMenuOpen === 'update' ? selectedSaveState : null} + onClose={() => { + setSettingsMenuOpen(undefined); + }} + /> + )} + {/* <NewDatabaseForm + open={addDbConnectionFormOpen} onClose={() => { - setAddDatabaseFormOpen(false); + setAddDbConnectionFormOpen(false); }} - /> + /> */} <DropdownContainer ref={dbSelectionMenuRef} className="w-[20rem]"> <DropdownButton + disabled={connecting || authCache.authorized === false || !!authCache.roomID} title={ <div className="flex items-center"> - {connecting ? ( + {connecting && session.currentSaveState ? ( <> <LoadingSpinner /> - <p className="ml-2 truncate">Connecting to {session.currentDatabase}</p> + <p className="ml-2 truncate">Connecting to {session.saveStates[session.currentSaveState].name}</p> </> - ) : session.currentDatabase ? ( + ) : session.currentSaveState ? ( <> - <div className="h-2 w-2 rounded-full bg-success-500" /> - <p className="ml-2 truncate">Connected DB: {session.currentDatabase}</p> + <div + className={`h-2 w-2 rounded-full ${ + session.saveStates[session.currentSaveState].db.status === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500' + }`} + /> + <p className="ml-2 truncate">Connected DB: {session.saveStates[session.currentSaveState].name}</p> </> - ) : session.databases === undefined ? ( + ) : session.saveStates === undefined ? ( <> <LoadingSpinner /> <p className="ml-2">Retrieving databases</p> </> - ) : session.databases?.length === 0 ? ( + ) : Object.keys(session.saveStates).length === 0 ? ( <> <p className="ml-2">Add your first Database</p> </> @@ -103,12 +110,12 @@ export default function DatabaseSelector({}) { </div> } onClick={() => { - if (session.databases?.length === 0) setAddDatabaseFormOpen(true); + if (session.saveStates && Object.keys(session.saveStates).length === 0) setSettingsMenuOpen('create'); else setDbSelectionMenuOpen(!dbSelectionMenuOpen); }} /> - {dbSelectionMenuOpen && session.databases !== undefined && ( + {dbSelectionMenuOpen && session.saveStates !== undefined && ( <DropdownItemContainer align="top-10 w-full"> <li className="flex items-center p-2 hover:bg-secondary-50 cursor-pointer" @@ -116,11 +123,11 @@ export default function DatabaseSelector({}) { e.preventDefault(); setDbSelectionMenuOpen(false); setConnecting(false); - setAddDatabaseFormOpen(true); + setSettingsMenuOpen('create'); }} title="Add new database" > - {session.databases.length === 0 ? ( + {session.saveStates && Object.keys(session.saveStates).length === 0 ? ( <> <Add /> <p className="ml-2">Add your first database</p> @@ -132,43 +139,47 @@ export default function DatabaseSelector({}) { </> )} </li> - {session.databases.map((db) => ( + {Object.values(session.saveStates).map((save) => ( <li - key={db.Name} + key={save.id} className="flex justify-between items-center px-4 py-2 hover:bg-primary-100 gap-2 cursor-pointer" onClick={(e) => { - if (db.Name !== session.currentDatabase) { + if (save.id !== session.currentSaveState) { e.preventDefault(); setDbSelectionMenuOpen(false); setConnecting(true); - dispatch(updateCurrentDatabase(db.Name)); + dispatch(updateCurrentSaveState(save.id)); dispatch(clearQB()); dispatch(clearSchema()); } else { setDbSelectionMenuOpen(false); } }} - onMouseEnter={() => setHovered(db.Name)} + onMouseEnter={() => setHovered(save.id)} onMouseLeave={() => setHovered(null)} - title={`Connect to ${db.Name}`} + title={`Connect to ${save.name}`} > - <div className={`h-[8px] w-[8px] rounded-full shrink-0 ${db.status ? 'bg-success-500' : 'bg-danger-500'}`} /> + <div + className={`h-[8px] w-[8px] rounded-full shrink-0 ${ + save.db.status === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500' + }`} + /> <div className="w-full shrink min-w-0 flex flex-col"> - <p className="truncate w-full shrink-0 min-w-0">{db.Name}</p> + <p className="truncate w-full shrink-0 min-w-0">{save.name}</p> <p className="bg-light text-2xs text-secondary-500 truncate w-fit shrink-0 min-w-0 max-w-full h-full border border-secondary-200 rounded-sm p-0.5"> - {db.Protocol} - {db.URL} + {save.db.protocol} + {save.db.url} </p> </div> - {hovered === db.Name && ( + {hovered === save.id && ( <div className="flex items-center ml-2"> <div className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" onClick={(e) => { e.preventDefault(); e.stopPropagation(); - setSettingsMenuOpen(true); - setSelectedDatabase(db); + setSettingsMenuOpen('update'); + setSelectedSaveState(save); }} > <Settings /> @@ -177,12 +188,12 @@ export default function DatabaseSelector({}) { className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" onClick={(e) => { e.preventDefault(); - dispatch(updateCurrentDatabase(undefined)); + dispatch(updateCurrentSaveState(undefined)); dispatch(clearQB()); dispatch(clearSchema()); - api.DeleteDatabase(db.Name); + wsDeleteState(save.id); }} - title="Delete database" + title="Delete database connection" > <Delete /> </div> diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/AddDatabase/mockDatabases.ts b/apps/web/src/components/navbar/DatabaseManagement/forms/AddDatabase/mockDatabases.ts deleted file mode 100644 index b713e5e6156da46b5156885eaca2550abfb0415d..0000000000000000000000000000000000000000 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/AddDatabase/mockDatabases.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { DatabaseType } from '@graphpolaris/shared/lib/data-access'; - -export const mockDatabases = [ - { - name: 'Recommendations', - subtitle: 'Hosted by Neo4j', - username: 'recommendations', - password: 'recommendations', - url: 'demo.neo4jlabs.com', - port: 7687, - protocol: 'neo4j+s://', - internal_database_name: 'recommendations', - type: DatabaseType.Neo4j, - }, - { - name: 'Movies', - subtitle: 'Hosted by Neo4j', - username: 'movies', - password: 'movies', - url: 'demo.neo4jlabs.com', - port: 7687, - protocol: 'neo4j+s://', - internal_database_name: 'movies', - type: DatabaseType.Neo4j, - }, - { - name: 'Northwind', - subtitle: 'Hosted by Neo4j', - username: 'northwind', - password: 'northwind', - url: 'demo.neo4jlabs.com', - port: 7687, - protocol: 'neo4j+s://', - internal_database_name: 'northwind', - type: DatabaseType.Neo4j, - }, - { - name: 'Fincen', - subtitle: 'Hosted by Neo4j', - username: 'fincen', - password: 'fincen', - url: 'demo.neo4jlabs.com', - port: 7687, - protocol: 'neo4j+s://', - internal_database_name: 'fincen', - type: DatabaseType.Neo4j, - }, - { - name: 'Slack', - subtitle: 'Hosted by Neo4j', - username: 'slack', - password: 'slack', - url: 'demo.neo4jlabs.com', - port: 7687, - protocol: 'neo4j+s://', - internal_database_name: 'slack', - type: DatabaseType.Neo4j, - }, - { - name: 'Game of Thrones', - subtitle: 'Hosted by Neo4j', - username: 'gameofthrones', - password: 'gameofthrones', - url: 'demo.neo4jlabs.com', - port: 7687, - protocol: 'neo4j+s://', - internal_database_name: 'gameofthrones', - type: DatabaseType.Neo4j, - }, -]; diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/AddDatabase/newdatabase.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/AddDatabase/newdatabase.tsx deleted file mode 100644 index d07b1c67156edd778dadbefdeb16a942b266bd19..0000000000000000000000000000000000000000 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/AddDatabase/newdatabase.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { - AddDatabaseRequest, - DatabaseType, - databaseNameMapping, - databaseProtocolMapping, - useAppDispatch, - useDatabaseAPI, - useSchemaAPI, - useSessionCache, -} from '@graphpolaris/shared/lib/data-access'; -import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; -import { ErrorOutline } from '@mui/icons-material'; -import { mockDatabases } from './mockDatabases'; -import { Dialog } from '@graphpolaris/shared/lib/components/Dialog'; -import { Button } from '@graphpolaris/shared/lib/components/buttons'; -import Input from '@graphpolaris/shared/lib/components/inputs'; - -const INITIAL_DB_STATE = { - username: 'neo4j', - password: 'DevOnlyPass', - url: 'localhost', - port: 7687, - name: 'neo4j', - protocol: 'neo4j://', - internal_database_name: 'neo4j', - type: DatabaseType.Neo4j, -}; - -interface Connection { - connecting: boolean; - status: null | string; - verified: boolean | null; -} - -export const NewDatabaseForm = (props: { onClose(): void; open: boolean }) => { - const dispatch = useAppDispatch(); - const ref = useRef<HTMLDialogElement>(null); - const [state, setState] = useState<AddDatabaseRequest>(INITIAL_DB_STATE); - const api = useDatabaseAPI(); - const schemaApi = useSchemaAPI(); - const session = useSessionCache(); - const [hasError, setHasError] = useState({}); - const [sampleData, setSampleData] = useState<boolean | null>(false); - const [connection, setConnection] = useState<Connection>({ - connecting: false, - status: null, - verified: null, - }); - - useEffect(() => { - if (!state) return; - if (state.type === DatabaseType.ArangoDB && state.port === 7687) { - setState({ ...state, port: 8529 }); - } else if (state.type === DatabaseType.Neo4j && state.port === 8529) { - setState({ ...state, port: 7687 }); - } - }, [state.type]); - - function handleInputChange(field: string, value: unknown) { - setState({ - ...state, - [field]: value, - }); - } - - async function testDatabaseConnection() { - setConnection(() => ({ - connecting: true, - status: 'Verifying database connection', - verified: null, - })); - - if (session.databases && !session.databases.hasOwnProperty(state.name)) { - setConnection((prevState) => ({ - ...prevState, - connecting: false, - status: 'Database already connected', - verified: false, - })); - return; - } - - try { - api - .TestDatabaseConnection(state) - .then((res: any) => { - setConnection((prevState) => ({ - ...prevState, - status: res.message, - verified: res.verified, - })); - }) - .catch((error) => { - setConnection((prevState) => ({ - ...prevState, - connecting: false, - status: 'Database connection failed', - verified: false, - })); - }); - } catch (e) { - setConnection((prevState) => ({ - ...prevState, - status: 'Database connection failed', - verified: false, - })); - } - } - - function loadMockDatabase({ username, password, url, port, name, protocol, internal_database_name, type }: AddDatabaseRequest) { - setState((prevState) => ({ - ...prevState, - username, - password, - url, - port, - name, - protocol, - internal_database_name, - type, - })); - setSampleData(false); - } - - function handlePortChanged(port: string): void { - if (!isNaN(Number(port))) setState({ ...state, port: Number(port) }); - } - - useEffect(() => { - if (connection.verified === true) { - try { - api - .AddDatabase(state, { updateDatabaseCache: true, setAsCurrent: true }) - .then(() => { - schemaApi.RequestSchema(state.name); - }) - .catch((e) => { - dispatch(addError(e.message)); - }); - } catch (e: any) { - dispatch(addError(e.message)); - } finally { - closeDialog(); - } - } - }, [connection.verified]); - - function closeDialog(): void { - setConnection({ - connecting: false, - status: null, - verified: null, - }); - setState(INITIAL_DB_STATE); - setSampleData(false); - props.onClose(); - ref.current?.close(); - } - - return ( - <Dialog open={props.open} onClose={props.onClose} className="lg:min-w-[50rem] "> - <div className="flex justify-between align-center"> - <h1 className="text-xl font-bold">New Database</h1> - <div> - {sampleData ? ( - <Button variant="outline" label="Go back" onClick={() => setSampleData(false)} /> - ) : ( - <> - <h1 className="font-light text-xs">No data?</h1> - <p className="font-light text-sm cursor-pointer underline" onClick={() => setSampleData(true)}> - Try sample data - </p> - </> - )} - </div> - </div> - - {sampleData ? ( - <div className="grid grid-cols-2 lg:grid-cols-3 gap-2"> - {mockDatabases.map((sample) => ( - <div - key={sample.name} - className="card hover:bg-secondary-100 cursor-pointer mb-2 border w-[15rem]" - onClick={() => loadMockDatabase(sample)} - > - <div className="card-body"> - <h2 className="card-title">{sample.name}</h2> - <p className="font-light text-secondary-400">{sample.subtitle}</p> - </div> - </div> - ))} - </div> - ) : ( - <> - <Input - type="text" - label="Name of the database" - value={state.name} - placeholder="neo4j" - required - errorText="This field is required" - validate={(v) => { - setHasError({ ...hasError, name: v.length === 0 }); - return v.length > 0; - }} - onChange={(value: string) => handleInputChange('name', value)} - /> - - <Input - type="text" - label="Internal database name" - value={state.internal_database_name} - placeholder="internal_database_name" - required - errorText="This field is required" - validate={(v) => { - setHasError({ ...hasError, internal_database_name: v.length === 0 }); - return v.length > 0; - }} - onChange={(value: string) => handleInputChange('internal_database_name', value)} - /> - - <div className="flex w-full gap-2"> - <Input - type="dropdown" - label="Database Type" - required - value={databaseNameMapping[state.type]} - options={databaseNameMapping} - onChange={(value: string | number) => { - setState({ - ...state, - type: databaseNameMapping.indexOf(String(value)), - }); - }} - /> - - <Input - type="dropdown" - label="Database Protocol" - required - value={state.protocol} - options={databaseProtocolMapping} - onChange={(value: string | number) => { - setState({ - ...state, - protocol: String(value), - }); - }} - /> - </div> - - <div className="flex w-full gap-2"> - <Input - type="text" - label="Hostname/IP" - value={state.url} - placeholder="neo4j" - required - errorText="This field is required" - validate={(v) => { - setHasError({ ...hasError, url: v.length === 0 }); - return v.length > 0; - }} - onChange={(value: string) => handleInputChange('url', value)} - /> - - <Input - type="text" - label="Port" - value={state.port.toString()} - placeholder="neo4j" - required - errorText="Must be between 1 and 9999" - validate={(v) => { - setHasError({ ...hasError, port: !(v <= 9999 && v > 0) }); - return v <= 9999 && v > 0; - }} - onChange={(value: string) => handlePortChanged(value)} - /> - </div> - - <div className="flex w-full gap-2"> - <Input - type="text" - label="Username" - value={state.username} - placeholder="username" - required - errorText="This field is required" - validate={(v) => { - setHasError({ ...hasError, username: v.length === 0 }); - return v.length > 0; - }} - onChange={(value: string) => handleInputChange('username', value)} - /> - - <Input - type="text" - label="Password" - value={state.password} - placeholder="password" - required - visible={false} - errorText="This field is required" - validate={(v) => { - setHasError({ ...hasError, password: v.length === 0 }); - return v.length > 0; - }} - onChange={(value: string) => handleInputChange('password', value)} - /> - </div> - - {!(connection.status === null) && ( - <div className={`flex flex-col justify-center items-center`}> - <div className="flex justify-center items-center"> - {connection.verified === false && <ErrorOutline className="text-secondary-400" />} - <p className="font-light text-sm text-secondary-400 ">{connection.status}</p> - </div> - {connection.verified === null && <progress className="progress w-56"></progress>} - </div> - )} - <div className="grid gap-2"> - <Button - type="primary" - block - label={connection.connecting ? 'Connecting...' : 'Connect'} - onClick={(event) => { - event.preventDefault(); - testDatabaseConnection(); - }} - disabled={connection.connecting || Object.values(hasError).some((e) => e === true)} - /> - <Button - variant="outline" - block - label="Cancel" - onClick={(event) => { - event.preventDefault(); - closeDialog(); - }} - /> - </div> - </> - )} - </Dialog> - ); -}; diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f7ac983b8eed74e033f4723291ff7c2e8a652593 --- /dev/null +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx @@ -0,0 +1,180 @@ +import React, { useEffect, useState } from 'react'; +import { + DatabaseInfo, + DatabaseType, + SaveStateI, + databaseNameMapping, + databaseProtocolMapping, + nilUUID, +} from '@graphpolaris/shared/lib/data-access'; +import { sampleSaveStates } from './mockSaveStates'; +import Input from '@graphpolaris/shared/lib/components/inputs'; +import { useImmer } from 'use-immer'; + +export const INITIAL_SAVE_STATE: SaveStateI = { + id: nilUUID, + name: 'Untitled', + db: { + username: 'neo4j', + password: 'DevOnlyPass', + url: 'localhost', + port: 7687, + protocol: 'neo4j://', + internalDatabaseName: 'neo4j', + type: DatabaseType.Neo4j, + }, +}; + +export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveStateI, error: boolean) => void }) => { + const [formData, setFormData] = useImmer(props.data); + const [hasError, setHasError] = useState<Record<string, boolean>>({}); + + function handlePortChanged(port: string): void { + if (!isNaN(Number(port))) + setFormData((draft) => { + draft.db.port = Number(port); + return draft; + }); + } + + useEffect(() => { + props.onChange(formData, Object.values(hasError).includes(true)); + }, [formData]); + + return ( + <> + <Input + type="text" + label="Name of database" + value={formData.name} + onChange={(value: string) => + setFormData((draft) => { + draft.name = value; + }) + } + /> + + <Input + type="text" + label="Internal database name" + value={formData.db.internalDatabaseName} + placeholder="internalDatabaseName" + required + errorText="This field is required" + validate={(v) => { + setHasError({ ...hasError, internalDatabaseName: v.length === 0 }); + return v.length > 0; + }} + onChange={(value: string) => + setFormData((draft) => { + draft.db.internalDatabaseName = value; + }) + } + /> + + <div className="flex w-full gap-2"> + <Input + type="dropdown" + label="Database Type" + required + value={databaseNameMapping[formData.db.type]} + options={databaseNameMapping} + onChange={(value: string | number) => { + setFormData((draft) => { + draft.db.type = databaseNameMapping.indexOf(value.toString()); + }); + }} + /> + + <Input + type="dropdown" + label="Database Protocol" + required + value={formData.db.protocol} + options={databaseProtocolMapping} + onChange={(value: string | number) => { + setFormData((draft) => { + draft.db.protocol = value.toString(); + }); + }} + /> + </div> + + <div className="flex w-full gap-2"> + <Input + type="text" + label="Hostname/IP" + value={formData.db.url} + placeholder="neo4j" + required + errorText="This field is required" + validate={(v) => { + setHasError({ ...hasError, url: v.length === 0 }); + return v.length > 0; + }} + onChange={(value: string) => { + setFormData((draft) => { + draft.db.url = value; + }); + }} + /> + + <Input + type="text" + label="Port" + value={formData.db.port.toString()} + placeholder="neo4j" + required + errorText="Must be between 1 and 9999" + validate={(v) => { + setHasError({ ...hasError, port: !(v <= 9999 && v > 0) }); + return v <= 9999 && v > 0; + }} + onChange={(value: string) => { + setFormData((draft) => { + draft.db.port = Number(value); + }); + }} + /> + </div> + + <div className="flex w-full gap-2"> + <Input + type="text" + label="Username" + value={formData.db.username} + placeholder="username" + required + errorText="This field is required" + validate={(v) => { + setHasError({ ...hasError, username: v.length === 0 }); + return v.length > 0; + }} + onChange={(value: string) => { + setFormData((draft) => { + draft.db.username = value; + }); + }} + /> + + <Input + type="text" + label="Password" + value={formData.db.password} + placeholder="password" + required + errorText="This field is required" + validate={(v) => { + setHasError({ ...hasError, password: v.length === 0 }); + return v.length > 0; + }} + onChange={(value: string) => { + setFormData((draft) => { + draft.db.password = value; + }); + }} + /> + </div> + </> + ); +}; diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ffe4c41be14f32e998eed4956740f4e04f7908c1 --- /dev/null +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { DatabaseInfo } from '@graphpolaris/shared/lib/data-access'; +import { DatabaseType, SaveStateI, nilUUID } from '@graphpolaris/shared/lib/data-access/api/wsState'; + +export type SaveStateSampleI = SaveStateI & { + subtitle: string; +}; + +export const sampleSaveStates: Array<SaveStateSampleI> = [ + { + id: nilUUID, + name: 'Recommendations', + subtitle: 'Hosted by Neo4j', + db: { + username: 'recommendations', + password: 'recommendations', + url: 'demo.neo4jlabs.com', + port: 7687, + protocol: 'neo4j+s://', + internalDatabaseName: 'recommendations', + type: DatabaseType.Neo4j, + }, + }, + { + id: nilUUID, + name: 'Movies', + subtitle: 'Hosted by Neo4j', + db: { + username: 'movies', + password: 'movies', + url: 'demo.neo4jlabs.com', + port: 7687, + protocol: 'neo4j+s://', + internalDatabaseName: 'movies', + type: DatabaseType.Neo4j, + }, + }, + { + id: nilUUID, + name: 'Northwind', + subtitle: 'Hosted by Neo4j', + db: { + username: 'northwind', + password: 'northwind', + url: 'demo.neo4jlabs.com', + port: 7687, + protocol: 'neo4j+s://', + internalDatabaseName: 'northwind', + type: DatabaseType.Neo4j, + }, + }, + { + id: nilUUID, + name: 'Fincen', + subtitle: 'Hosted by Neo4j', + db: { + username: 'fincen', + password: 'fincen', + url: 'demo.neo4jlabs.com', + port: 7687, + protocol: 'neo4j+s://', + internalDatabaseName: 'fincen', + type: DatabaseType.Neo4j, + }, + }, + { + id: nilUUID, + name: 'Slack', + subtitle: 'Hosted by Neo4j', + db: { + username: 'slack', + password: 'slack', + url: 'demo.neo4jlabs.com', + port: 7687, + protocol: 'neo4j+s://', + internalDatabaseName: 'slack', + type: DatabaseType.Neo4j, + }, + }, + { + id: nilUUID, + name: 'Game of Thrones', + subtitle: 'Hosted by Neo4j', + db: { + username: 'gameofthrones', + password: 'gameofthrones', + url: 'demo.neo4jlabs.com', + port: 7687, + protocol: 'neo4j+s://', + internalDatabaseName: 'gameofthrones', + type: DatabaseType.Neo4j, + }, + }, +]; + +export const SampleDatabaseSelector = (props: { onClick: (data: SaveStateI) => void }) => { + return ( + <div className="grid grid-cols-2 lg:grid-cols-3 gap-2"> + {sampleSaveStates.map((sample) => ( + <div + key={sample.name} + className="card hover:bg-secondary-100 cursor-pointer mb-2 border w-[15rem]" + onClick={() => props.onClick(sample as SaveStateI)} + > + <div className="card-body"> + <h2 className="card-title">{sample.name}</h2> + <p className="font-light text-secondary-400">{sample.subtitle}</p> + </div> + </div> + ))} + </div> + ); +}; diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx index 9c6fc99afd4806f72bb04918bb65d9f01ec73c45..859a6fd905e843716e7f8feca7e5322a4fbbee04 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx @@ -1,125 +1,138 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; import { - AddDatabaseRequest, databaseNameMapping, databaseProtocolMapping, useAppDispatch, - useDatabaseAPI, - DatabaseInfo, + SaveStateI, + wsUpdateState, + DatabaseStatus, + wsTestDatabaseConnection, + TestDatabaseConnectionResponse, + wsCreateState, + nilUUID, + DatabaseType, } 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 { DatabaseForm } from './databaseForm'; +import { SampleDatabaseSelector } from './mockSaveStates'; + +export const INITIAL_SAVE_STATE: SaveStateI = { + id: nilUUID, + name: 'Untitled', + db: { + username: 'neo4j', + password: 'DevOnlyPass', + url: 'localhost', + port: 7687, + protocol: 'neo4j://', + internalDatabaseName: 'neo4j', + type: DatabaseType.Neo4j, + }, +}; -interface Connection { +type Connection = { updating: boolean; status: null | string; verified: boolean | null; -} - -const DEFAULT_DB: AddDatabaseRequest = { - name: '', - internal_database_name: '', - url: '', - port: 7687, - protocol: '', - username: '', - password: '', - type: 0, }; -export const SettingsForm = (props: { onClose(): void; open: boolean; database: DatabaseInfo | null }) => { +export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update'; saveState: SaveStateI | null }) => { const dispatch = useAppDispatch(); const ref = useRef<HTMLDialogElement>(null); - const [state, setState] = useState<AddDatabaseRequest>(DEFAULT_DB); - const api = useDatabaseAPI(); - const [hasError, setHasError] = useState({}); + const [formData, setFormData] = useImmer(props.saveState && props.open === 'update' ? props.saveState : INITIAL_SAVE_STATE); + const [hasError, setHasError] = useState(false); + const [sampleDataPanel, setSampleDataPanel] = useState<boolean | null>(false); const [connection, setConnection] = useState<Connection>({ updating: false, status: null, verified: null, }); + const formDataRef = useRef<SaveStateI | null>(null); + const formTitle = props.open === 'create' ? 'Create' : 'Update'; + + const refImperativeHandles = useRef<any>(null); + useImperativeHandle(refImperativeHandles, () => ({ + processDbTested: (data: TestDatabaseConnectionResponse) => { + if (!formDataRef.current) { + console.error('formDataRef.current is null'); + return; + } + if (data.status === 'success') { + setConnection((prevState) => ({ + updating: false, + status: 'Database connection verified', + verified: true, + })); + if (props.open === 'create') { + wsCreateState(formDataRef.current); + } else { + wsUpdateState(formDataRef.current); + } + } else { + setConnection((prevState) => ({ + updating: false, + status: 'Database connection failed', + verified: false, + })); + } + }, + processStateUpdated: (data: SaveStateI) => { + let _data = JSON.parse(JSON.stringify(data)); + _data.db.status = DatabaseStatus.tested; + dispatch(addSaveState(_data)); + formDataRef.current = null; + closeDialog(); + }, + })); useEffect(() => { - const db = props.database; - if (db) { - setState({ - name: db.Name, - internal_database_name: db.InternalDatabaseName, - url: db.URL, - port: db.Port, - protocol: db.Protocol, - username: db.Username, - password: db.Password, - type: db.Type, - }); - } - }, [props.database]); + Broker.instance().subscribe(refImperativeHandles.current.processDbTested, 'tested_connection'); + Broker.instance().subscribe(refImperativeHandles.current.processStateUpdated, 'updated_save_state'); + Broker.instance().subscribe(refImperativeHandles.current.processStateUpdated, 'save_state'); + + return () => { + Broker.instance().unSubscribeAll('tested_connection'); + Broker.instance().unSubscribeAll('updated_save_state'); + Broker.instance().unSubscribeAll('save_state'); + }; + }, []); useEffect(() => { - if (connection.verified) { - api.GetAllDatabases().catch((e) => console.debug(e)); - closeDialog(); + if (props.saveState && props.open === 'update') { + setFormData(props.saveState); + setSampleDataPanel(null); + } else { + setSampleDataPanel(false); } - }, [connection.verified]); + }, [props.saveState]); - function handleInputChange(field: keyof AddDatabaseRequest, value: string) { - if (field != 'port' && field != 'type') { - setState((prevState) => ({ ...prevState, [field]: value })); - } - } + useEffect(() => { + formDataRef.current = formData; + }, [formData]); async function handleSubmit() { setConnection(() => ({ updating: true, - status: 'Updating database credentials', + status: formTitle.slice(0, -1) + 'ing database credentials', verified: null, })); - if (props.database != null) { - try { - api - .UpdateDatabase({ - name: props.database.Name, - internal_database_name: state.internal_database_name, - password: state.password, - port: state.port, - protocol: state.protocol, - type: state.type, - url: state.url, - username: state.username, - }) - .then(() => { - setConnection(() => ({ - updating: false, - status: 'Worked', - verified: true, - })); - }) - .catch((e) => { - dispatch(addError('Database updating went wrong')); - setConnection((prevState) => ({ - ...prevState, - updating: false, - status: 'Database updating went wrong', - verified: false, - })); - }); - } catch (e) { - dispatch(addError('An error occured')); - setConnection((prevState) => ({ - updating: false, - status: 'An error occured', - verified: false, - })); - } - } + wsTestDatabaseConnection(formData.db); } function handlePortChanged(port: string): void { - if (!isNaN(Number(port))) setState({ ...state, port: Number(port) }); + if (!isNaN(Number(port))) + setFormData((draft) => { + draft.db.port = Number(port); + return draft; + }); } function closeDialog(): void { @@ -128,124 +141,51 @@ export const SettingsForm = (props: { onClose(): void; open: boolean; database: status: null, verified: null, }); - setState(DEFAULT_DB); - props.onClose(); + setFormData(INITIAL_SAVE_STATE); ref.current?.close(); + props.onClose(); } return ( - <Dialog open={props.open} onClose={props.onClose}> + <Dialog open={!!props.open} onClose={props.onClose} className="lg:min-w-[50rem]"> <div className="flex justify-between align-center"> - <h1 className="card-title">Update {state.name} Database</h1> + <h1 className="text-xl font-bold"> + {formTitle} {formData.name} Database + </h1> + <div> + {sampleDataPanel === true ? ( + <Button variant="outline" label="Go back" onClick={() => setSampleDataPanel(false)} /> + ) : sampleDataPanel === false ? ( + <> + <h1 className="font-light text-xs">No data?</h1> + <p className="font-light text-sm cursor-pointer underline" onClick={() => setSampleDataPanel(true)}> + Try sample data + </p> + </> + ) : ( + '' + )} + </div> </div> <> - <Input type="text" label="Name of database" value={state.name} onChange={() => {}} disabled /> - - <Input - type="text" - label="Internal database name" - value={state.internal_database_name} - placeholder="internal_database_name" - required - errorText="This field is required" - validate={(v) => { - setHasError({ ...hasError, internal_database_name: v.length === 0 }); - return v.length > 0; - }} - onChange={(value: string) => handleInputChange('internal_database_name', value)} - /> - - <div className="flex w-full gap-2"> - <Input - type="dropdown" - label="Database Type" - required - value={databaseNameMapping[state.type]} - options={databaseNameMapping} - onChange={(value: string | number) => { - setState({ - ...state, - type: databaseNameMapping.indexOf(String(value)), - }); - }} - /> - - <Input - type="dropdown" - label="Database Protocol" - required - value={state.protocol} - options={databaseProtocolMapping} - onChange={(value: string | number) => { - setState({ - ...state, - protocol: String(value), - }); - }} - /> - </div> - - <div className="flex w-full gap-2"> - <Input - type="text" - label="Hostname/IP" - value={state.url} - placeholder="neo4j" - required - errorText="This field is required" - validate={(v) => { - setHasError({ ...hasError, url: v.length === 0 }); - return v.length > 0; - }} - onChange={(value: string) => handleInputChange('url', value)} - /> - - <Input - type="text" - label="Port" - value={state.port.toString()} - placeholder="neo4j" - required - errorText="Must be between 1 and 9999" - validate={(v) => { - setHasError({ ...hasError, port: !(v <= 9999 && v > 0) }); - return v <= 9999 && v > 0; - }} - onChange={(value: string) => handlePortChanged(value)} - /> - </div> - - <div className="flex w-full gap-2"> - <Input - type="text" - label="Username" - value={state.username} - placeholder="username" - required - errorText="This field is required" - validate={(v) => { - setHasError({ ...hasError, username: v.length === 0 }); - return v.length > 0; + {sampleDataPanel === true ? ( + <SampleDatabaseSelector + onClick={(data) => { + setFormData(data); + setHasError(false); + setSampleDataPanel(false); }} - onChange={(value: string) => handleInputChange('username', value)} /> - - <Input - type="text" - label="Password" - value={state.password} - visible={false} - placeholder="password" - required - errorText="This field is required" - validate={(v) => { - setHasError({ ...hasError, password: v.length === 0 }); - return v.length > 0; + ) : ( + <DatabaseForm + data={formData} + onChange={(data: SaveStateI, error: boolean) => { + setFormData({ ...data, id: formData.id }); + setHasError(error); }} - onChange={(value: string) => handleInputChange('password', value)} /> - </div> + )} {!(connection.status === null) && ( <div className={`flex flex-col justify-center items-center`}> @@ -260,12 +200,12 @@ export const SettingsForm = (props: { onClose(): void; open: boolean; database: <div className="grid md:grid-cols-2 gap-3 card-actions w-full justify-stretch items-center"> <Button type="primary" - label={connection.updating ? 'Updating...' : 'Update'} + label={connection.updating ? formTitle.slice(0, -1) + 'ing...' : formTitle} onClick={(event) => { event.preventDefault(); handleSubmit(); }} - disabled={connection.updating || Object.values(hasError).some((e) => e === true)} + disabled={connection.updating || hasError} /> <Button variant="outline" diff --git a/apps/web/src/components/navbar/databasemenu.tsx b/apps/web/src/components/navbar/databasemenu.tsx index 9f53a853a5e2f973dfed45b0d35a927b33c24d8e..15a2e2f18c995b282869d9c37dd58797c7035b7e 100644 --- a/apps/web/src/components/navbar/databasemenu.tsx +++ b/apps/web/src/components/navbar/databasemenu.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import { useSessionCache } from '@graphpolaris/shared/lib/data-access'; +import { SaveStateI, useSessionCache } from '@graphpolaris/shared/lib/data-access'; export const DatabaseMenu = (props: { onClick: (database: string) => void }) => { const session = useSessionCache(); return ( <ul className="menu dropdown-content absolute right-48 z-[1] p-2 shadow-xl bg-secondary-50 rounded-box w-52" tabIndex={0}> - {session.databases && - session.databases.map((db: any) => ( - <li key={db.Name}> - <button onClick={() => props.onClick(db.Name)}>{db.Name}</button> + {session.saveStates && + Object.values(session.saveStates).map((ss: SaveStateI) => ( + <li key={ss.name}> + <button onClick={() => props.onClick(ss.name)}>{ss.name}</button> </li> ))} </ul> diff --git a/apps/web/src/components/navbar/navbar.tsx b/apps/web/src/components/navbar/navbar.tsx index e635ca6f0bdc7a7a5752ae660c8512dc3530ec76..47f714926ded8ca9ae2ca520a2e4c6221ac88cc7 100644 --- a/apps/web/src/components/navbar/navbar.tsx +++ b/apps/web/src/components/navbar/navbar.tsx @@ -11,16 +11,17 @@ import React, { useState, useRef, useEffect } from 'react'; import logo_white from './gp-logo-white.svg'; import logo from './gp-logo.svg'; -import { useAuthorizationCache } from '@graphpolaris/shared/lib/data-access'; +import { useAuthorizationCache, useAuth } from '@graphpolaris/shared/lib/data-access'; import { SearchBar } from './search/SearchBar'; -import DatabaseSelector from './DatabaseManagement/DatabaseSelector'; +import DatabaseSelector from './DatabaseManagement/dbConnectionSelector'; import { DropdownItem, DropdownItemContainer } from '@graphpolaris/shared/lib/components/dropdowns'; import ColorMode from '@graphpolaris/shared/lib/components/color-mode'; import GpLogo from './gp-logo'; export const Navbar = () => { const dropdownRef = useRef<HTMLDivElement>(null); - const auth = useAuthorizationCache(); + const auth = useAuth(); + const authCache = useAuthorizationCache(); const [menuOpen, setMenuOpen] = useState(false); const currentLogo = !'dark' ? logo_white : logo; // TODO: support dark mode @@ -55,32 +56,35 @@ export const Navbar = () => { className="relative inline-flex items-center justify-center w-8 h-8 overflow-hidden bg-secondary-500 rounded hover:bg-secondary-600 transition-colors duration-150 ease-in-out cursor-pointer" onClick={() => setMenuOpen(!menuOpen)} > - <span className="font-medium text-light">{auth.username?.slice(0, 2).toUpperCase()}</span> + <span className="font-medium text-light">{authCache.username?.slice(0, 2).toUpperCase()}</span> </div> {menuOpen && ( <DropdownItemContainer className="w-56" align="right-7"> <div className="menu-title border-b"> - <h2>user: {auth.username}</h2> - <h3 className="text-xs break-words">session: {auth.sessionID}</h3> + <h2>user: {authCache.username}</h2> + <h3 className="text-xs break-words">session: {authCache.sessionID}</h3> </div> - {auth.authorized ? ( + {authCache.authorized ? ( <> <DropdownItem value="Share" - submenu={ - <> - <DropdownItem value="Visual" onClick={() => {}} /> - <DropdownItem value="Knowledge base" onClick={() => {}} /> - </> - } + onClick={() => { + auth.newShareRoom(); + }} + // submenu={ + // <> + // <DropdownItem value="Visual" onClick={() => {}} /> + // <DropdownItem value="Knowledge base" onClick={() => {}} /> + // </> + // } /> <DropdownItem value="Advanced" submenu={ <> - <DropdownItem value="" onClick={() => {}} /> + <DropdownItem value="TBD" onClick={() => {}} /> </> } /> @@ -93,6 +97,11 @@ export const Navbar = () => { </> )} + {authCache?.roomID && ( + <div className="menu-title border-b"> + <h3 className="text-xs break-words">Share ID: {authCache.roomID}</h3> + </div> + )} <div className="menu-title border-t"> <h3 className="text-xs">Version: {buildInfo}</h3> </div> diff --git a/libs/shared/lib/components/dropdowns/index.tsx b/libs/shared/lib/components/dropdowns/index.tsx index 4ce20ad797d0c3eb1d839900f836b42311994e64..96ec000e594db2fc1ef5bf1c8010ce16d648545a 100644 --- a/libs/shared/lib/components/dropdowns/index.tsx +++ b/libs/shared/lib/components/dropdowns/index.tsx @@ -20,14 +20,16 @@ type DropdownButtonProps = { title: string | ReactNode; onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; size?: 'xs' | 'sm' | 'md' | 'xl'; + disabled?: boolean; }; -export function DropdownButton({ title, onClick, size }: DropdownButtonProps) { +export function DropdownButton({ title, onClick, size, disabled }: DropdownButtonProps) { return ( <> <button - className="inline-flex w-full justify-between items-center gap-x-1.5 rounded bg-light px-3 py-2 text-secondary-900 shadow-sm ring-1 ring-inset ring-secondary-300 hover:bg-secondary-50" + className="inline-flex w-full justify-between items-center gap-x-1.5 rounded bg-light px-3 py-2 text-secondary-900 shadow-sm ring-1 ring-inset ring-secondary-300 hover:bg-secondary-50 disabled:bg-secondary-100 disabled:cursor-not-allowed disabled:text-secondary-400" onClick={onClick} + disabled={disabled} > <span className={`text-${size}`}>{title}</span> <svg className="-mr-1 h-5 w-5 text-secondary-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> diff --git a/libs/shared/lib/data-access/api/database.ts b/libs/shared/lib/data-access/api/database.ts deleted file mode 100644 index a8bd519114f3ebf4ac7bc78c09bbc246be976c25..0000000000000000000000000000000000000000 --- a/libs/shared/lib/data-access/api/database.ts +++ /dev/null @@ -1,168 +0,0 @@ -// All database related API calls - -import { useAuth } from '../authorization'; -import { useAppDispatch, useSessionCache } from '../store'; -import { clearQB } from '../store/querybuilderSlice'; -import { clearSchema } from '../store/schemaSlice'; -import { updateCurrentDatabase, updateDatabaseList } from '../store/sessionSlice'; - -export enum DatabaseType { - ArangoDB = 0, - Neo4j = 1, -} - -export const databaseNameMapping: string[] = ['arangodb', 'neo4j']; -export const databaseProtocolMapping: string[] = ['neo4j://', 'neo4j+s://', 'bolt://', 'bolt+s://']; - -export type AddDatabaseRequest = { - name: string; - internal_database_name: string; - url: string; - port: number; - protocol: string; - username: string; - password: string; - type: DatabaseType; // Database type. 0 = ArangoDB, 1 = Neo4j -}; - -export type DatabaseInfo = { - Name: string; - InternalDatabaseName: string; - URL: string; - Port: number; - Protocol: string; - Username: string; - Password: string; - Type: number; - status: boolean; -}; - -export type AddDatabaseOptions = { - setAsCurrent?: boolean; - updateDatabaseCache?: boolean; -}; - -export type GetDatabasesOptions = { - updateSessionCache?: boolean; -}; - -export type DeleteDatabasesOptions = { - updateSessionCache?: boolean; -}; - -export type VerifyConnectionRequest = string; - -export const useDatabaseAPI = () => { - const cache = useSessionCache(); - const dispatch = useAppDispatch(); - const domain = import.meta.env.BACKEND_URL; - const useruri = import.meta.env.BACKEND_USER; - const { fetchAuthenticated } = useAuth(); - - function AddDatabase(request: AddDatabaseRequest, options: AddDatabaseOptions = {}): Promise<void> { - const { setAsCurrent = true, updateDatabaseCache = false } = options; - return new Promise((resolve, reject) => { - fetchAuthenticated(`${domain}${useruri}/database`, { - method: 'POST', - body: JSON.stringify(request), - }) - .then((response: Response) => { - console.info('Added Database', response); - if (!response.ok) { - reject(response.statusText); - } - if (setAsCurrent){ - dispatch(updateCurrentDatabase(request.name)); - dispatch(clearQB()); - dispatch(clearSchema()); - } - if (updateDatabaseCache) GetAllDatabases({ updateSessionCache: true }).catch(reject); - - resolve(); - }) - .catch(reject); - }); - } - - function GetAllDatabases(options: GetDatabasesOptions = {}): Promise<Array<string>> { - const { updateSessionCache: updateDatabaseCache = true } = options; - return new Promise((resolve, reject) => { - fetchAuthenticated(`${domain}${useruri}/database`) - .then((response: Response) => { - if (!response.ok) { - new Promise((resolve, reject) => { - reject(response.statusText); - }); - } - response - .json() - .then((json) => { - if (updateDatabaseCache) dispatch(updateDatabaseList(json.databases)); - resolve(json.databases); - }) - .catch(reject); - }) - .catch(reject); - }); - } - - function DeleteDatabase(name: string, options: DeleteDatabasesOptions = {}): Promise<void> { - const { updateSessionCache: updateDatabaseCache = true } = options; - return new Promise((resolve, reject) => { - fetchAuthenticated(`${domain}${useruri}/database/` + name, { - method: 'DELETE', - }) - .then((response: Response) => { - if (!response.ok) { - reject(response.statusText); - } - - if (updateDatabaseCache) GetAllDatabases({ updateSessionCache: true }).catch(reject); - - resolve(); - }) - .catch(reject); - }); - } - - function TestDatabaseConnection(request: AddDatabaseRequest): Promise<void> { - return new Promise((resolve, reject) => { - fetchAuthenticated(`${domain}${useruri}/database/test-connection`, { - method: 'POST', - body: JSON.stringify(request), - }) - .then((response: Response) => { - if (!response.ok) { - reject(response.statusText); - } - resolve(response.json()); - }) - .catch(reject); - }); - } - - function UpdateDatabase(request: AddDatabaseRequest): Promise<void> { - return new Promise((resolve, reject) => { - fetchAuthenticated(`${domain}${useruri}/database/update`, { - method: 'PATCH', - body: JSON.stringify(request), - }) - .then((response: Response) => { - if (!response.ok) { - reject(response.statusText); - } - resolve(); - }) - .catch(reject); - }); - } - - return { - DatabaseType, - AddDatabase, - GetAllDatabases, - DeleteDatabase, - TestDatabaseConnection, - UpdateDatabase, - }; -}; diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1f41476ff8264c7e98336725648fedd00dc5f6a3 --- /dev/null +++ b/libs/shared/lib/data-access/api/eventBus.tsx @@ -0,0 +1,126 @@ +import { + useAuth, + useAuthorizationCache, + useAppDispatch, + useSessionCache, + useQuerybuilderGraph, + useQuerybuilderHash, + useML, + useMLEnabledHash, + useConfig, + useQuerybuilderSettings, + readInSchemaFromBackend, + assignNewGraphQueryResult, + setQuerybuilderNodes, + resetGraphQueryResults, + useQuerybuilder, +} from '@graphpolaris/shared/lib/data-access'; +import { WebSocketHandler } from '@graphpolaris/shared/lib/data-access/socket'; +import Broker from '@graphpolaris/shared/lib/data-access/socket/broker'; +import { addError, addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import { GraphQueryResultFromBackendPayload, queryingBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; +import { allMLTypes, LinkPredictionInstance, setMLResult } from '@graphpolaris/shared/lib/data-access/store/mlSlice'; +import { QueryBuilderState } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; +import { QueryMultiGraph, Query2BackendQuery } from '@graphpolaris/shared/lib/querybuilder'; +import { SchemaFromBackend } from '@graphpolaris/shared/lib/schema'; +import { useRef, useState, useEffect } from 'react'; +import { DatabaseInfo, DatabaseStatus, SaveStateI, TestDatabaseConnectionResponse, wsGetStates } from './wsState'; +import { wsSchemaRequest } from './wsSchema'; +import { addSaveState, testedSaveState, updateSaveStateList } from '../store/sessionSlice'; + +export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }) => { + const { login } = useAuth(); + const auth = useAuthorizationCache(); + const dispatch = useAppDispatch(); + const session = useSessionCache(); + const queryHash = useQuerybuilderHash(); + const queryBuilder = useQuerybuilder(); + const mlHash = useMLEnabledHash(); + const queryBuilderSettings = useQuerybuilderSettings(); + + useEffect(() => { + // Default + Broker.instance().subscribe((data: SchemaFromBackend) => { + dispatch(readInSchemaFromBackend(data)); + dispatch(addInfo('Schema graph updated')); + }, 'schema_result'); + Broker.instance().subscribe((data: GraphQueryResultFromBackendPayload) => { + dispatch(assignNewGraphQueryResult(data)); + dispatch(addInfo('Query Executed!')); + }, 'query_result'); + // Broker.instance().subscribe((data: QueryBuilderState) => dispatch(setQuerybuilderNodes(data)), 'query_builder_state'); + allMLTypes.forEach((mlType) => { + Broker.instance().subscribe((data: LinkPredictionInstance[]) => dispatch(setMLResult({ type: mlType, result: data })), mlType); + }); + + Broker.instance().subscribe((data: SaveStateI[]) => { + dispatch(updateSaveStateList(data)); + }, 'save_states'); + Broker.instance().subscribe((data: any) => {}, 'save_state_status'); + Broker.instance().subscribe((data: SaveStateI) => { + dispatch(addSaveState(data)); + }, 'save_state'); + Broker.instance().subscribe((data: SaveStateI) => { + wsGetStates(); + }, 'delete_save_state'); + Broker.instance().subscribe((response: TestDatabaseConnectionResponse) => { + if (response && response.status === 'success') dispatch(testedSaveState(response.saveStateID)); + }, 'tested_connection'); + + login(); + + return () => { + Broker.instance().unSubscribeAll('schema_result'); + Broker.instance().unSubscribeAll('query_result'); + + Broker.instance().unSubscribeAll('save_states'); + Broker.instance().unSubscribeAll('save_state'); + Broker.instance().unSubscribeAll('save_state_status'); + Broker.instance().unSubscribeAll('delete_save_state'); + Broker.instance().unSubscribeAll('tested_connection'); + // Broker.instance().unSubscribeAll('query_builder_state'); + allMLTypes.forEach((mlType) => { + Broker.instance().unSubscribeAll(mlType); + }); + }; + }, []); + + useEffect(() => { + // New active database + if (session.currentSaveState) { + wsSchemaRequest(session.currentSaveState); + } + }, [session.currentSaveState]); + + useEffect(() => { + // Newly (un)authorized + if (auth.authorized && auth.jwt) { + props.onAuthorized(); + WebSocketHandler.instance() + .useAuth(auth) + .connect(() => { + wsGetStates(); + // WebSocketHandler.instance().sendMessage({ //TODO!! + // sessionID: auth?.sessionID || '', + // key: 'broadcastState', + // body: { type: 'subscribe', status: '', value: {} }, + // }); + }); + } else { + // dispatch(logout()); + } + }, [auth]); + + useEffect(() => { + if (!queryBuilder.ignoreReactivity) { + props.onRunQuery(); + // WebSocketHandler.instance().sendMessage({ //TODO!! + // sessionID: auth?.sessionID || '', + // key: 'broadcastState', + // body: { type: 'query_builder_state', status: '', value: queryBuilder }, + // }); + } + }, [queryHash, mlHash, queryBuilderSettings]); + + return <div className="hide"></div>; +}; diff --git a/libs/shared/lib/data-access/api/index.ts b/libs/shared/lib/data-access/api/index.ts index c31d6f4c9e5397bf6979083e195719cca4e0fc26..e107aebcced1304b59c849d47f4662f5b9d78572 100644 --- a/libs/shared/lib/data-access/api/index.ts +++ b/libs/shared/lib/data-access/api/index.ts @@ -1,3 +1,2 @@ -export * from './database'; -export * from './schema'; -export * from './query'; +export * from './wsState'; +export * from './wsSchema'; diff --git a/libs/shared/lib/data-access/api/query.ts b/libs/shared/lib/data-access/api/query.ts deleted file mode 100644 index 29e6752f1bc14b30a645b65d5b67211a014edf6b..0000000000000000000000000000000000000000 --- a/libs/shared/lib/data-access/api/query.ts +++ /dev/null @@ -1,42 +0,0 @@ -// All database related API calls - -import { BackendQueryFormat } from '../../querybuilder/model/BackendQueryFormat'; -import { useAuth } from '../authorization'; -import { useAuthorizationCache, useSessionCache } from '../store'; - -export const useQueryAPI = () => { - const domain = import.meta.env.BACKEND_URL; - const query_url = import.meta.env.BACKEND_QUERY; - const { fetchAuthenticated } = useAuth(); - - async function execute(query: BackendQueryFormat) { - const response = await fetchAuthenticated(`${domain}${query_url}/execute`, { - method: 'POST', - body: JSON.stringify(query), - }); - - if (!response?.ok) { - const ret = await response.text(); - console.error(response, ret); - throw Error(response.statusText); - } - const ret = await response.json(); - console.debug('Sent Query EXECUTION', ret); - } - - async function retrieveCachedQuery(queryID: string) { - // TODO: check if this method is needed! - // const response = await fetchAuthenticated(`${domain}/query/retrieve-cached/`, { - // method: 'POST', - // body: JSON.stringify({ queryID }) - // }); - // if (!response?.ok) { - // const ret = await response.text(); - // console.error(response, ret); - // } - // // const ret = await response.json(); - // console.log(response); - } - - return { execute, retrieveCachedQuery }; -}; diff --git a/libs/shared/lib/data-access/api/schema.ts b/libs/shared/lib/data-access/api/schema.ts deleted file mode 100644 index c310676c3c8a56355b980f75cc90a3b453867f1f..0000000000000000000000000000000000000000 --- a/libs/shared/lib/data-access/api/schema.ts +++ /dev/null @@ -1,38 +0,0 @@ -// All database related API calls - -import { useAuth } from '../authorization'; -import { useSessionCache } from '../store'; - -export const useSchemaAPI = () => { - const cache = useSessionCache(); - const domain = import.meta.env.BACKEND_URL; - const schema = import.meta.env.BACKEND_SCHEMA; - const { fetchAuthenticated } = useAuth(); - - async function RequestSchema(databaseName?: string) { - if (!databaseName) databaseName = cache.currentDatabase; - if (!databaseName) { - throw Error('Must call with a database name'); - } - - const request = { - databaseName, - cached: false, - // cached: true, - }; - - const response = await fetchAuthenticated(`${domain}${schema}`, { - method: 'POST', - body: JSON.stringify(request), - }); - - if (!response?.ok) { - const ret = await response.text(); - console.error(response, ret); - } - // const ret = await response.json(); - // console.debug('Schema Requested', response); - } - - return { RequestSchema }; -}; diff --git a/libs/shared/lib/data-access/api/wsQuery.ts b/libs/shared/lib/data-access/api/wsQuery.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c7cf9a712cfd6a0ded6d76aad715be84570eb43 --- /dev/null +++ b/libs/shared/lib/data-access/api/wsQuery.ts @@ -0,0 +1,16 @@ +// All database related API calls + +import { log } from 'console'; +import { useAuth } from '../authorization'; +import { WebSocketHandler } from '../socket'; +import { useSessionCache } from '../store'; +import { BackendQueryFormat } from '../../querybuilder'; + +export function wsQueryRequest(query: BackendQueryFormat) { + if (query.cached === undefined) query.cached = false; + WebSocketHandler.instance().sendMessage({ + key: 'query', + subKey: 'get', + body: query, + }); +} diff --git a/libs/shared/lib/data-access/api/wsSchema.ts b/libs/shared/lib/data-access/api/wsSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..006e353b8955948bf0de117ea2ee02f8c58d0b21 --- /dev/null +++ b/libs/shared/lib/data-access/api/wsSchema.ts @@ -0,0 +1,17 @@ +// All database related API calls + +import { log } from 'console'; +import { useAuth } from '../authorization'; +import { WebSocketHandler } from '../socket'; +import { useSessionCache } from '../store'; + +export function wsSchemaRequest(saveStateID: string) { + WebSocketHandler.instance().sendMessage({ + key: 'schema', + subKey: 'get', + body: { + cached: false, + saveStateID: saveStateID, + }, + }); +} diff --git a/libs/shared/lib/data-access/api/wsState.tsx b/libs/shared/lib/data-access/api/wsState.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6a0ff2eaba529ffab5543c9b90b6db647ee15461 --- /dev/null +++ b/libs/shared/lib/data-access/api/wsState.tsx @@ -0,0 +1,102 @@ +import { useEffect } from 'react'; +import { WebSocketHandler } from '../socket'; +import Broker from '../socket/broker'; +import { keyTypeI, subKeyTypeI } from '../socket/types'; +import { useAppDispatch, useConfig } from '../store'; +import { addSaveState, updateSaveStateList } from '../store/sessionSlice'; + +// export function wsGetState() { +// Broker.instance().subscribe((data) => dispatch(readInSchemaFromBackend(data)), 'schema_result'); +// WebSocketHandler.instance().sendMessage({}); +// } + +export const databaseNameMapping: string[] = ['arangodb', 'neo4j']; +export const databaseProtocolMapping: string[] = ['neo4j://', 'neo4j+s://', 'bolt://', 'bolt+s://']; + +export enum DatabaseType { + ArangoDB = 0, + Neo4j = 1, +} + +export enum DatabaseStatus { + untested = 0, + testing = 1, + tested = 2, +} + +export type DatabaseInfo = { + internalDatabaseName: string; + url: string; + port: number; + protocol: string; + username: string; + password: string; + type: number; + status?: DatabaseStatus; +}; + +export const nilUUID = '00000000-0000-0000-0000-000000000000'; + +export type SaveStateI = { + id: string; + name: string; + db: DatabaseInfo; +}; + +export function wsGetStates() { + WebSocketHandler.instance().sendMessage({ + key: 'state', + subKey: 'getAll', + }); +} + +export function wsSelectState(saveStateId: string | undefined) { + if (saveStateId === undefined) saveStateId = ''; + WebSocketHandler.instance().sendMessage({ + key: 'state', + subKey: 'select', + body: { saveStateId: saveStateId }, //messageTypeGetSaveState + }); + WebSocketHandler.instance().useSaveStateID(saveStateId); +} + +export function wsCreateState(request: SaveStateI) { + WebSocketHandler.instance().sendMessage({ + key: 'state', + subKey: 'create', + body: request, //SaveStateModel + }); +} + +export function wsDeleteState(id: string) { + WebSocketHandler.instance().sendMessage({ + key: 'state', + subKey: 'delete', + body: { saveStateId: id }, //messageTypeGetSaveState + }); +} + +export function wsTestSaveStateConnection(id: string) { + WebSocketHandler.instance().sendMessage({ + key: 'state', + subKey: 'testConnection', + body: { saveStateId: id }, //messageTypeGetSaveState + }); +} + +export function wsUpdateState(request: SaveStateI) { + WebSocketHandler.instance().sendMessage({ + key: 'state', + subKey: 'update', + body: request, //SaveStateModel + }); +} + +export type TestDatabaseConnectionResponse = { status: 'success' | 'fail'; saveStateID: string }; +export function wsTestDatabaseConnection(dbConnection: DatabaseInfo) { + WebSocketHandler.instance().sendMessage({ + key: 'dbConnection', + subKey: 'testConnection', + body: dbConnection, //DBConnectionModel + }); +} diff --git a/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx b/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx index 336b6af78b5dc8ea757a80f515c4f0d77edafcbb..7809272df10017570de1a3629d22ab22493e7a71 100644 --- a/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx +++ b/libs/shared/lib/data-access/authorization/dashboardAlerts.tsx @@ -4,8 +4,9 @@ import { useImmer } from 'use-immer'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import { useAppDispatch, useConfig } from '../store'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import { removeLastError, removeLastWarning } from '../store/configSlice'; +import { removeLastError, removeLastInfo, removeLastSuccess, removeLastWarning } from '../store/configSlice'; import { includes } from 'lodash-es'; +import { ReceiveMessageI } from '../socket/types'; type Message = { message: ReactNode; @@ -63,37 +64,6 @@ export const DashboardAlerts = (props: { timer?: number }) => { } } - useEffect(() => { - Broker.instance().subscribeDefault((data: any, routingKey: string) => { - let message: Message | undefined = undefined; - - // Use the logic below to define which broker messages should be shown as alerts. - if (routingKey === 'schema_result') { - message = { - message: ( - <> - <CheckCircleOutlineIcon /> Schema graph updated - </> - ), - className: 'alert-success', - }; - } else if (routingKey === 'query_result') { - message = { - message: ( - <> - <CheckCircleOutlineIcon /> Query Executed! - </> - ), - className: 'alert-success', - }; - } - processMessage(message, data, routingKey); - }); - return () => { - Broker.instance().unSubscribeDefault(); - }; - }, []); - async function processError() { if (config.errors.length > 0) { await processMessage( @@ -106,7 +76,7 @@ export const DashboardAlerts = (props: { timer?: number }) => { className: 'alert-error', }, undefined, - 'error', + 'error' ); dispatch(removeLastError()); } @@ -124,12 +94,48 @@ export const DashboardAlerts = (props: { timer?: number }) => { className: 'alert-warning', }, undefined, - 'warning', + 'warning' ); dispatch(removeLastWarning()); } } + async function processInfo() { + if (config.infos.length > 0) { + await processMessage( + { + message: ( + <> + <CheckCircleOutlineIcon /> {config.infos[config.infos.length - 1]} + </> + ), + className: 'alert-success bg-primary', + }, + undefined, + 'info' + ); + dispatch(removeLastInfo()); + } + } + + async function processSuccess() { + if (config.successes.length > 0) { + await processMessage( + { + message: ( + <> + <CheckCircleOutlineIcon /> {config.successes[config.successes.length - 1]} + </> + ), + className: 'alert-success', + }, + undefined, + 'success' + ); + dispatch(removeLastSuccess()); + } + } + useEffect(() => { processError(); }, [config.errors]); @@ -138,6 +144,14 @@ export const DashboardAlerts = (props: { timer?: number }) => { processWarning(); }, [config.warnings]); + useEffect(() => { + processInfo(); + }, [config.infos]); + + useEffect(() => { + processSuccess(); + }, [config.successes]); + return ( <> {messages && @@ -160,7 +174,6 @@ export const DashboardAlerts = (props: { timer?: number }) => { }} > <span className="flex flex-row content-center gap-3 text-light">{m.message.message}</span> - {/* {!message && (m.data?.status ? m.data.status : m.routingKey)} */} </div> ); })} diff --git a/libs/shared/lib/data-access/authorization/useAuth.tsx b/libs/shared/lib/data-access/authorization/useAuth.tsx index 3d77c89072086cee5f6a63f08f327987c1383198..abab06a86f29343dc28ee7ac655e14809f267996 100644 --- a/libs/shared/lib/data-access/authorization/useAuth.tsx +++ b/libs/shared/lib/data-access/authorization/useAuth.tsx @@ -1,11 +1,13 @@ import { useEffect, useRef, useState } from 'react'; import { useAppDispatch, useAuthorizationCache } from '../store'; -import { authorized } from '../store/authSlice'; +import { authorized, changeRoom } from '../store/authSlice'; export type AuthenticationHeader = { username: string; + userID: string; sessionID: string; + roomID: string; jwt: string; }; @@ -23,20 +25,7 @@ export const useAuth = () => { const auth = useAuthorizationCache(); const handleError = (err: any) => { - if (domain.includes('localhost')) { - console.warn('skipping login for localhost'); - dispatch( - authorized({ - username: 'UserID', - sessionID: 'SessionID', - jwt: 'jwt', - authorized: true, - }) - ); - return; - } else { - console.error(err); - } + console.error(err); }; const login = () => { @@ -48,6 +37,7 @@ export const useAuth = () => { dispatch( authorized({ username: res.username, + userID: res.userID, sessionID: res.sessionID, jwt: res.jwt, authorized: true, @@ -59,26 +49,20 @@ export const useAuth = () => { .catch(handleError); }; - const fetchAuthenticated = (input: RequestInfo | URL, init?: RequestInit | undefined): Promise<Response> => { - if (!init) init = fetchSettings; - - if (!domain.includes('localhost')) { - // Production logic - init.credentials = 'include'; - init.redirect = 'follow'; - init.method = init.method || 'GET'; - init.headers = { - 'Content-Type': 'application/json', - sessionid: auth.sessionID || '', - // Authorization: `Bearer ${auth.jwt}`, - ...init.headers, - }; - } - - return fetch(input, init); + const newShareRoom = () => { + fetch(`${domain}${useruri}/share`, { ...fetchSettings, method: 'POST' }) + .then((res) => + res + .json() + .then((res: { Roomid: string; Sessionid: string }) => { + // TODO: send to backend current state and make redux accordingly + dispatch(changeRoom(res.Roomid)); + }) + .catch(handleError) + ) + .catch(handleError); }; - return { login, fetchAuthenticated }; + return { login, newShareRoom }; }; - // export useAuth; diff --git a/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx b/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx index d457018c01fef178bd1a910674862746ae3df0c4..096738ed4dc70d2cb84c5f91dc507f2d8ca597a4 100644 --- a/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx +++ b/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx @@ -4,17 +4,19 @@ * © Copyright Utrecht University (Department of Information and Computing Sciences) */ -import { AuthenticationHeader } from '../..'; import { UseIsAuthorizedState } from '../../store/authSlice'; import Broker from '../broker'; +import { ReceiveMessageI, SendMessageI, SendMessageWithSessionI } from '../types'; import BackendMessageReceiver from './BackendMessageReceiver'; /** The websockethandler creates a websocket and wait for messages send to the socket. */ export class WebSocketHandler implements BackendMessageReceiver { + private static singletonInstance: WebSocketHandler; private webSocket: WebSocket | undefined; private url: string; private connected: boolean; private authHeader: UseIsAuthorizedState | undefined; + private saveStateID: string | undefined; /** @param domain The domain to make the websocket connection with. */ public constructor(domain: string) { @@ -22,11 +24,21 @@ export class WebSocketHandler implements BackendMessageReceiver { this.connected = false; } + public static instance(): WebSocketHandler { + if (!this.singletonInstance) this.singletonInstance = new WebSocketHandler(import.meta.env.BACKEND_WSS_URL); + return this.singletonInstance; + } + public useAuth(authHeader: UseIsAuthorizedState): WebSocketHandler { this.authHeader = authHeader; return this; } + public useSaveStateID(saveStateID: string): WebSocketHandler { + this.saveStateID = saveStateID; + return this; + } + /** * Create a websocket to the given URL. * @param {string} URL is the URL to which the websocket connection is opened. @@ -35,22 +47,42 @@ export class WebSocketHandler implements BackendMessageReceiver { // If there already is already a current websocket connection, close it first. if (this.webSocket) this.close(); - const params = new URLSearchParams(); + const params = new URLSearchParams(window.location.search); + if (this.authHeader?.userID) params.set('userID', this.authHeader?.userID ?? ''); // TODO!! need a better more safe way to do this + if (this.authHeader?.roomID) params.set('roomID', this.authHeader?.roomID ?? ''); + if (this.saveStateID) params.set('saveStateID', this.saveStateID ?? ''); if (this.authHeader?.sessionID) params.set('sessionID', this.authHeader?.sessionID ?? ''); if (this.authHeader?.jwt) params.set('jwt', this.authHeader?.jwt ?? ''); this.webSocket = new WebSocket(this.url + '?' + params.toString()); - this.webSocket.onopen = () => onOpen(); + this.webSocket.onopen = () => { + this.connected = true; + onOpen(); + }; this.webSocket.onmessage = this.onWebSocketMessage; this.webSocket.onerror = this.onError; this.webSocket.onclose = this.onClose; + } - this.connected = true; + public sendMessage(message: SendMessageI): void { + console.debug('%cSending WS message: ', 'background: #222; color: #bada55', message); + let fullMessage = message as SendMessageWithSessionI; + fullMessage.sessionID = this.authHeader?.sessionID ?? ''; + if (message.body && typeof message.body !== 'string') { + fullMessage.body = JSON.stringify(message.body); + } + + if (this.webSocket && this.connected && this.webSocket.readyState === 1) this.webSocket.send(JSON.stringify(fullMessage)); + else + this.connect(() => { + if (this.webSocket && this.connected && this.webSocket.readyState === 1) this.webSocket.send(JSON.stringify(fullMessage)); + }); } /** Closes the current websocket connection. */ public close = (): void => { if (this.webSocket) this.webSocket.close(); this.connected = false; + this.webSocket = undefined; }; /** @returns A boolean which indicates if there currently is a socket connection. */ @@ -58,12 +90,28 @@ export class WebSocketHandler implements BackendMessageReceiver { return this.connected; }; + public attemptReconnect = () => { + console.warn('Attempting to reconnect WS'); + + if (!this.connected || !this.webSocket || this.webSocket.readyState !== 1) { + this.connect(() => { + setTimeout(() => WebSocketHandler.instance().attemptReconnect(), 5000); + }); + } else { + console.log('WS reconnected', this.webSocket?.readyState, this.connected); + } + }; + /** * Websocket connection close event handler. * @param {any} event Contains the event data. */ private onClose(event: any): void { - console.log(event.data); + console.warn('WS connection was closed from the server side', event.data); + if (this.webSocket) this.webSocket.close(); + this.connected = false; + this.webSocket = undefined; + setTimeout(() => WebSocketHandler.instance().attemptReconnect(), 5000); } /** @@ -71,7 +119,7 @@ export class WebSocketHandler implements BackendMessageReceiver { * @param {any} event Contains the event data. */ public onWebSocketMessage = (event: MessageEvent<any>) => { - let data = JSON.parse(event.data); + let data: ReceiveMessageI = JSON.parse(event.data); Broker.instance().publish(data.value, data.type); }; @@ -80,6 +128,6 @@ export class WebSocketHandler implements BackendMessageReceiver { * @param {any} event contains the event data. */ private onError(event: any): void { - console.error(event); + console.error('WS error', event); } } diff --git a/libs/shared/lib/data-access/socket/broker/index.tsx b/libs/shared/lib/data-access/socket/broker/index.tsx index 4f56a0a402204a469b7c2c17b0b9290ffb12b254..203c53090dd36efb57032777064e2d47f4ff9439 100644 --- a/libs/shared/lib/data-access/socket/broker/index.tsx +++ b/libs/shared/lib/data-access/socket/broker/index.tsx @@ -4,6 +4,8 @@ * © Copyright Utrecht University (Department of Information and Computing Sciences) */ +import { ReceiveMessageI } from '../types'; + /** * A broker that handles incoming messages from the backend. * It works with routingkeys, a listener can subscribe to messages from the backend with a specific routingkey. @@ -20,7 +22,7 @@ export default class Broker { private static singletonInstance: Broker; private listeners: Record<string, Record<string, Function>> = {}; - private catchAllListener: Function | undefined; + private catchAllListener: ((data: Record<string, any>, routingKey: string) => void) | undefined; /** mostRecentMessages is a dictionary with <routingkey, messageObject>. It stores the most recent message for that routingkey. */ private mostRecentMessages: Record<string, unknown> = {}; @@ -33,10 +35,10 @@ export default class Broker { /** * Notify all listeners which are subscribed with the specified routingkey. - * @param {unknown} jsonObject An json object with unknown type. + * @param {ReceiveMessageI} jsonObject An json object with unknown type. * @param {string} routingKey The routing to publish the message to. */ - public publish(jsonObject: unknown, routingKey: string): void { + public publish(jsonObject: Record<string, any>, routingKey: string): void { this.mostRecentMessages[routingKey] = jsonObject; if (this.listeners[routingKey] && Object.keys(this.listeners[routingKey]).length != 0) { @@ -44,15 +46,15 @@ export default class Broker { this.catchAllListener(jsonObject, routingKey); } Object.values(this.listeners[routingKey]).forEach((listener) => listener(jsonObject, routingKey)); - console.debug(routingKey, `message processed with routing key`, jsonObject); + console.debug('%c' + routingKey + ` WS response`, 'background: #222; color: #DBAB2F', jsonObject); } - // If there are no listeners, log the message + // If there are no listeners, log the messagep else { if (this.catchAllListener) { this.catchAllListener(jsonObject, routingKey); console.debug(routingKey, `catch all used for message with routing key`, jsonObject); } else { - console.debug(routingKey, `no listeners for message with routing key`, jsonObject); + console.debug('%c' + routingKey + ` no listeners for message with routing key`, 'background: #663322; color: #DBAB2F', jsonObject); } } } @@ -67,7 +69,7 @@ export default class Broker { newListener: Function, routingKey: string, key: string = (Date.now() + Math.floor(Math.random() * 100)).toString(), - consumeMostRecentMessage: boolean = true + consumeMostRecentMessage: boolean = false ): string { if (!this.listeners[routingKey]) this.listeners[routingKey] = {}; @@ -88,7 +90,7 @@ export default 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: Function): void { + public subscribeDefault(newListener: (data: Record<string, any>, routingKey: string) => void): void { this.catchAllListener = newListener; } diff --git a/libs/shared/lib/data-access/socket/types.ts b/libs/shared/lib/data-access/socket/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce9f967eb729e718f98b3624e5ae8f9982879af9 --- /dev/null +++ b/libs/shared/lib/data-access/socket/types.ts @@ -0,0 +1,50 @@ +export type ReceiveMessageI = { + type: string; + status: string; + value: Record<string, any>; +}; + +type SchemaServiceOrchestratorMessage = { + databaseName: string; + cached: boolean; +}; +type SchemaStatsServiceOrchestratorMessage = { + databaseName: string; + cached: boolean; +}; +type QueryOrchestratorMessage = { + databaseName: string; + cached: boolean; + queryID: string; +}; + +export type keyTypeI = 'broadcastState' | 'dbConnection' | 'schema' | 'query' | 'state'; +export type subKeyTypeI = + // Crud + | 'create' + | 'getAll' + | 'delete' + | 'update' + | 'get' + | 'select' + // Custom + | 'newDbConnection' + | 'editDbConnection' + | 'deleteDbConnection' + | 'getDbConnection' + | 'getAllDbConnections' + | 'testConnection' + | 'getSchema' + | 'getSchemaStats' + | 'runQuery'; + +export type SendMessageI = { + key: keyTypeI; + subKey?: subKeyTypeI; + body?: any; +}; + +export type SendMessageWithSessionI = SendMessageI & { + sessionID: string; + body?: string; +}; diff --git a/libs/shared/lib/data-access/store/authSlice.ts b/libs/shared/lib/data-access/store/authSlice.ts index 13b1bfbad255f28295e9bdb2a19f95aef9eb980b..54427243537e456ef73844ff98b49084b3c9b9d6 100644 --- a/libs/shared/lib/data-access/store/authSlice.ts +++ b/libs/shared/lib/data-access/store/authSlice.ts @@ -1,10 +1,15 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; -export type UseIsAuthorizedState = { +export type UseIsAuthorizedState = SingleIsAuthorizedState & { + roomID: string | undefined; +}; + +export type SingleIsAuthorizedState = { authorized: boolean | undefined; jwt: string | undefined; sessionID: string | undefined; + userID: string | undefined; username: string | undefined; }; @@ -13,6 +18,8 @@ export const initialState: UseIsAuthorizedState = { authorized: undefined, jwt: undefined, sessionID: undefined, + roomID: undefined, + userID: undefined, username: undefined, }; @@ -21,28 +28,48 @@ export const authSlice = createSlice({ // `createSlice` will infer the state type from the `initialState` argument initialState, reducers: { - authorized(state, action: PayloadAction<UseIsAuthorizedState>) { - console.info('Authorized'); + authorized(state, action: PayloadAction<SingleIsAuthorizedState>) { + console.info('%cAuthorized', 'background-color: blue'); state.authorized = action.payload.authorized; state.jwt = action.payload.jwt; + state.userID = action.payload.userID; state.sessionID = action.payload.sessionID; state.username = action.payload.username; }, + changeRoom(state, action: PayloadAction<string | undefined>) { + console.info('Changing Room to', action.payload); + state.roomID = action.payload; + const query = new URLSearchParams(window.location.search); + if (!!action?.payload) { + query.set('roomID', action?.payload || 'null'); + history.pushState(null, '', '?' + query.toString()); + } else { + query.delete('roomID'); + history.pushState(null, '', '?' + query.toString()); + } + }, logout(state) { console.info('Logging out'); state.authorized = undefined; state.jwt = undefined; state.sessionID = undefined; + state.userID = undefined; state.username = undefined; + const query = new URLSearchParams(window.location.search); + query.delete('roomID'); + history.pushState(null, '', '?' + query.toString()); }, unauthorized(state) { console.warn('Unauthorized'); state.authorized = false; + const query = new URLSearchParams(window.location.search); + query.delete('roomID'); + history.pushState(null, '', '?' + query.toString()); }, }, }); -export const { authorized, unauthorized, logout } = authSlice.actions; +export const { authorized, unauthorized, logout, changeRoom } = authSlice.actions; // Other code such as selectors can use the imported `RootState` type export const authState = (state: RootState) => state.auth; diff --git a/libs/shared/lib/data-access/store/configSlice.ts b/libs/shared/lib/data-access/store/configSlice.ts index 2d6d2393ceb73ccfd257dbd339320e0896bde9e0..3e3fa8c91d276454a8ad6df99487d7ad017c1ef8 100644 --- a/libs/shared/lib/data-access/store/configSlice.ts +++ b/libs/shared/lib/data-access/store/configSlice.ts @@ -3,26 +3,17 @@ import type { RootState } from './store'; // Define the initial state using that type export const initialState: { - queryListOpen: boolean; - queryStatusList: { - queries: Record<string, any>; - queryIDsOrder: string[]; - }; - functionsMenuOpen: boolean; - currentDatabaseKey: string; - elementsperDatabaseObject: Record<string, number>; autoSendQueries: boolean; errors: string[]; warnings: string[]; + infos: string[]; + successes: string[]; } = { - queryListOpen: false, - queryStatusList: { queries: {}, queryIDsOrder: [] }, - functionsMenuOpen: false, - currentDatabaseKey: '', - elementsperDatabaseObject: {}, autoSendQueries: true, errors: [], warnings: [], + infos: [], + successes: [], }; export const configSlice = createSlice({ @@ -30,6 +21,18 @@ export const configSlice = createSlice({ // `createSlice` will infer the state type from the `initialState` argument initialState, reducers: { + addSuccess: (state, action: PayloadAction<string>) => { + state.successes.push(action.payload); + }, + removeLastSuccess: (state) => { + state.successes.shift(); + }, + addInfo: (state, action: PayloadAction<string>) => { + state.infos.push(action.payload); + }, + removeLastInfo: (state) => { + state.infos.shift(); + }, addError: (state, action: PayloadAction<string>) => { console.error('Error Received!', action.payload); state.errors.push(action.payload); @@ -47,7 +50,8 @@ export const configSlice = createSlice({ }, }); -export const { addError, removeLastError, addWarning, removeLastWarning } = configSlice.actions; +export const { addError, removeLastError, addWarning, removeLastWarning, addSuccess, removeLastSuccess, addInfo, removeLastInfo } = + configSlice.actions; // Other code such as selectors can use the imported `RootState` type export const configState = (state: RootState) => state.config; diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts index e22bf1997eea13b269dcf34db7e80b53cc58fb09..282db06af7fc37f251898ff8cb92c820b1b23668 100644 --- a/libs/shared/lib/data-access/store/hooks.ts +++ b/libs/shared/lib/data-access/store/hooks.ts @@ -5,6 +5,7 @@ import type { RootState, AppDispatch } from './store'; import { configState } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { queryBuilderSettingsState, + queryBuilderState, selectQuerybuilderGraph, selectQuerybuilderHash, } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; @@ -22,15 +23,16 @@ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; /** Gives the graphQueryResult from the store */ export const useGraphQueryResult = () => useAppSelector(selectGraphQueryResult); -// Gives the schema form the store (as a graphology object) +// Gives the schema export const useSchemaGraph = () => useAppSelector(schemaGraph); export const useSchemaSettings = () => useAppSelector(schemaSettingsState); - -// Gives the schema form the store (as a graphology object) export const useSchemaLayout = () => useAppSelector(selectSchemaLayout); + +// Querybuilder Slices export const useQuerybuilderGraph = () => useAppSelector(selectQuerybuilderGraph); export const useQuerybuilderHash = () => useAppSelector(selectQuerybuilderHash); export const useQuerybuilderSettings = () => useAppSelector(queryBuilderSettingsState); +export const useQuerybuilder = () => useAppSelector(queryBuilderState); // Overall Configuration of the app export const useConfig = () => useAppSelector(configState); diff --git a/libs/shared/lib/data-access/store/index.ts b/libs/shared/lib/data-access/store/index.ts index 7bff27287890322aada6b478bfeee1afeddaa318..8b0db040cef6866abff42b1979a170037a2ac232 100644 --- a/libs/shared/lib/data-access/store/index.ts +++ b/libs/shared/lib/data-access/store/index.ts @@ -2,7 +2,7 @@ export * from './store'; export * from './hooks'; export { setSchema, readInSchemaFromBackend, schemaSlice, selectSchemaLayout } from './schemaSlice'; -export { querybuilderSlice, setQuerybuilderGraph as setQuerybuilderNodes } from './querybuilderSlice'; +export { querybuilderSlice, setQuerybuilderGraph, setQuerybuilderNodes } from './querybuilderSlice'; export { selectGraphQueryResult, selectGraphQueryResultLinks, diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts index 65673108e8ae68f0d5d1fac123ff3392d081806e..98e282a8c9e1ed49eda15b77f5bf87dc498c22e7 100644 --- a/libs/shared/lib/data-access/store/querybuilderSlice.ts +++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts @@ -13,12 +13,16 @@ export type QueryBuilderSettings = { layout: AllLayoutAlgorithms | 'manual'; }; -// Define the initial state using that type -export const initialState: { +export type QueryBuilderState = { graph: QueryMultiGraph; + ignoreReactivity: boolean; settings: QueryBuilderSettings; -} = { +}; + +// Define the initial state using that type +export const initialState: QueryBuilderState = { graph: defaultGraph(), + ignoreReactivity: false, settings: { limit: 500, depth: { min: 1, max: 1 }, @@ -34,6 +38,12 @@ export const querybuilderSlice = createSlice({ setQuerybuilderGraph: (state, action: PayloadAction<QueryMultiGraph>) => { // @ts-ignore state.graph = action.payload; + state.ignoreReactivity = false; + }, + setQuerybuilderNodes: (state, action: PayloadAction<QueryBuilderState>) => { + state.graph = action.payload.graph; + state.settings = action.payload.settings; + state.ignoreReactivity = true; }, clearQB: (state) => { state.graph = defaultGraph(); @@ -44,6 +54,7 @@ export const querybuilderSlice = createSlice({ }, }); +export const queryBuilderState = (state: RootState) => state.querybuilder; export const queryBuilderSettingsState = (state: RootState) => state.querybuilder.settings; export const setQuerybuilderGraphology = (payload: QueryGraphology) => { @@ -97,4 +108,4 @@ export const selectQuerybuilderHash = (state: RootState): any => { // state.schema.schemaLayout; export default querybuilderSlice.reducer; -export const { setQuerybuilderGraph, clearQB, setQuerybuilderSettings } = querybuilderSlice.actions; +export const { setQuerybuilderGraph, clearQB, setQuerybuilderSettings, setQuerybuilderNodes } = querybuilderSlice.actions; diff --git a/libs/shared/lib/data-access/store/sessionSlice.ts b/libs/shared/lib/data-access/store/sessionSlice.ts index d3d94aa8b83ca0f3dc55a226b534005a6be2084d..e4c1a559c9ec69e016bf902d4fffc824d3e2485f 100644 --- a/libs/shared/lib/data-access/store/sessionSlice.ts +++ b/libs/shared/lib/data-access/store/sessionSlice.ts @@ -1,7 +1,7 @@ +import { includes } from 'lodash-es'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; -import { BackendQueryFormat } from '../../querybuilder'; -import { DatabaseInfo } from '../api'; +import { DatabaseInfo, DatabaseStatus, SaveStateI, wsSelectState } from '../api/wsState'; /** Message format of the error message from the backend */ export type ErrorMessage = { @@ -11,40 +11,52 @@ export type ErrorMessage = { /** Cache type */ export type SessionCacheI = { - currentDatabase?: string; - databases: DatabaseInfo[] | undefined; + currentSaveState?: string; // id of the current save state + saveStates: Record<string, SaveStateI>; }; // Define the initial state using that type export const initialState: SessionCacheI = { - currentDatabase: undefined, - databases: undefined, + currentSaveState: undefined, + saveStates: {}, }; export const sessionSlice = createSlice({ name: 'session', - // `createSlice` will infer the state type from the `initialState` argument initialState, reducers: { - updateCurrentDatabase(state, action: PayloadAction<string|undefined>) { - state.currentDatabase = action.payload; + updateCurrentSaveState(state, action: PayloadAction<string | undefined>) { + state.currentSaveState = action.payload; + wsSelectState(state.currentSaveState); }, - updateDatabaseList(state, action: PayloadAction<DatabaseInfo[]>) { - console.debug('Updating database list', action); - state.databases = action.payload; - if (state.databases.length > 0) { - const foundDatabase = state.databases.find((db) => db.Name === state.currentDatabase); - if (!foundDatabase) { - state.currentDatabase = state.databases[0].Name; - } else { - state.currentDatabase = foundDatabase?.Name || undefined; - } + updateSaveStateList(state, action: PayloadAction<SaveStateI[]>) { + state.saveStates = {}; + action.payload.forEach((ss) => { + state.saveStates[ss.id] = ss; + }); + + if (!state.currentSaveState || !(state.currentSaveState in state.saveStates)) { + if (Object.keys(state.saveStates).length > 0) { + state.currentSaveState = Object.keys(state.saveStates)[0]; + } else state.currentSaveState = undefined; + } + wsSelectState(state.currentSaveState); + }, + addSaveState(state, action: PayloadAction<SaveStateI>) { + if (state.saveStates === undefined) state.saveStates = {}; + state.saveStates[action.payload.id] = action.payload; + state.currentSaveState = action.payload.id; + wsSelectState(state.currentSaveState); + }, + testedSaveState(state, action: PayloadAction<string>) { + if (action.payload in state.saveStates) { + state.saveStates[action.payload].db.status = DatabaseStatus.tested; } }, }, }); -export const { updateCurrentDatabase, updateDatabaseList } = sessionSlice.actions; +export const { updateCurrentSaveState, updateSaveStateList, addSaveState, testedSaveState } = 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/querybuilder/model/BackendQueryFormat.tsx b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx index de4c9ef31cfbb1869bcf45145296604fd57456ec..7fc2ddbc1d6cd54a7ecc8db439e213a4c404b6c6 100644 --- a/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx +++ b/libs/shared/lib/querybuilder/model/BackendQueryFormat.tsx @@ -10,7 +10,7 @@ import { MLTypes } from '../../data-access/store/mlSlice'; /** JSON query format used to send a query to the backend. */ export interface BackendQueryResultFormat { - databaseName: string; + saveStateID: string; return: { entities: number[]; relations: number[]; @@ -27,7 +27,7 @@ export interface BackendQueryResultFormat { /** JSON query format used to send a query to the backend. */ export interface BackendQueryFormat { - databaseName: string; + saveStateID: string; limit: number; return: string[]; query: QueryStruct[]; @@ -38,6 +38,7 @@ export interface BackendQueryFormat { machineLearning: MachineLearning[]; // modifiers: ModifierStruct[]; // prefix: string; + cached: boolean | undefined; } /** Interface for an entity in the JSON for the query. */ diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index a671ece20dbaf4e5b574effb22780a4c55d97f65..e6d3b474e78e8273befb34e40b0fa4adf29fbf53 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useConfig, + useQuerybuilder, useQuerybuilderGraph, useQuerybuilderSettings, useSchemaGraph, @@ -60,7 +61,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { relation: RelationPill, logic: LogicPill, }), - [], + [] ); var edgeTypes = useMemo(() => ({ connection: ConnectionLine, attribute_connection: ConnectionLine }), []); @@ -186,7 +187,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { name: dragData.name, schemaKey: dragData.name, }, - schema.getNodeAttribute(dragData.name, 'attributes'), + schema.getNodeAttribute(dragData.name, 'attributes') ); dispatch(setQuerybuilderGraphology(graphologyGraph)); @@ -203,7 +204,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { schemaKey: dragData.label, collection: dragData.collection, }, - schema.getEdgeAttribute(dragData.label, 'attributes'), + schema.getEdgeAttribute(dragData.label, 'attributes') ); if (config.autoSendQueries) { @@ -256,7 +257,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { } } }, - [graph], + [graph] ); const onConnectStart = useCallback( @@ -274,7 +275,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { attribute: { handleData: handleData }, }; }, - [graph], + [graph] ); const onConnectEnd = useCallback( @@ -314,7 +315,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { // setToggleSettings('logic'); } }, - [reactFlow.project], + [reactFlow.project] ); const onEdgeUpdateStart = useCallback(() => { @@ -339,7 +340,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { dispatch(setQuerybuilderGraphology(graphologyGraph)); } }, - [graph], + [graph] ); const onEdgesChange = (params: OnEdgesChange) => { @@ -356,7 +357,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { } isEdgeUpdating.current = false; }, - [graph], + [graph] ); const onNodeContextMenu = (event: React.MouseEvent, node: Node) => { diff --git a/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx b/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx index 3f6ebb25b86b821f35757e1468fbcf1bdaa422da..bb9dc3aac9641c9fd7ee0ed6a1403a7aa4af7756 100644 --- a/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Meta } from '@storybook/react'; import { Provider } from 'react-redux'; -import { setQuerybuilderNodes, setSchema, store } from '@graphpolaris/shared/lib/data-access/store'; +import { setQuerybuilderGraph, setSchema, store } from '@graphpolaris/shared/lib/data-access/store'; import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils'; import { Schema } from '@graphpolaris/shared/lib/schema/panel'; @@ -56,7 +56,7 @@ export const SchemaAndQueryBuilderInteractivity = { const schema = SchemaUtils.schemaBackend2Graphology(movieSchemaRaw); const graph = new QueryMultiGraphology(); - dispatch(setQuerybuilderNodes(graph.export())); + dispatch(setQuerybuilderGraph(graph.export())); dispatch(setSchema(schema.export())); }, }; diff --git a/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx b/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx index 8f40b7593141bd349b3e1b8f305cb5e0bf08ee67..cd0530b4e35964f92b727b06743259789d01e2d8 100644 --- a/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/stories/querybuilder-simple.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { querybuilderSlice, setQuerybuilderNodes, setSchema, store } from '@graphpolaris/shared/lib/data-access/store'; +import { querybuilderSlice, setQuerybuilderGraph, setSchema, store } from '@graphpolaris/shared/lib/data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; @@ -149,7 +149,7 @@ export const Simple = { // type: 'entity_relation', // sourceHandle: handles.relation.entity, // }); - store.dispatch(setQuerybuilderNodes(graph.export())); + store.dispatch(setQuerybuilderGraph(graph.export())); }, }; diff --git a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx index 376d05b1f68a1305856ed5f844d7b82626184f18..9eb58c7ba64a810f53b26b9fcb16c408af78ce28 100644 --- a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-entity.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { querybuilderSlice, setQuerybuilderNodes, setSchema, store } from '@graphpolaris/shared/lib/data-access/store'; +import { querybuilderSlice, setQuerybuilderGraph, setSchema, store } from '@graphpolaris/shared/lib/data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; @@ -36,7 +36,7 @@ export const SingleEntity = { y: 100, name: 'Entity Pill', }); - store.dispatch(setQuerybuilderNodes(graph.export())); + store.dispatch(setQuerybuilderGraph(graph.export())); }, }; diff --git a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx index e3b9f4cc3486b65020f3af684e1d5d51c9acdbba..fe73dc689e1d2b02fd1d8275fc78e568f683ffff 100644 --- a/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/stories/querybuilder-single-relationship.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { querybuilderSlice, setQuerybuilderNodes, setSchema, store } from '@graphpolaris/shared/lib/data-access/store'; +import { querybuilderSlice, setQuerybuilderGraph, setSchema, store } from '@graphpolaris/shared/lib/data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; @@ -38,7 +38,7 @@ export const SingleRelationship = { collection: 'Relation Pill', depth: { min: 0, max: 1 }, }); - store.dispatch(setQuerybuilderNodes(graph.export())); + store.dispatch(setQuerybuilderGraph(graph.export())); }, }; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx index 139c807ac695900aa2754a921c2eb9c365a4d517..9e5750185ee7e9a3bb7cbdd212b182b026770a36 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill-full.stories.tsx @@ -1,10 +1,5 @@ import React from 'react'; -import { - querybuilderSlice, - schemaSlice, - setQuerybuilderNodes, - searchResultSlice, -} from '@graphpolaris/shared/lib/data-access/store'; +import { querybuilderSlice, schemaSlice, setQuerybuilderGraph, searchResultSlice } from '@graphpolaris/shared/lib/data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; @@ -34,7 +29,7 @@ export const Flow = { const graph = new QueryMultiGraphology(); graph.addPill2Graphology({ id: '2', type: QueryElementTypes.Entity, x: 100, y: 100, name: 'Entity Pill' }); - dispatch(setQuerybuilderNodes(graph.export())); + dispatch(setQuerybuilderGraph(graph.export())); }, args: {}, }; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx index b0a877a187598fb0242bdd555256290054da8e94..5da8f51bcf87b9eb55c64c991eab36cb46f76b6c 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-full_reactflow.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { querybuilderSlice, schemaSlice, setQuerybuilderNodes, searchResultSlice } from '@graphpolaris/shared/lib/data-access/store'; +import { querybuilderSlice, schemaSlice, setQuerybuilderGraph, searchResultSlice } from '@graphpolaris/shared/lib/data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; @@ -32,7 +32,7 @@ graph.addPill2Graphology({ }); console.log(graph.export()); -mockStore.dispatch(setQuerybuilderNodes(graph.export())); +mockStore.dispatch(setQuerybuilderGraph(graph.export())); export const Flow = { args: {}, diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts index 085dcba4efeb71339226e7159b6c52db3f36bf9d..b57c1eaa914a251737009dec831af8682a60c82b 100644 --- a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts @@ -9,7 +9,8 @@ import { MathAggregations } from '../model/logic/numberAggregations'; import { QueryBuilderSettings } from '../../data-access/store/querybuilderSlice'; const defaultQuery = { - databaseName: 'database', + saveStateID: 'database', + cached: false, query: [], limit: 500, return: ['*'], diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.ts index 57b65c31d952c19211400f287fc8778e5c9284b1..9246a7286e95a8e5ab313ed34390ee924254a0f0 100644 --- a/libs/shared/lib/querybuilder/query-utils/query2backend.ts +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.ts @@ -157,17 +157,18 @@ function queryLogicUnion(graphLogicChunks: AllLogicStatement[]): AllLogicStateme * @returns {BackendQueryFormat} A JSON object in the `JSONFormat`. */ export function Query2BackendQuery( - databaseName: string, + saveStateID: string, graph: QueryMultiGraph, settings: QueryBuilderSettings, ml: ML = mlDefaultState ): BackendQueryFormat { let query: BackendQueryFormat = { - databaseName: databaseName, + saveStateID: saveStateID, query: [], machineLearning: [], limit: settings.limit, return: ['*'], // TODO + cached: false, }; Object.keys(ml).forEach((mlType) => { @@ -200,7 +201,7 @@ export function Query2BackendQuery( }); }); - return Query2BackendQuery(databaseName, graphologyQuery.export(), settings, ml); + return Query2BackendQuery(saveStateID, graphologyQuery.export(), settings, ml); } // Chunk extraction: traverse graph to find all paths of logic between relations and entities let graphSequenceChunks: QueryGraphNodes[][] = []; @@ -286,7 +287,7 @@ export function Query2BackendQuery( return ret; }); - console.debug('New processed query', graph, query); + console.debug('%cNew processed query', 'color: aquamarine', graph, query); return query; } diff --git a/libs/shared/lib/schema/panel/schema.tsx b/libs/shared/lib/schema/panel/schema.tsx index fa644223691ead5c18c376216433f336139984c4..73b9a8fd7daa318bc397468df08debecc11ea386 100644 --- a/libs/shared/lib/schema/panel/schema.tsx +++ b/libs/shared/lib/schema/panel/schema.tsx @@ -13,11 +13,12 @@ import { EntityNode } from '../pills/nodes/entity/entity-node'; import { RelationNode } from '../pills/nodes/relation/relation-node'; import NodeEdge from '../pills/edges/node-edge'; import SelfEdge from '../pills/edges/self-edge'; -import { useSchemaAPI } from '../../data-access'; import { SchemaDialog } from './schemaDialog'; +import { wsSchemaRequest } from '@graphpolaris/shared/lib/data-access/api/wsSchema'; import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; import { Button } from '../../components/buttons'; import ControlContainer from '../../components/controls'; +import { wsGetStates } from '../../data-access'; interface Props { content?: string; @@ -42,7 +43,6 @@ const edgeTypes = { }; export const Schema = (props: Props) => { - const api_schema = useSchemaAPI(); const session = useSessionCache(); const settings = useSchemaSettings(); const searchResults = useSearchResultSchema(); @@ -99,7 +99,7 @@ export const Schema = (props: Props) => { nds.map((node) => ({ ...node, selected: searchResults.includes(node.id) || searchResults.includes(node.data.label), - })), + })) ); }, [searchResults]); @@ -117,7 +117,8 @@ export const Schema = (props: Props) => { iconName="Cached" onClick={(e) => { e.stopPropagation(); - api_schema.RequestSchema(session.currentDatabase); + if (session.currentSaveState) wsSchemaRequest(session.currentSaveState); + else wsGetStates(); }} /> <Button diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx index c5518de96f127d8b6554b0ecd5a339d847f85c46..6520c642e7bf35a0c2f0386f6fb949fb79ac8dc7 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx @@ -30,8 +30,6 @@ export const RelationNode = React.memo(({ id, selected, data }: NodeProps<Schema * @param event React Mouse drag event. */ const onDragStart = (event: React.DragEvent<HTMLDivElement>) => { - console.log(data); - const eventData: SchemaEdge = { type: QueryElementTypes.Relation, name: id, //TODO id? diff --git a/libs/shared/lib/vis/index.tsx b/libs/shared/lib/vis/index.tsx index 27152704b3fb3bdec415c06c07bc55021bb47b43..3abc7b697884c0527d0b103751d3d60aa79480e1 100644 --- a/libs/shared/lib/vis/index.tsx +++ b/libs/shared/lib/vis/index.tsx @@ -37,8 +37,6 @@ export const createVisualizationComponent = () => { const VisualizationComponent: VISComponentType = Visualizations[vis.activeVisualization]; - console.log('createVisualizationComponent', VisualizationComponent, vis); - React.useEffect(() => { dispatch(addVisualization({ id: VisualizationComponent.displayName, settings: VisualizationComponent.localConfigSchema })); }, [vis.activeVisualization]); diff --git a/libs/shared/lib/vis/visualizations/nodelink/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelink/nodelinkvis.tsx index 8bb1eff66b89c7eece52a00d05ae8e5530a8fdb0..67afb9d1f1ea693f90f9315d55172d930f8c1064 100644 --- a/libs/shared/lib/vis/visualizations/nodelink/nodelinkvis.tsx +++ b/libs/shared/lib/vis/visualizations/nodelink/nodelinkvis.tsx @@ -39,7 +39,7 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch }: VisualizationProp useEffect(() => { if (data) { - console.debug('graphQueryResult', data); + console.debug('%cResult from graphQuery', 'color: aquamarine', data); setGraph( parseQueryResult(data, ml, { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 176c04faf1c1006e16683e7814389662f7d9fbf1..c584b0f6fd86b4cd64003b6f039bb050330e62ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,10 +110,13 @@ importers: version: 6.9.0(react-dom@18.2.0)(react@18.2.0) reactflow: specifier: 11.4.0-next.1 - version: 11.4.0-next.1(react-dom@18.2.0)(react@18.2.0) + version: 11.4.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) styled-components: specifier: ^5.3.6 version: 5.3.9(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + use-immer: + specifier: ^0.9.0 + version: 0.9.0(immer@10.0.2)(react@18.2.0) devDependencies: '@import-meta-env/cli': specifier: ^0.6.5 @@ -5656,14 +5659,14 @@ packages: '@babel/runtime': 7.21.0 dev: true - /@reactflow/background@11.1.0-next.1(react-dom@18.2.0)(react@18.2.0): + /@reactflow/background@11.1.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-OXCWt3rKz7/pctEqL2e82ziIJwfxGO9McC2a/JGso75rhCu+b7dWejhESNRS+9rgu1PdQpjDvB/wgQKIQqGoWA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: '@babel/runtime': 7.21.0 - '@reactflow/core': 11.4.0-next.1(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.4.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5687,14 +5690,14 @@ packages: - immer dev: false - /@reactflow/controls@11.1.0-next.1(react-dom@18.2.0)(react@18.2.0): + /@reactflow/controls@11.1.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-jqvwxI9VFc4ZPBfZE98MigwE+UJbxLHBC47y0pt1bGqnxzK1XcAMocDVcvlTMbHWbDmouFmpk+cUoYSkQx5/cQ==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: '@babel/runtime': 7.21.0 - '@reactflow/core': 11.4.0-next.1(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.4.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5716,7 +5719,7 @@ packages: - immer dev: false - /@reactflow/core@11.4.0-next.1(react-dom@18.2.0)(react@18.2.0): + /@reactflow/core@11.4.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-OgHMl9qs7ZMidoc+pUcZ4O1TxszrpW0jcb2tZQOfB5WpJL40HmwXrGYZdk9IhG1ANo4N0nwS5MBvho2Ddo7aSw==} peerDependencies: react: '>=17' @@ -5779,14 +5782,14 @@ packages: - immer dev: false - /@reactflow/minimap@11.3.0-next.1(react-dom@18.2.0)(react@18.2.0): + /@reactflow/minimap@11.3.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ZNo6oLTKSLHO/rdfribRO5pmBMWT8Y5Hbn5zKkzJgjPKmaa7sG6ZjuOJNBz4LuKy7GrP5Uu2wGSc8svxNvYogA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: '@babel/runtime': 7.21.0 - '@reactflow/core': 11.4.0-next.1(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.4.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) '@types/d3-selection': 3.0.5 '@types/d3-zoom': 3.0.2 classcat: 5.0.4 @@ -5835,14 +5838,14 @@ packages: - immer dev: false - /@reactflow/node-toolbar@1.1.0-next.1(react-dom@18.2.0)(react@18.2.0): + /@reactflow/node-toolbar@1.1.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-eNMS5nr9ehGnzIWqJohVx3uAGrCGxmcW7SonRyolIO6f3DqxNeg4jt6r+uNLkc6ZfndLgPZa34LsQA4EEU/aEg==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: '@babel/runtime': 7.21.0 - '@reactflow/core': 11.4.0-next.1(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.4.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -17584,17 +17587,17 @@ packages: react: 18.2.0 dev: false - /reactflow@11.4.0-next.1(react-dom@18.2.0)(react@18.2.0): + /reactflow@11.4.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-csAycDAeNSq1saVPhjNoNpell9VfLeXId0ojgl/GqASq2YLVDfBr9xVM34obB8l+k4zs2M4zpj1wZsBDqSdzBA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/background': 11.1.0-next.1(react-dom@18.2.0)(react@18.2.0) - '@reactflow/controls': 11.1.0-next.1(react-dom@18.2.0)(react@18.2.0) - '@reactflow/core': 11.4.0-next.1(react-dom@18.2.0)(react@18.2.0) - '@reactflow/minimap': 11.3.0-next.1(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-toolbar': 1.1.0-next.1(react-dom@18.2.0)(react@18.2.0) + '@reactflow/background': 11.1.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/controls': 11.1.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.4.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/minimap': 11.3.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-toolbar': 1.1.0-next.1(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -20302,7 +20305,7 @@ packages: dependencies: '@types/node': 17.0.12 esbuild: 0.17.12 - postcss: 8.4.21 + postcss: 8.4.31 resolve: 1.22.1 rollup: 3.20.0 sass: 1.64.2