Skip to content

Commit

Permalink
Merge pull request #26 from neo4j-labs/feature/connectionModalAuraParser
Browse files Browse the repository at this point in the history
Support for dropping env files + parsing from copy/paste URIs
  • Loading branch information
msenechal authored Apr 2, 2024
2 parents 85aa994 + fd6dda3 commit 6931b2d
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 28 deletions.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"preview": "vite preview"
},
"dependencies": {
"@neo4j-ndl/base": "^2.6.0",
"@neo4j-ndl/react": "^2.6.2",
"@neo4j-ndl/base": "2.6.0",
"@neo4j-ndl/react": "2.6.2",
"@tanstack/react-table": "^8.9.3",
"autoprefixer": "^10.4.17",
"eslint-plugin-react": "^7.33.2",
Expand All @@ -21,7 +21,8 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3",
"tailwindcss": "^3.4.1"
"tailwindcss": "^3.4.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/node": "^20.11.16",
Expand All @@ -35,7 +36,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"prettier": "^2.7.1",
"typescript": "^5.0.2",
"typescript": "5.3.3",
"vite": "^4.4.5"
}
}
10 changes: 9 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import ConnectionModal from './templates/shared/components/ConnectionModal';
import Header from './templates/shared/components/Header';
import User from './templates/shared/components/User';

import { FileContextProvider } from './context/connectionFile';

import './ConnectionModal.css';

function App() {
const messages = messagesData.listMessages;
const [activeTab, setActiveTab] = useState<string>('Home');
Expand All @@ -33,7 +37,11 @@ function App() {
<Route path='/cybersecurity-preview' element={<Cybersecurity />} />
<Route
path='/connection-modal-preview'
element={<ConnectionModal open={true} setOpenConnection={() => null} setConnectionStatus={() => null} />}
element={
<FileContextProvider>
<ConnectionModal open={true} setOpenConnection={() => null} setConnectionStatus={() => null} />
</FileContextProvider>
}
/>
<Route path='/chat-widget-preview' element={<Chatbot messages={messages} />} />
<Route
Expand Down
5 changes: 5 additions & 0 deletions src/ConnectionModal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.ndl-dropzone-header{
display: flex!important;
flex-direction: row!important;
gap: 30px!important;
}
26 changes: 26 additions & 0 deletions src/context/connectionFile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from 'react';

interface FileContextType {
file: File[] | [];
setFile: Dispatch<SetStateAction<File[]>>;
}
const FileContext = createContext<FileContextType | undefined>(undefined);
interface FileContextProviderProps {
children: ReactNode;
}
const FileContextProvider: React.FC<FileContextProviderProps> = ({ children }) => {
const [file, setFile] = useState<File[] | []>([]);
const value: FileContextType = {
file,
setFile,
};
return <FileContext.Provider value={value}>{children}</FileContext.Provider>;
};
const useFileContext = () => {
const context = useContext(FileContext);
if (!context) {
throw new Error('useFileContext must be used within a FileContextProvider');
}
return context;
};
export { FileContextProvider, useFileContext };
102 changes: 80 additions & 22 deletions src/templates/shared/components/ConnectionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Dialog, TextInput, Dropdown, Banner } from '@neo4j-ndl/react';
import { Button, Dialog, TextInput, Dropdown, Banner, Dropzone } from '@neo4j-ndl/react';
import { useState } from 'react';
import { setDriver } from '../utils/Driver';

Expand All @@ -21,16 +21,62 @@ export default function ConnectionModal({
message,
}: ConnectionModalProps) {
const protocols = ['neo4j', 'neo4j+s', 'neo4j+ssc', 'bolt', 'bolt+s', 'bolt+ssc'];
const [selectedProtocol, setSelectedProtocol] = useState<string>('neo4j');
const [hostname, setHostname] = useState<string>('localhost');
const [protocol, setProtocol] = useState<string>('neo4j+s');
const [URI, setURI] = useState<string>('localhost');
const [port, setPort] = useState<number>(7687);
const [database, setDatabase] = useState<string>('neo4j');
const [username, setUsername] = useState<string>('neo4j');
const [password, setPassword] = useState<string>('password');
const [connectionMessage, setMessage] = useState<Message | null>(null);

const [isLoading, setIsLoading] = useState<boolean>(false);

const parseAndSetURI = (uri: string) => {
const uriParts = uri.split('://');
const uriHost = uriParts.pop() || URI;
setURI(uriHost);
const uriProtocol = uriParts.pop() || protocol;
setProtocol(uriProtocol);
const uriPort = Number(uriParts.pop()) || port;
setPort(uriPort);
};

const handleHostPasteChange: React.ClipboardEventHandler<HTMLInputElement> = (event) => {
event.clipboardData.items[0]?.getAsString((value) => {
parseAndSetURI(value);
});
};

const onDropHandler = async (files: Partial<globalThis.File>[]) => {
setIsLoading(true);
if (files.length) {
const [file] = files;

if (file.text) {
const text = await file.text();
const lines = text.split(/\r?\n/);
const configObject = lines.reduce((acc: Record<string, string>, line: string) => {
if (line.startsWith('#') || line.trim() === '') {
return acc;
}

const [key, value] = line.split('=');
if (['NEO4J_URI', 'NEO4J_USERNAME', 'NEO4J_PASSWORD', 'NEO4J_DATABASE'].includes(key)) {
acc[key] = value;
}
return acc;
}, {});
parseAndSetURI(configObject.NEO4J_URI);
setUsername(configObject.NEO4J_USERNAME);
setPassword(configObject.NEO4J_PASSWORD);
setDatabase(configObject.NEO4J_DATABASE);
}
}
setIsLoading(false);
};

function submitConnection() {
const connectionURI = `${selectedProtocol}://${hostname}:${port}`;
const connectionURI = `${protocol}://${URI}${URI.split(':')[1] ? '' : `:${port}`}`;
setDriver(connectionURI, username, password).then((isSuccessful) => {
setConnectionStatus(isSuccessful);
isSuccessful
Expand All @@ -49,41 +95,53 @@ export default function ConnectionModal({
<Dialog.Content className='n-flex n-flex-col n-gap-token-4'>
{message && <Banner type={message.type}>{message.content}</Banner>}
{connectionMessage && <Banner type={connectionMessage.type}>{connectionMessage.content}</Banner>}
<div className='n-flex max-h-24'>
<Dropzone
isTesting={false}
customTitle={<>Drop your env file here</>}
className='n-p-6 end-0 top-0 w-full h-full'
acceptedFileExtensions={['.txt', '.env']}
dropZoneOptions={{
onDrop: (f: Partial<globalThis.File>[]) => {
onDropHandler(f);
},
maxSize: 500,
onDropRejected: (e) => {
if (e.length) {
// eslint-disable-next-line no-console
console.log(`Failed To Upload, File is larger than 500 bytes`);
}
},
}}
/>
{isLoading && <div>Loading...</div>}
</div>
<div className='n-flex n-flex-row n-flex-wrap'>
<Dropdown
id='protocol'
label='Protocol'
type='select'
size='medium'
disabled={false}
selectProps={{
onChange: (newValue) => newValue && setSelectedProtocol(newValue.value),
onChange: (newValue) => newValue && setProtocol(newValue.value),
options: protocols.map((option) => ({ label: option, value: option })),
value: { label: selectedProtocol, value: selectedProtocol },
value: { label: protocol, value: protocol },
}}
className='w-1/4 inline-block'
fluid
/>
<div className='ml-[2.5%] w-[55%] mr-[2.5%] inline-block'>
<div className='ml-[5%] w-[70%] inline-block'>
<TextInput
id='url'
value={hostname}
value={URI}
disabled={false}
label='Hostname'
label='URI'
placeholder='localhost'
autoFocus
fluid
onChange={(e) => setHostname(e.target.value)}
/>
</div>
<div className='w-[15%] inline-block'>
<TextInput
id='port'
value={port}
disabled={false}
label='Port'
placeholder='7687'
fluid
onChange={(e) => setPort(Number(e.target.value))}
onChange={(e) => setURI(e.target.value)}
onPaste={(e) => handleHostPasteChange(e)}
/>
</div>
</div>
Expand All @@ -97,7 +155,7 @@ export default function ConnectionModal({
onChange={(e) => setDatabase(e.target.value)}
className='w-full'
/>
<div className='n-flex n-flex-row n-flex-wrap'>
<div className='n-flex n-flex-row n-flex-wrap mb-2'>
<div className='w-[48.5%] mr-1.5 inline-block'>
<TextInput
id='username'
Expand Down
2 changes: 1 addition & 1 deletion src/templates/shared/utils/Driver.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
import neo4j, { Driver } from 'neo4j-driver';

let driver: Driver;
export let driver: Driver;

export async function setDriver(connectionURI: string, username: string, password: string) {
try {
Expand Down

0 comments on commit 6931b2d

Please sign in to comment.