Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
Commits on Source (4)
Showing
with 409 additions and 171 deletions
GRAPHPOLARIS_VERSION=dev
BACKEND_URL=http://localhost
BACKEND_WSS_URL=ws://localhost:3001/ws
STAGING=dev
SKIP_LOGIN=true
BACKEND_USER=:3001
GRAPHPOLARIS_VERSION=dev
SENTRY_ENABLED=false
SENTRY_URL=
GP_AUTH_URL=
WIP_TABLEVIS=false
WIP_NODELINKVIS=false
WIP_RAWJSONVIS=false
WIP_PAOHVIS=true
WIP_MATRIXVIS=true
WIP_SEMANTICSUBSTRATESVIS=true
WIP_MAPVIS=true
WIP_INSIGHT_SHARING=true
WIP_VIEWER_PERMISSIONS=true
WIP_SHARABLE_EXPLORATION=true
......@@ -3,7 +3,7 @@ VITE_BACKEND_WSS_URL=ws://api.graphpolaris.com/socket/
VITE_STAGING=prod
VITE_SKIP_LOGIN=false
VITE_BACKEND_USER=/user
VITE_BACKEND_USER=/socket
VITE_BACKEND_QUERY=/query
VITE_BACKEND_SCHEMA=/schema
......
......@@ -8,16 +8,16 @@
/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
* We do not test components/renderfunctions/styling files.
* See testing plan for more details.*/
import React, { useState, useRef, useEffect, useCallback } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { useAuthCache, useAuthentication } from '@graphpolaris/shared/lib/data-access';
import { DropdownItem } from '@graphpolaris/shared/lib/components/dropdowns';
import GpLogo from './gp-logo';
import { Popover, PopoverContent, PopoverTrigger } from '@graphpolaris/shared/lib/components/layout/Popover';
import { useDispatch } from 'react-redux';
import { showManagePermissions, showSharableExploration } from 'config';
import { Button, Dialog, DialogContent, DialogTrigger, useActiveSaveStateAuthorization, useSessionCache } from '@graphpolaris/shared';
import { showSharableExploration } from 'config';
import { Button, useActiveSaveStateAuthorization } from '@graphpolaris/shared';
import { ManagementTrigger, ManagementViews } from '@graphpolaris/shared/lib/management';
import { Members } from '@graphpolaris/shared/lib/management/Members';
const AuthURL = import.meta.env.GP_AUTH_URL;
export const Navbar = () => {
const dropdownRef = useRef<HTMLDivElement>(null);
......@@ -88,7 +88,7 @@ export const Navbar = () => {
<DropdownItem
value="Log out"
onClick={() => {
location.replace(`${import.meta.env['GP_AUTH_URL']}/flows/-/default/invalidation/`);
location.replace(`${AuthURL}/outpost.goauthentik.io/sign_out`);
}}
/>
</>
......
......@@ -24,7 +24,7 @@ interface TooltipOptions {
placement?: Placement;
open?: boolean;
onOpenChange?: (open: boolean) => void;
boundaryElement?: React.RefObject<HTMLElement> | null;
boundaryElement?: React.RefObject<HTMLElement> | HTMLElement | null;
showArrow?: boolean;
interactive?: boolean;
}
......@@ -60,14 +60,14 @@ export function useTooltip({
flip({
crossAxis: placement.includes('-'),
fallbackAxisSideDirection: 'start',
padding: 5
padding: 5,
}),
shift({ padding: 5 }),
],
};
if (boundaryElement != null) {
const boundary = boundaryElement?.current ?? undefined;
const boundary = boundaryElement instanceof HTMLElement ? (boundaryElement ?? undefined) : (boundaryElement?.current ?? undefined);
config.middleware.find((x) => x.name == 'flip')!.options[0].boundary = boundary;
config.middleware.find((x) => x.name == 'shift')!.options[0].boundary = boundary;
config.middleware.push(hide({ boundary }));
......@@ -125,9 +125,7 @@ export function Tooltip({ children, ...options }: { children: React.ReactNode }
// or other positioning options.
const tooltip = useTooltip(options);
return <TooltipContext.Provider value={tooltip}>
{children}
</TooltipContext.Provider>;
return <TooltipContext.Provider value={tooltip}>{children}</TooltipContext.Provider>;
}
export const TooltipTrigger = React.forwardRef<HTMLElement, React.HTMLProps<HTMLElement> & { asChild?: boolean; x?: number; y?: number }>(
......
......@@ -77,11 +77,13 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
console.debug('Setting state fetched from database', saveStateID, saveStates);
const state = saveStates[saveStateID];
if (state) {
dispatch(setQuerybuilderNodes(state.queryBuilder));
if (state.queries && state.queries.length > 0) {
dispatch(setQuerybuilderNodes(state.queries[0]));
}
dispatch(
setVisualizationState(
Object.keys(state.visualization).length !== 0 // should only occur in mock data
? (state.visualization as VisState)
Object.keys(state.visualizations).length !== 0 // should only occur in mock data
? (state.visualizations as VisState)
: {
activeVisualizationIndex: -1,
openVisualizationArray: [],
......@@ -189,7 +191,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
if (response && response.result) {
dispatch(setQueryText(response));
}
}, 'query_translation_result');
}, 'queryTranslation_result');
login();
......@@ -199,7 +201,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
unsubs.forEach((unsub) => {
unsub();
});
Broker.instance().unSubscribeAll('query_translation_result');
Broker.instance().unSubscribeAll('queryTranslation_result');
Broker.instance().unSubscribeAll('schema_stats_result');
Broker.instance().unSubscribeAll('schema_inference');
};
......@@ -208,9 +210,9 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
useEffect(() => {
if (session.currentSaveState) {
let state = { ...session.saveStates[session.currentSaveState] };
if (!isEqual(state.queryBuilder, queryBuilder) && state.queryBuilder?.graph?.nodes) {
console.debug('Updating queryBuilder state', state.queryBuilder, queryBuilder);
state.queryBuilder = { ...queryBuilder };
if (!isEqual(state.queries?.[0], queryBuilder)) {
console.debug('Updating queryBuilder state', state.queries, queryBuilder);
state.queries = [{ ...queryBuilder }];
dispatch(updateSelectedSaveState(state));
wsUpdateState(state);
}
......@@ -220,9 +222,9 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
useEffect(() => {
if (session.currentSaveState) {
let state = { ...session.saveStates[session.currentSaveState] };
if (!isEqual(state.visualization, visState)) {
if (!isEqual(state.visualizations, visState)) {
console.debug('Updating visState state', visState);
state.visualization = { ...visState };
state.visualizations = { ...visState };
dispatch(updateSelectedSaveState(state));
wsUpdateState(state);
}
......
......@@ -12,7 +12,7 @@ import { ReceiveMessageI, SendMessageI, SendMessageWithSessionI } from './types'
* It works with routingkeys, a listener can subscribe to messages from the backend with a specific routingkey.
* Possible routingkeys:
* - query_result: Contains an object with nodes and edges or a numerical result.
* - query_translation_result: Contains the query translated to the database language.
* - queryTranslation_result: Contains the query translated to the database language.
* - schema_result: Contains the schema of the users database.
* - query_status_update: Contains an update to if a query is being executed.
* - query_database_error: Contains the error received from the database.
......
......@@ -24,9 +24,9 @@ export function wsManualQueryRequest(query: string) {
type QueryTranslationResponse = (data: QueryBuilderText) => void;
export function wsQueryTranslationSubscription(callback: QueryTranslationResponse) {
const id = Broker.instance().subscribe(callback, 'query_translation_result');
const id = Broker.instance().subscribe(callback, 'queryTranslation_result');
return () => {
Broker.instance().unSubscribe('query_translation_result', id);
Broker.instance().unSubscribe('queryTranslation_result', id);
};
}
......
import { QueryBuilderState } from '../store/querybuilderSlice';
import { QueryBuilderState, SchemaState } from '../store/querybuilderSlice';
import { URLParams, setParam } from '../api/url';
import { Broker } from './broker';
import { DateStringStatement } from '../../querybuilder/model/logic/general';
......@@ -9,8 +9,8 @@ export const databaseNameMapping: string[] = ['arangodb', 'neo4j'];
export const databaseProtocolMapping: string[] = ['neo4j://', 'neo4j+s://', 'bolt://', 'bolt+s://'];
export enum DatabaseType {
ArangoDB = 0,
Neo4j = 1,
ArangoDB = 'arango',
Neo4j = 'neo4j',
}
export enum DatabaseStatus {
......@@ -26,7 +26,7 @@ export type DatabaseInfo = {
protocol: string;
username: string;
password: string;
type: number;
type: string;
};
export const SaveStateAuthorizationObjectsArray = ['database', 'visualization', 'query', 'schema'] as const;
......@@ -42,13 +42,15 @@ export const nilUUID = '00000000-0000-0000-0000-000000000000';
export type SaveStateI = {
id: string;
user_id: string;
userId: string;
name: string;
db: DatabaseInfo;
schema: any;
queryBuilder: QueryBuilderState;
visualization: VisState | {};
share_state: any;
dbConnections: DatabaseInfo[];
schemas: SchemaState[];
queries: QueryBuilderState[];
visualizations: VisState;
createdAt: string;
updatedAt: string;
shareState: any;
};
type GetStateResponse = (data: SaveStateI) => void;
......@@ -169,6 +171,11 @@ export function wsUpdateState(request: SaveStateI, callback?: GetStateResponse)
}
export function wsTestDatabaseConnection(dbConnection: DatabaseInfo, callback?: TestSaveStateConnectionResponse) {
if (!dbConnection) {
console.warn('dbConnection is undefined on wsTestDatabaseConnection');
if (callback) callback({ status: 'fail', saveStateID: '' });
return;
}
Broker.instance().sendMessage(
{
key: 'dbConnection',
......
......@@ -20,10 +20,11 @@ export const useAuthentication = () => {
const login = () => {
fetch(`${domain}${userURI}/headers`, fetchSettings)
.then((res) =>
.then((res) => {
res
.json()
.then((res: UserAuthenticationHeader) => {
console.log(res, 'headers');
dispatch(
authenticated({
username: res.username,
......@@ -35,8 +36,8 @@ export const useAuthentication = () => {
}),
);
})
.catch(handleError),
)
.catch(handleError);
})
.catch(handleError);
};
......
......@@ -36,6 +36,10 @@ export type QueryBuilderState = {
unionTypes: { [nodeId: string]: QueryUnionType };
};
export type SchemaState = {
settings: Record<string, any>;
}
// Define the initial state using that type
export const initialState: QueryBuilderState = {
graph: defaultGraph(),
......@@ -65,6 +69,11 @@ export const querybuilderSlice = createSlice({
state.graph = action.payload;
state.ignoreReactivity = false;
},
/**
* Sets the querybuilder nodes, settings, and attributes being shown,
* if the payload contains the required information.
* @param {QueryBuilderState} action.payload the payload with the new state
*/
setQuerybuilderNodes: (state: QueryBuilderState, action: PayloadAction<QueryBuilderState>) => {
if (action.payload.graph?.nodes && action.payload.graph?.edges) {
state.graph = action.payload.graph;
......
......@@ -5,8 +5,8 @@ import { isEqual } from 'lodash-es';
export type VisStateSettings = VisualizationSettingsType[];
export type VisState = {
activeVisualizationIndex: number;
openVisualizationArray: VisStateSettings;
activeVisualizationIndex: number; // uses underscore_case to match data model from backend
openVisualizationArray: VisStateSettings; // uses underscore_case to match data model from backend
};
export const initialState: VisState = {
......
......@@ -12,13 +12,13 @@ export function ConnectionInspector() {
<span className="text-xs font-semibold">Name</span>
<span className="text-xs">{session.saveStates[session.currentSaveState].name}</span>
<span className="text-xs font-semibold">Database</span>
<span className="text-xs">{session.saveStates[session.currentSaveState].db.internalDatabaseName}</span>
<span className="text-xs">{session.saveStates[session.currentSaveState].dbConnections?.[0]?.internalDatabaseName}</span>
<span className="text-xs font-semibold">Protocol</span>
<span className="text-xs">{session.saveStates[session.currentSaveState].db.protocol}</span>
<span className="text-xs">{session.saveStates[session.currentSaveState].dbConnections?.[0]?.protocol}</span>
<span className="text-xs font-semibold">Hostname</span>
<span className="text-xs">{session.saveStates[session.currentSaveState].db.url}</span>
<span className="text-xs">{session.saveStates[session.currentSaveState].dbConnections?.[0]?.url}</span>
<span className="text-xs font-semibold">Port</span>
<span className="text-xs">{session.saveStates[session.currentSaveState].db.port}</span>
<span className="text-xs">{session.saveStates[session.currentSaveState].dbConnections?.[0]?.port}</span>
</div>
)}
</div>
......
......@@ -15,7 +15,6 @@ export function InspectorPanel(props: { children?: React.ReactNode }) {
const selection = useSelection();
const focus = useFocus();
const dispatch = useDispatch();
const { activeVisualizationIndex } = useVisualization();
const inspector = useMemo(() => {
if (selection) return <SelectionConfig />;
......
......@@ -6,20 +6,24 @@ import { databaseNameMapping, databaseProtocolMapping, DatabaseType, Input, nilU
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,
},
schema: {},
queryBuilder: qbInitialState,
visualization: {},
share_state: {},
user_id: '',
dbConnections: [
{
username: 'neo4j',
password: 'DevOnlyPass',
url: 'localhost',
port: 7687,
protocol: 'neo4j://',
internalDatabaseName: 'neo4j',
type: DatabaseType.Neo4j,
},
],
schemas: [{ settings: {} }],
queries: [qbInitialState],
visualizations: { activeVisualizationIndex: -1, openVisualizationArray: [] },
shareState: {},
createdAt: '',
updatedAt: '',
userId: '',
};
export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveStateI, error: boolean) => void }) => {
......@@ -29,7 +33,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
function handlePortChanged(port: string): void {
if (!isNaN(Number(port)))
setFormData((draft) => {
draft.db.port = Number(port);
draft.dbConnections[0].port = Number(port);
return draft;
});
}
......@@ -54,7 +58,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
<Input
type="text"
label="Internal database name"
value={formData.db.internalDatabaseName}
value={formData.dbConnections[0].internalDatabaseName}
placeholder="internalDatabaseName"
required
errorText="This field is required"
......@@ -64,7 +68,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
}}
onChange={(value: string) =>
setFormData((draft) => {
draft.db.internalDatabaseName = value;
draft.dbConnections[0].internalDatabaseName = value;
})
}
/>
......@@ -75,11 +79,11 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
className="w-full"
label="Database Type"
required
value={databaseNameMapping[formData.db.type]}
value={formData.dbConnections[0].type}
options={databaseNameMapping}
onChange={(value: string | number) => {
setFormData((draft) => {
draft.db.type = databaseNameMapping.indexOf(value.toString());
draft.dbConnections[0].type = value.toString();
});
}}
/>
......@@ -88,12 +92,12 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
type="dropdown"
label="Database Protocol"
required
value={formData.db.protocol}
value={formData.dbConnections[0].protocol}
options={databaseProtocolMapping}
info="Protocol via which the database connection will be established"
onChange={(value: string | number) => {
setFormData((draft) => {
draft.db.protocol = value.toString();
draft.dbConnections[0].protocol = value.toString();
});
}}
/>
......@@ -103,7 +107,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
<Input
type="text"
label="Hostname/IP"
value={formData.db.url}
value={formData.dbConnections[0].url}
placeholder="neo4j"
required
errorText="This field is required"
......@@ -114,7 +118,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
}}
onChange={(value: string) => {
setFormData((draft) => {
draft.db.url = value;
draft.dbConnections[0].url = value;
});
}}
/>
......@@ -122,7 +126,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
<Input
type="text"
label="Port"
value={formData.db.port.toString()}
value={formData.dbConnections[0].port.toString()}
placeholder="neo4j"
required
errorText="Must be between 1 and 9999"
......@@ -133,7 +137,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
}}
onChange={(value: string) => {
setFormData((draft) => {
draft.db.port = Number(value);
draft.dbConnections[0].port = Number(value);
});
}}
/>
......@@ -143,7 +147,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
<Input
type="text"
label="Username"
value={formData.db.username}
value={formData.dbConnections[0].username}
placeholder="username"
required
errorText="This field is required"
......@@ -154,7 +158,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
}}
onChange={(value: string) => {
setFormData((draft) => {
draft.db.username = value;
draft.dbConnections[0].username = value;
});
}}
/>
......@@ -163,7 +167,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
type="text"
visible={false}
label="Password"
value={formData.db.password}
value={formData.dbConnections[0].password}
placeholder="password"
required
errorText="This field is required"
......@@ -174,7 +178,7 @@ export const DatabaseForm = (props: { data: SaveStateI; onChange: (data: SaveSta
}}
onChange={(value: string) => {
setFormData((draft) => {
draft.db.password = value;
draft.dbConnections[0].password = value;
});
}}
/>
......
......@@ -66,7 +66,8 @@ export function Databases({ onClose, saveStates, changeActive, setSelectedSaveSt
if (saveStates[a].name.toLowerCase() <= saveStates[b].name.toLowerCase()) return dir;
else return -dir;
} else {
if (saveStates[a].db[orderBy[0]].toLowerCase() <= saveStates[b].db[orderBy[0]].toLowerCase()) return dir;
if (saveStates[a].dbConnections?.[0][orderBy[0]].toLowerCase() <= saveStates[b].dbConnections?.[0][orderBy[0]].toLowerCase())
return dir;
else return -dir;
}
}),
......@@ -119,10 +120,10 @@ export function Databases({ onClose, saveStates, changeActive, setSelectedSaveSt
</Button>
</td>
<td className="text-left">
<span className="font-light">{saveStates[key].db.protocol}</span>
<span className="font-light">{saveStates[key].dbConnections?.[0]?.protocol}</span>
</td>
<td className="text-left">
<span className="font-light">{saveStates[key].db.url}</span>
<span className="font-light">{saveStates[key].dbConnections?.[0]?.url}</span>
</td>
<td className="text-right flex justify-end">
<Button
......
......@@ -13,120 +13,144 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [
name: 'Recommendations',
subtitle: 'Hosted by Neo4j',
description: 'Network of movies, actors, directors and reviews by people',
db: {
username: 'recommendations',
password: 'recommendations',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'recommendations',
type: DatabaseType.Neo4j,
},
schema: {},
queryBuilder: qbInitialState,
visualization: {},
share_state: {},
user_id: '',
dbConnections: [
{
username: 'recommendations',
password: 'recommendations',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'recommendations',
type: DatabaseType.Neo4j,
},
],
schemas: [],
queries: [qbInitialState],
visualizations: { activeVisualizationIndex: -1, openVisualizationArray: [] },
shareState: {},
userId: '',
createdAt: '',
updatedAt: '',
},
{
id: nilUUID,
name: 'Movies',
subtitle: 'Hosted by Neo4j',
description: 'Movies and people related to those movies as actors, directors and producers',
db: {
username: 'movies',
password: 'movies',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'movies',
type: DatabaseType.Neo4j,
},
schema: {},
queryBuilder: qbInitialState,
visualization: {},
share_state: {},
user_id: '',
dbConnections: [
{
username: 'movies',
password: 'movies',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'movies',
type: DatabaseType.Neo4j,
},
],
schemas: [],
queries: [qbInitialState],
visualizations: { activeVisualizationIndex: -1, openVisualizationArray: [] },
shareState: {},
userId: '',
createdAt: '',
updatedAt: '',
},
{
id: nilUUID,
name: 'Northwind',
subtitle: 'Hosted by Neo4j',
description: 'Retail-system with products, orders, customers, suppliers and employees',
db: {
username: 'northwind',
password: 'northwind',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'northwind',
type: DatabaseType.Neo4j,
},
schema: {},
queryBuilder: qbInitialState,
visualization: {},
share_state: {},
user_id: '',
dbConnections: [
{
username: 'northwind',
password: 'northwind',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'northwind',
type: DatabaseType.Neo4j,
},
],
schemas: [],
queries: [qbInitialState],
visualizations: { activeVisualizationIndex: -1, openVisualizationArray: [] },
shareState: {},
userId: '',
createdAt: '',
updatedAt: '',
},
{
id: nilUUID,
name: 'Fincen',
subtitle: 'Hosted by Neo4j',
description: 'FinCEN files investigation for banks and countries',
db: {
username: 'fincen',
password: 'fincen',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'fincen',
type: DatabaseType.Neo4j,
},
schema: {},
queryBuilder: qbInitialState,
visualization: {},
share_state: {},
user_id: '',
dbConnections: [
{
username: 'fincen',
password: 'fincen',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'fincen',
type: DatabaseType.Neo4j,
},
],
schemas: [],
queries: [qbInitialState],
visualizations: { activeVisualizationIndex: -1, openVisualizationArray: [] },
shareState: {},
userId: '',
createdAt: '',
updatedAt: '',
},
{
id: nilUUID,
name: 'Slack',
subtitle: 'Hosted by Neo4j',
description: 'Communication network consisting of several types of users and messages',
db: {
username: 'slack',
password: 'slack',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'slack',
type: DatabaseType.Neo4j,
},
schema: {},
queryBuilder: qbInitialState,
visualization: {},
share_state: {},
user_id: '',
dbConnections: [
{
username: 'slack',
password: 'slack',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'slack',
type: DatabaseType.Neo4j,
},
],
schemas: [],
queries: [qbInitialState],
visualizations: { activeVisualizationIndex: -1, openVisualizationArray: [] },
shareState: {},
userId: '',
createdAt: '',
updatedAt: '',
},
{
id: nilUUID,
name: 'Game of Thrones',
subtitle: 'Hosted by Neo4j',
description: 'Character interactions and actors in the Game of Thrones movie',
db: {
username: 'gameofthrones',
password: 'gameofthrones',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'gameofthrones',
type: DatabaseType.Neo4j,
},
schema: {},
queryBuilder: qbInitialState,
visualization: {},
share_state: {},
user_id: '',
dbConnections: [
{
username: 'gameofthrones',
password: 'gameofthrones',
url: 'demo.neo4jlabs.com',
port: 7687,
protocol: 'neo4j+s://',
internalDatabaseName: 'gameofthrones',
type: DatabaseType.Neo4j,
},
],
schemas: [],
queries: [qbInitialState],
visualizations: { activeVisualizationIndex: -1, openVisualizationArray: [] },
shareState: {},
userId: '',
createdAt: '',
updatedAt: '',
},
];
......
......@@ -16,10 +16,10 @@ export const UpsertDatabase = (props: {
const databaseHandler = useHandleDatabase();
const ref = useRef<HTMLDialogElement>(null);
const authCache = useAuthCache();
const [formData, setFormData] = useImmer(
const [formData, setFormData] = useImmer<SaveStateI>(
props.saveState && props.open === 'update'
? props.saveState
: { ...INITIAL_SAVE_STATE, user_id: authCache.authentication?.userID || '' },
: { ...INITIAL_SAVE_STATE, userId: authCache.authentication?.userID || '' },
);
const [hasError, setHasError] = useState(false);
const [sampleDataPanel, setSampleDataPanel] = useState<boolean | null>(false);
......@@ -42,7 +42,7 @@ export const UpsertDatabase = (props: {
}
function closeDialog(): void {
setFormData({ ...INITIAL_SAVE_STATE, user_id: authCache.authentication?.userID || '' });
setFormData({ ...INITIAL_SAVE_STATE, userId: authCache.authentication?.userID || '' });
ref.current?.close();
props.onClose();
}
......@@ -54,7 +54,7 @@ export const UpsertDatabase = (props: {
<SampleDatabaseSelector
onClick={(data) => {
setHasError(false);
handleSubmit({ ...data, user_id: authCache.authentication?.userID || '' });
handleSubmit({ ...data, userId: authCache.authentication?.userID || '' });
}}
/>
) : (
......
......@@ -32,20 +32,21 @@ export const useHandleDatabase = () => {
forceAdd: boolean = false,
concludedCallback: () => void,
): Promise<void> {
console.log('submitDatabaseChange', saveStateData);
setConnectionStatus(() => ({
updating: true,
status: 'Testing database connection',
verified: null,
}));
wsTestDatabaseConnection(saveStateData.db, (data) => {
wsTestDatabaseConnection(saveStateData.dbConnections?.[0], (data) => {
if (!saveStateData) {
console.error('formData is null');
return;
}
if (saveStateData.user_id !== authCache.authentication?.userID && authCache.authentication?.userID) {
if (saveStateData.userId !== authCache.authentication?.userID && authCache.authentication?.userID) {
console.error('user_id is not equal to auth.userID');
saveStateData.user_id = authCache.authentication.userID;
saveStateData.userId = authCache.authentication.userID;
}
if (data && data.status === 'success') {
setConnectionStatus((prevState) => ({
......
import { useEffect, useMemo, useState } from 'react';
import { Tooltip, TooltipTrigger, TooltipContent, DropdownItem, TextInput, Icon } from '../../components';
import { ReactFlowInstance, Node } from 'reactflow';
import { NodeAttribute, SchemaReactflowEntityNode } from '../model';
import {
useAppDispatch,
useQuerybuilderAttributesShown,
useQuerybuilderGraph,
useQuerybuilderHash,
useQuerybuilderUnionTypes,
} from '../..';
import { isEqual } from 'lodash-es';
import {
attributeShownToggle,
QueryUnionType,
setQuerybuilderGraphology,
setQueryUnionType,
toQuerybuilderGraphology,
} from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
import { getDataTypeIcon } from '../../components/DataTypeIcon';
export const ContextMenu = (props: {
open: boolean;
position?: { x: number; y: number };
node?: Node;
reactFlowWrapper: React.RefObject<HTMLDivElement>;
reactFlow: ReactFlowInstance;
onClose: () => void;
}) => {
const [filter, setFilter] = useState<string>('');
const dispatch = useAppDispatch();
const graph = useQuerybuilderGraph();
const qbHash = useQuerybuilderHash();
const graphologyGraph = useMemo(() => toQuerybuilderGraphology(graph), [graph, qbHash]);
const state = useMemo(() => {
const divPos = props.reactFlowWrapper.current?.getBoundingClientRect();
if (!divPos || !props.node) return;
let position = { x: 0, y: 0 };
if (props.position) {
position = props.position;
} else {
position = props.reactFlow.flowToScreenPosition({ x: props.node.data.x, y: props.node.data.y });
}
return {
open: props.open,
x: position.x - divPos.x,
y: position.y - divPos.y + 10,
};
}, [props.open, props.node]);
const filteredAttributes = useMemo<NodeAttribute[]>(() => {
if (props.node == null) return [];
if (filter == null || filter.length == 0) return props.node.data.attributes;
return (props.node.data.attributes as NodeAttribute[]).filter((attr) => {
return attr.handleData.attributeName?.toLocaleLowerCase().includes(filter.toLocaleLowerCase());
});
}, [filter, props.node]);
const attributesBeingShown = useQuerybuilderAttributesShown();
function isAttributeAdded(attribute: NodeAttribute): boolean {
return attributesBeingShown.some((x) => isEqual(x, attribute.handleData));
}
function addAttribute(attribute: NodeAttribute) {
dispatch(attributeShownToggle(attribute.handleData));
}
const unionType = useQuerybuilderUnionTypes();
function setUnionType(unionType: QueryUnionType) {
if (!props.node) return;
dispatch(setQueryUnionType({ nodeId: props.node.id, unionType: unionType }));
}
function removeNode() {
if (!props.node) return;
graphologyGraph.dropNode(props.node.id);
dispatch(setQuerybuilderGraphology(graphologyGraph));
props.onClose();
}
return (
<Tooltip open={props.open && state !== undefined} interactive={true} showArrow={false} placement="bottom-start">
<TooltipTrigger x={state ? state.x : 0} y={state ? state.y : 0} />
<TooltipContent>
<DropdownItem
value={'Add/remove attribute'}
onClick={(e) => {}}
submenu={[
<TextInput
type={'text'}
placeholder="Filter"
size="xs"
className="mb-1 min-w-40 rounded-sm"
value={filter}
onClick={(e) => e.stopPropagation()}
onChange={(v) => setFilter(v)}
/>,
filteredAttributes.map((attr) => (
<DropdownItem
key={attr.handleData.attributeName + attr.handleData.nodeId}
value={attr.handleData.attributeName ?? ''}
selected={isAttributeAdded(attr)}
onClick={(_) => addAttribute(attr)}
>
<Icon component={getDataTypeIcon(attr?.handleData?.attributeType)} className="ms-2 float-end" size={16} />
</DropdownItem>
)),
]}
/>
<DropdownItem
value="Union type"
submenu={[
<DropdownItem
value="AND"
onClick={(_) => setUnionType(QueryUnionType.AND)}
selected={props.node ? unionType[props.node.id] != QueryUnionType.OR : false} // Also selected when null
/>,
<DropdownItem
value="OR"
onClick={(_) => setUnionType(QueryUnionType.OR)}
selected={props.node ? unionType[props.node.id] == QueryUnionType.OR : false}
/>,
]}
/>
<DropdownItem value="Remove" className="text-danger" onClick={(e) => removeNode()} />
</TooltipContent>
</Tooltip>
);
};
......@@ -9,7 +9,11 @@ import {
useSchemaInference,
useSearchResultQB,
} from '@graphpolaris/shared/lib/data-access/store';
import { setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
import {
QueryUnionType,
setQuerybuilderGraphology,
toQuerybuilderGraphology,
} from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import ReactFlow, {
Background,
......@@ -22,6 +26,7 @@ import ReactFlow, {
OnConnectStartParams,
ReactFlowInstance,
ReactFlowProvider,
Viewport,
isNode,
useReactFlow,
} from 'reactflow';
......@@ -40,6 +45,7 @@ import { ConnectingNodeDataI } from './utils/connectorDrop';
import { resultSetFocus } from '../../data-access/store/interactionSlice';
import { QueryBuilderDispatcherContext } from './QueryBuilderDispatcher';
import { QueryBuilderNav, QueryBuilderToggleSettings } from './QueryBuilderNav';
import { ContextMenu } from './ContextMenu';
export type QueryBuilderProps = {
onRunQuery?: () => void;
......@@ -82,6 +88,9 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
const searchResults = useSearchResultQB();
const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null);
const [allowZoom, setAllowZoom] = useState(true);
const [contextMenuOpen, setContextMenuOpen] = useState<{ open: boolean; node?: Node; position?: { x: number; y: number } }>({
open: false,
});
useEffect(() => {
const searchResultKeys = new Set([...searchResults.nodes.map((node) => node.key), ...searchResults.edges.map((edge) => edge.key)]);
......@@ -472,6 +481,21 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
}
};
const onNodeContextMenu = (event: React.MouseEvent, node: Node) => {
if (event.shiftKey) return;
event.preventDefault();
setContextMenuOpen({ open: true, node: node, position: { x: event.clientX, y: event.clientY } });
};
const onMove = useCallback(
(event: MouseEvent | TouchEvent, viewport: Viewport) => {
if (contextMenuOpen.open) {
setContextMenuOpen({ ...contextMenuOpen, open: false });
}
},
[contextMenuOpen],
);
useEffect(() => {
try {
applyLayout();
......@@ -502,6 +526,16 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
},
}}
>
<ContextMenu
open={contextMenuOpen.open}
node={contextMenuOpen.node}
position={contextMenuOpen.position}
reactFlowWrapper={reactFlowWrapper}
reactFlow={reactFlow}
onClose={() => {
setContextMenuOpen({ ...contextMenuOpen, open: false });
}}
/>
<div ref={reactFlowWrapper} className="h-full w-full flex flex-col">
<QueryBuilderNav
toggleSettings={toggleSettings}
......@@ -582,6 +616,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
reactFlowInstanceRef.current = reactFlowInstance;
onInit(reactFlowInstance);
}}
onMove={onMove}
onNodesChange={saveStateAuthorization.query.W ? onNodesChange : () => {}}
onDragOver={saveStateAuthorization.query.W ? onDragOver : () => {}}
onConnect={saveStateAuthorization.query.W ? onConnect : () => {}}
......@@ -594,7 +629,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
onEdgeUpdateEnd={saveStateAuthorization.query.W ? onEdgeUpdateEnd : () => {}}
onDrop={saveStateAuthorization.query.W ? onDrop : () => {}}
// onContextMenu={onContextMenu}
// onNodeContextMenu={saveStateAuthorization.query.W ? onNodeContextMenu : () => {}}
onNodeContextMenu={saveStateAuthorization.query.W ? onNodeContextMenu : () => {}}
// onNodesDelete={onNodesDelete}
// onNodesChange={onNodesChange}
deleteKeyCode="Backspace"
......