diff --git a/app/package-lock.json b/app/package-lock.json index af9d037..e5aac93 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,6 +8,8 @@ "name": "app", "version": "0.0.0", "dependencies": { + "@headlessui/react": "^1.7.18", + "@heroicons/react": "^2.1.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -826,6 +828,30 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@headlessui/react": { + "version": "1.7.18", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz", + "integrity": "sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@heroicons/react": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.1.tgz", + "integrity": "sha512-JyyN9Lo66kirbCMuMMRPtJxtKJoIsXKS569ebHGGRKbl8s4CtUfLnyKJxteA+vIKySocO4s1SkTkGS4xtG/yEA==", + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -1177,6 +1203,31 @@ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.1.tgz", + "integrity": "sha512-IFOFuRUTaiM/yibty9qQ9BfycQnYXIDHGP2+cU+0LrFFGNhVxCXSQnaY6wkX8uJVteFEBjUondX0Hmpp7TNcag==", + "dependencies": { + "@tanstack/virtual-core": "3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz", + "integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1704,6 +1755,11 @@ "node": ">= 6" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", diff --git a/app/package.json b/app/package.json index 29072f6..1c72cdc 100644 --- a/app/package.json +++ b/app/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@headlessui/react": "^1.7.18", + "@heroicons/react": "^2.1.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/app/src/App.css b/app/src/App.css index b9d355d..450bf4d 100644 --- a/app/src/App.css +++ b/app/src/App.css @@ -1,4 +1,4 @@ -#root { +/*#root { max-width: 1280px; margin: 0 auto; padding: 2rem; @@ -40,3 +40,4 @@ .read-the-docs { color: #888; } +*/ \ No newline at end of file diff --git a/app/src/App.jsx b/app/src/App.jsx index 2be4772..fb4f232 100644 --- a/app/src/App.jsx +++ b/app/src/App.jsx @@ -1,10 +1,117 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import reactLogo from './assets/react.svg' import viteLogo from '/vite.svg' import './App.css' +import Home from './pages/Home' -function Signin() { - const [state, setState] = useState({email: '', password: ''}) +function API(url, user) { + const call = (verb, route, callbacks, body) => { + const headers = { + method: verb + } + if(body) { + headers['body'] = JSON.stringify(body) + } + let response = fetch(url+route, headers) + if(callbacks && callbacks.success !== undefined) { + response = response.then(r => r.ok && callbacks.success()) + } + else if(callbacks && callbacks.json !== undefined) { + response = response + .then(r => (r.ok && r.json()) || (Promise.reject(r.text()))) + .then(callbacks.json) + } + if(callbacks && callbacks.error !== undefined) { + response = response.catch(err => { + console.log(err) + callbacks.error({error: err}) + }) + } + return response + } + //const callv2 = (verb, route, return_type, body) => {} + const get = (route, callbacks) => call('GET', route, callbacks) + const post = (route, body, callbacks) => call('POST', route, callbacks, body) + return { + get: get, + post: post, + auth: { + sendOTP: (user_email, onSuccess) => { + console.log('/login/otp?user_email='+user_email) + get('/login/otp?user_email='+user_email, {success: onSuccess}) + }, + getToken: (user_email, otp, onSuccess, onError) => { + post('/login', {otp: otp, user_email: user_email}, { + json: (token) => onSuccess(token), + error: (err) => onError(err) + }) + }, + authenticated: user !== undefined + }, + userdata: { + datasources: { + read: (callback) => { + console.log(user) + get("/datasources?owner="+user.user_email, { + json: callback + }) + }, + write: (datasource) => post("/datasources", Object.assign({}, datasource, {owner: user.user_email})) + }, + notebooks: { + read: (callback) => { + get("/notebooks?owner="+user.user_email, { + json: callback + }) + }, + write: (nb) => post("/notebooks", Object.assign({}, nb, {owner: user.user_email})) + } + }, + sql: {} + } +} + +/*function Login({show, onLogin, api}) { + const [state, setState] = useState({user_email: '', otp: '', stage: 'init'}) + console.log(state) + if(!show) return + if(state.stage === 'init') + return ( + + setState(prev => Object.assign({}, prev, {user_email: email}))} + /> + + + ) + return ( + + setState(prev => Object.assign({}, prev, {otp: code}))} + /> + + + ) +}*/ + + + +function Signin({api, show, onLogin}) { + const [state, setState] = useState({user_email: '', otp: '', stage: 'init'}) + console.log(state) + if(!show) return + if(state.stage === 'init') return (
@@ -19,7 +126,6 @@ function Signin() {
-
+
+ +
+ +
+ + ) + return ( +
+
+ Your Company +

+ Sign in to your account +

+
+ +
-
- -
- - Forgot password? - -
-
+
setState(prev => Object.assign({}, prev, {otp: e.target.value}))} + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" />
-
- - -

- Not a member?{' '} - - Start a 14 day free trial - -

+ +
) } +/*function Home({show}) { + if(!show) return + return ( +
+

Home

+
+ ) +}*/ + function App() { const [count, setCount] = useState(0) - + const [state, setState] = useState({pageid: 'login'}) + const api = API('https://sequel.gessfred.xyz/api', state.user) + const setStateProperty = property => setState(prev => Object.assign({}, prev, property)) + useEffect(() => { + const cachedUser = localStorage.getItem("user") + if(cachedUser) { + setStateProperty({user: JSON.parse(cachedUser), pageid: 'main'}) + } + }, []) return ( <> - + { + localStorage.setItem('user', JSON.stringify(user)) + setState({user: user, pageid: 'main'}) + }} + /> + ) } diff --git a/app/src/components/Dropdown.jsx b/app/src/components/Dropdown.jsx new file mode 100644 index 0000000..1bb477c --- /dev/null +++ b/app/src/components/Dropdown.jsx @@ -0,0 +1,79 @@ +import { Fragment, useState } from 'react' +import { Listbox, Transition } from '@headlessui/react' +import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid' + + +function classNames(...classes) { + return classes.filter(Boolean).join(' ') +} + +export default function Dropdown({name, items, selected, setSelected}) { + + return ( + + {({ open }) => ( + <> + {name} +
+ + + + {selected.name} + + + + + + + + {items.map((person) => ( + + classNames( + active ? 'bg-indigo-600 text-white' : 'text-gray-900', + 'relative cursor-default select-none py-2 pl-3 pr-9' + ) + } + value={person} + > + {({ selected, active }) => ( + <> +
+ + + {person.name} + +
+ + {selected ? ( + + + ) : null} + + )} +
+ ))} +
+
+
+ + )} +
+ ) +} \ No newline at end of file diff --git a/app/src/index.css b/app/src/index.css index e7d4bb2..24be1a2 100644 --- a/app/src/index.css +++ b/app/src/index.css @@ -2,7 +2,7 @@ @tailwind components; @tailwind utilities; -:root { +/*:root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; @@ -70,3 +70,4 @@ button:focus-visible { background-color: #f9f9f9; } } +*/ \ No newline at end of file diff --git a/app/src/pages/ConnectionEditor.jsx b/app/src/pages/ConnectionEditor.jsx new file mode 100644 index 0000000..9cd2281 --- /dev/null +++ b/app/src/pages/ConnectionEditor.jsx @@ -0,0 +1,139 @@ +import { Fragment, useRef, useState } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { ExclamationTriangleIcon } from '@heroicons/react/24/outline' +import Dropdown from '../components/Dropdown' +//import uniqid from 'uniqid' + +const engines = [ + { + id: 1, + name: 'PostgresSQL', + avatar: + 'https://wiki.postgresql.org/images/3/30/PostgreSQL_logo.3colors.120x120.png', + }, + { + id: 2, + name: 'Arlene Mccoy', + avatar: + 'https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', + }, + { + id: 3, + name: 'Devon Webb', + avatar: + 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80', + }, + { + id: 4, + name: 'Tom Cook', + avatar: + 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', + }, + { + id: 5, + name: 'Tanya Fox', + avatar: + 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', + } +] + + +export function Input({name, value, setValue}) { + const hint = false + return ( +
+ +
+ {hint &&
+ $ +
} + setValue(e.target.value)} + /> +
+
+ ) +} + +export default function ConnectionEditor({api, show, onHide}) { + const [open, setOpen] = useState(true) + const [connection, setConnection] = useState({engine: engines[0]}) + const cancelButtonRef = useRef(null) + const setConnectionProperty = property => value => setConnection(prev => Object.assign({}, prev, {[property]: value})) + + return ( + + + +
+ + +
+
+ + +
+
+
+
+
+ + New connection + +
+ + + +
+
+
+
+
+ + +
+
+
+
+
+
+
+ ) +} diff --git a/app/src/pages/Home.jsx b/app/src/pages/Home.jsx new file mode 100644 index 0000000..67af2ab --- /dev/null +++ b/app/src/pages/Home.jsx @@ -0,0 +1,204 @@ +import { Fragment, useState } from 'react' +import { Disclosure, Menu, Transition } from '@headlessui/react' +import { Bars3Icon, BellIcon, XMarkIcon } from '@heroicons/react/24/outline' +import ConnectionEditor from './ConnectionEditor' + +const user = { + name: 'Tom Cook', + email: 'tom@example.com', + imageUrl: + 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', + } + const navigation = [ + { name: 'Dashboard', href: '#', current: true }, + { name: 'Team', href: '#', current: false }, + { name: 'Projects', href: '#', current: false }, + { name: 'Calendar', href: '#', current: false }, + { name: 'Reports', href: '#', current: false }, + ] + const userNavigation = [ + { name: 'Your Profile', href: '#' }, + { name: 'Settings', href: '#' }, + { name: 'Sign out', href: '#' }, + ] + + function classNames(...classes) { + return classes.filter(Boolean).join(' ') + } + + export default function Home({show}) { + const [state, setState] = useState({showConnectionEditor: false}) + if(!show) return + return ( + <> + {/* + This example requires updating your template: + + ``` + + + ``` + */} +
+ + {({ open }) => ( + <> +
+
+
+
+

Home

+
+
+
+ {navigation.map((item) => ( + + {item.name} + + ))} +
+
+
+
+
+ + + {/* Profile dropdown */} + +
+ + + Open user menu + + +
+ + + {userNavigation.map((item) => ( + + {({ active }) => ( + + {item.name} + + )} + + ))} + + +
+
+
+
+ {/* Mobile menu button */} + + + Open main menu + {open ? ( + +
+
+
+ + +
+ {navigation.map((item) => ( + + {item.name} + + ))} +
+
+
+
+ +
+
+
{user.name}
+
{user.email}
+
+ +
+
+ {userNavigation.map((item) => ( + + {item.name} + + ))} +
+
+
+ + )} +
+ +
+
+

Connections

+
+
+
+
+ + {}} /> +
+
+
+ + ) + } \ No newline at end of file diff --git a/web/src/App.js b/web/src/App.js index d78f3b4..00d1e67 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -233,7 +233,7 @@ function Login({show, onLogin, api}) { ) } -function App2() { +function App() { const [state, setState] = useState({pageid: 'login'}) const api = API(process.env.REACT_APP_API_URL, state.user) const setStateProperty = property => setState(prev => Object.assign({}, prev, property)) @@ -282,47 +282,5 @@ function App2() { ) } -function App() { - return ( -
-
- Your Company -

Sign in to your account

-
- -
-
-
- -
- -
-
- -
-
- - -
-
- -
-
- -
- -
-
- -

- Not a member? - Start a 14 day free trial -

-
-
- ) -} export default App