diff --git a/api/src/controllers/connections.js b/api/src/controllers/connections.js index b53f3087e..93a0fbcc7 100644 --- a/api/src/controllers/connections.js +++ b/api/src/controllers/connections.js @@ -14,7 +14,7 @@ module.exports = { const dir = await datadir(); this.datastore = nedb.create(path.join(dir, 'connections.jsonl')); }, - + list_meta: 'get', async list() { return this.datastore.find(); @@ -32,7 +32,15 @@ module.exports = { save_meta: 'post', async save(connection) { - const res = await this.datastore.insert(connection); - return res; + if (connection._id) { + return await this.datastore.update(_.pick(connection, '_id'), connection); + } else { + return await this.datastore.insert(connection); + } + }, + + delete_meta: 'post', + async delete(connection) { + return await this.datastore.remove(_.pick(connection, '_id')); }, }; diff --git a/api/src/engines/postgre/connect.js b/api/src/engines/postgres/connect.js similarity index 100% rename from api/src/engines/postgre/connect.js rename to api/src/engines/postgres/connect.js diff --git a/web/src/appobj/AppObjects.js b/web/src/appobj/AppObjects.js new file mode 100644 index 000000000..72a3f33a0 --- /dev/null +++ b/web/src/appobj/AppObjects.js @@ -0,0 +1,41 @@ +import React from 'react'; +import styled from 'styled-components'; +import { showMenu } from '../modals/DropDownMenu'; + +const AppObjectDiv = styled.div` + margin: 5px; +`; + +const IconWrap = styled.span` + margin-right: 10px; +`; + +export function AppObjectCore({ title, Icon, Menu, data, makeAppObj }) { + const handleContextMenu = event => { + if (!Menu) return; + + event.preventDefault(); + showMenu(event.pageX, event.pageY, ); + }; + + return ( + + + + + {title} + + ); +} + +export function AppObjectControl({ data, makeAppObj }) { + const appobj = makeAppObj(data); + return ; +} + +export function AppObjectList({ list, makeAppObj }) { + return (list || []).map(x => { + const appobj = makeAppObj(x); + return ; + }); +} diff --git a/web/src/appobj/connectionAppObject.js b/web/src/appobj/connectionAppObject.js new file mode 100644 index 000000000..3c2798645 --- /dev/null +++ b/web/src/appobj/connectionAppObject.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { MicrosoftIcon, SqliteIcon, PostgreSqlIcon, MySqlIcon, ServerIcon } from '../icons'; +import { DropDownMenuItem } from '../modals/DropDownMenu'; +import showModal from '../modals/showModal'; +import ConnectionModal from '../modals/ConnectionModal'; +import axios from '../utility/axios'; + +function getIcon(engine) { + switch (engine) { + case 'mssql': + return MicrosoftIcon; + case 'sqlite': + return SqliteIcon; + case 'postgres': + return PostgreSqlIcon; + case 'mysql': + return MySqlIcon; + } + return ServerIcon; +} + +function Menu({ data, makeAppObj }) { + const handleEdit = () => { + showModal(modalState => ); + }; + const handleDelete = () => { + axios.post('connections/delete', data); + }; + return ( + <> + Edit + Delete + + ); +} + +export default function connectionAppObject({ _id, server, displayName, engine }) { + const title = displayName || server; + const key = _id; + const Icon = getIcon(engine); + + return { title, key, Icon, Menu }; +} diff --git a/web/src/modals/ConnectionModal.js b/web/src/modals/ConnectionModal.js index de7d3687e..7e4e9c3ef 100644 --- a/web/src/modals/ConnectionModal.js +++ b/web/src/modals/ConnectionModal.js @@ -1,16 +1,16 @@ import React from 'react'; -import axios from 'axios'; +import axios from '../utility/axios'; import ModalBase from './ModalBase'; import { FormRow, FormButton, FormTextField, FormSelectField, FormSubmit } from '../utility/forms'; import { TextField } from '../utility/inputs'; import { Formik, Form } from 'formik'; // import FormikForm from '../utility/FormikForm'; -export default function ConnectionModal({ modalState }) { +export default function ConnectionModal({ modalState, connection }) { const [sqlConnectResult, setSqlConnectResult] = React.useState('Not connected'); const handleTest = async values => { - const resp = await axios.post('http://localhost:3000/connections/test', values); + const resp = await axios.post('connections/test', values); console.log('resp.data', resp.data); const { error, version } = resp.data; @@ -20,20 +20,19 @@ export default function ConnectionModal({ modalState }) { }; const handleSubmit = async values => { - const resp = await axios.post('http://localhost:3000/connections/save', values); - console.log('resp.data', resp.data); + const resp = await axios.post('connections/save', values); // modalState.close(); }; return ( -

Add connection

- +

{connection ? 'Edit connection' : 'Add connection'}

+
- + diff --git a/web/src/modals/DropDownMenu.js b/web/src/modals/DropDownMenu.js new file mode 100644 index 000000000..ad4b51706 --- /dev/null +++ b/web/src/modals/DropDownMenu.js @@ -0,0 +1,291 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import styled from 'styled-components'; +import { LoadingToken, sleep } from '../utility/common'; + +const ContextMenuStyled = styled.ul` + position: absolute; + list-style: none; + background-color: #fff; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.15); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + min-width: 160px; + z-index: 1050; +`; + +const KeyTextSpan = styled.span` + font-style: italic; + font-weight: bold; + text-align: right; + margin-left: 16px; +`; + +const StyledLink = styled.a` + padding: 3px 20px; + line-height: 1.42; + display: block; + white-space: nop-wrap; + color: #262626; + + &:hover { + background-color: #f5f5f5; + text-decoration: none; + color: #262626; + } +`; + +export function DropDownMenuItem({ children, keyText, onClick }) { + const handleMouseEnter = () => { + // if (this.context.parentMenu) this.context.parentMenu.closeSubmenu(); + }; + + return ( +
  • + + {children} + {keyText && {keyText}} + +
  • + ); +} + +// (DropDownMenuItem as any).contextTypes = { +// parentMenu: PropTypes.any +// }; + +// interface IDropDownMenuLinkProps { +// href: string; +// keyText?: string; +// } + +// export class DropDownMenuLink extends React.Component { +// render() { +// return
  • {this.props.children}{this.props.keyText && {this.props.keyText}}
  • ; +// } + +// handleMouseEnter() { +// if (this.context.parentMenu) this.context.parentMenu.closeSubmenu(); +// } +// } + +// (DropDownMenuLink as any).contextTypes = { +// parentMenu: PropTypes.any +// }; + +// // export function DropDownMenu(props: { children?: any }) { +// // return
    +// // +// //
      +// // {props.children} +// //
    +// //
    +// // } + +// export function DropDownMenuDivider(props: {}) { +// return
  • ; +// } + +// export class DropDownSubmenuItem extends React.Component { +// menuInstance: ContextMenu; +// domObject: Element; + +// render() { +// return
  • this.domObject = x}> null}>{this.props.title}
  • ; +// } + +// closeSubmenu() { +// if (this.menuInstance != null) { +// this.menuInstance.close(); +// this.menuInstance = null; +// } + +// if (this.context.parentMenu) this.context.parentMenu.submenu = null; +// } + +// closeOtherSubmenu() { +// if (this.context.parentMenu) this.context.parentMenu.closeSubmenu(); +// } + +// handleMouseEnter() { +// this.closeOtherSubmenu(); + +// let offset = $(this.domObject).offset(); +// let width = $(this.domObject).width(); + +// this.menuInstance = showMenuCore(offset.left + width, offset.top, this); +// if (this.context.parentMenu) this.context.parentMenu.submenu = this; +// } +// } + +// (DropDownSubmenuItem as any).contextTypes = { +// parentMenu: PropTypes.any +// }; + +// export class DropDownMenu extends React.Component { +// domButton: Element; + +// constructor(props) { +// super(props); +// this.state = { +// isExpanded: false, +// }; +// } + +// render() { +// let className = this.props.classOverride || ('btn btn-xs btn-default drop_down_menu_button ' + (this.props.className || '')); +// return +// } + +// @autobind +// menuButtonClick() { +// if (this.state.isExpanded) { +// hideMenu(); +// return; +// } +// let offset = $(this.domButton).offset(); +// let height = $(this.domButton).height(); +// this.setState({ isExpanded: true }) +// showMenu(offset.left, offset.top + height + 5, this, () => this.setState({ isExpanded: false })); +// } +// } + +export function ContextMenu({ left, top, children }) { + return {children}; +} + +// export class ContextMenu extends React.Component { +// domObject: Element; +// submenu: DropDownSubmenuItem; + +// render() { +// return
      this.domObject = x} onContextMenu={e => e.preventDefault()}> +// {this.props.children} +//
    ; +// } + +// componentDidMount() { +// fixPopupPlacement(this.domObject); +// } + +// getChildContext() { +// return { parentMenu: this }; +// } + +// closeSubmenu() { +// if (this.submenu) { +// this.submenu.closeSubmenu(); +// } +// } + +// close() { +// this.props.container.remove(); +// this.closeSubmenu(); +// } +// } + +// (ContextMenu as any).childContextTypes = { +// parentMenu: PropTypes.any +// }; + +let menuHandle = null; +let hideToken = null; + +function showMenuCore(left, top, contentHolder, closeCallback = null) { + let container = document.createElement('div'); + let handle = { + container, + closeCallback, + close() { + this.container.remove(); + }, + }; + document.body.appendChild(container); + ReactDOM.render( + + {contentHolder} + , + container + ); + return handle; +} + +export function showMenu(left, top, contentHolder, closeCallback = null) { + hideMenu(); + if (hideToken) hideToken.cancel(); + menuHandle = showMenuCore(left, top, contentHolder, closeCallback); + captureMouseDownEvents(); +} + +function captureMouseDownEvents() { + document.addEventListener('mousedown', mouseDownListener, true); +} + +function releaseMouseDownEvents() { + document.removeEventListener('mousedown', mouseDownListener, true); +} + +function captureMouseUpEvents() { + document.addEventListener('mouseup', mouseUpListener, true); +} + +function releaseMouseUpEvents() { + document.removeEventListener('mouseup', mouseUpListener, true); +} + +async function mouseDownListener(e) { + captureMouseUpEvents(); +} + +async function mouseUpListener(e) { + let token = new LoadingToken(); + hideToken = token; + await sleep(0); + if (token.isCanceled) return; + hideMenu(); +} + +function hideMenu() { + if (menuHandle == null) return; + menuHandle.close(); + if (menuHandle.closeCallback) menuHandle.closeCallback(); + menuHandle = null; + releaseMouseDownEvents(); + releaseMouseUpEvents(); +} + +function getElementOffset(element) { + var de = document.documentElement; + var box = element.getBoundingClientRect(); + var top = box.top + window.pageYOffset - de.clientTop; + var left = box.left + window.pageXOffset - de.clientLeft; + return { top: top, left: left }; +} + +export function fixPopupPlacement(element) { + const { width, height } = element.getBoundingClientRect(); + let offset = getElementOffset(element); + + let newLeft = null; + let newTop = null; + + if (offset.left + width > window.innerWidth) { + newLeft = offset.left - width; + } + if (offset.top + height > window.innerHeight) { + newTop = offset.top - height; + } + + if (newLeft != null) element.style.left = `${newLeft}px`; + if (newTop != null) element.style.top = `${newTop}px`; +} diff --git a/web/src/modals/showModal.js b/web/src/modals/showModal.js new file mode 100644 index 000000000..81d27a6b4 --- /dev/null +++ b/web/src/modals/showModal.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import useModalState from './useModalState'; + +function ShowModalComponent({ renderModal, container }) { + const modalState = useModalState(true); + if (!modalState.isOpen) { + container.remove(); + } + return renderModal(modalState); +} + +export default function showModal(renderModal) { + const container = document.createElement('div'); + document.body.appendChild(container); + ReactDOM.render(, container); +} diff --git a/web/src/modals/useModalState.js b/web/src/modals/useModalState.js index 6d3a6d963..07815fe31 100644 --- a/web/src/modals/useModalState.js +++ b/web/src/modals/useModalState.js @@ -1,7 +1,7 @@ import React from 'react'; -export default function useModalState() { - const [isOpen, setOpen] = React.useState(false); +export default function useModalState(isOpenDefault = false) { + const [isOpen, setOpen] = React.useState(isOpenDefault); const close = () => setOpen(false); const open = () => setOpen(true); return { isOpen, open, close }; diff --git a/web/src/utility/axios.js b/web/src/utility/axios.js new file mode 100644 index 000000000..bb2629326 --- /dev/null +++ b/web/src/utility/axios.js @@ -0,0 +1,5 @@ +import axios from 'axios'; + +export default axios.create({ + baseURL: 'http://localhost:3000', +}); diff --git a/web/src/utility/common.js b/web/src/utility/common.js new file mode 100644 index 000000000..a9e9d09f9 --- /dev/null +++ b/web/src/utility/common.js @@ -0,0 +1,13 @@ +export class LoadingToken { + constructor() { + this.isCanceled = false; + } + + cancel() { + this.isCanceled = true; + } +} + +export function sleep(milliseconds) { + return new Promise(resolve => window.setTimeout(() => resolve(null), milliseconds)); +} diff --git a/web/src/useFetch.js b/web/src/utility/useFetch.js similarity index 72% rename from web/src/useFetch.js rename to web/src/utility/useFetch.js index 5b4dc009d..b46ae9f1a 100644 --- a/web/src/useFetch.js +++ b/web/src/utility/useFetch.js @@ -1,11 +1,12 @@ import React from 'react'; -import axios from 'axios' +import axios from './axios'; export default function useFetch(url, defValue) { const [value, setValue] = React.useState(defValue); async function loadValue() { - setValue(await axios.get(url)); + const resp = await axios.get(url); + setValue(resp.data); } React.useEffect(() => { loadValue(); diff --git a/web/src/widgets/DatabaseWidget.js b/web/src/widgets/DatabaseWidget.js index eb6b4f816..b77a2ee53 100644 --- a/web/src/widgets/DatabaseWidget.js +++ b/web/src/widgets/DatabaseWidget.js @@ -1,13 +1,19 @@ import React from 'react'; import useModalState from '../modals/useModalState'; import ConnectionModal from '../modals/ConnectionModal'; +import useFetch from '../utility/useFetch'; +import { AppObjectList } from '../appobj/AppObjects'; +import connectionAppObject from '../appobj/connectionAppObject'; export default function DatabaseWidget() { const modalState = useModalState(); + const connections = useFetch('connections/list', []); + console.log(connections); return ( <> + ); }