mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-17 22:36:01 +00:00
context menu, editing connection
This commit is contained in:
@@ -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'));
|
||||
},
|
||||
};
|
||||
|
||||
41
web/src/appobj/AppObjects.js
Normal file
41
web/src/appobj/AppObjects.js
Normal file
@@ -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, <Menu data={data} makeAppObj={makeAppObj} />);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppObjectDiv onContextMenu={handleContextMenu}>
|
||||
<IconWrap>
|
||||
<Icon />
|
||||
</IconWrap>
|
||||
{title}
|
||||
</AppObjectDiv>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppObjectControl({ data, makeAppObj }) {
|
||||
const appobj = makeAppObj(data);
|
||||
return <AppObjectCore {...appobj} data={data} makeAppObj={makeAppObj} />;
|
||||
}
|
||||
|
||||
export function AppObjectList({ list, makeAppObj }) {
|
||||
return (list || []).map(x => {
|
||||
const appobj = makeAppObj(x);
|
||||
return <AppObjectCore key={appobj.key} {...appobj} data={x} makeAppObj={makeAppObj} />;
|
||||
});
|
||||
}
|
||||
43
web/src/appobj/connectionAppObject.js
Normal file
43
web/src/appobj/connectionAppObject.js
Normal file
@@ -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 => <ConnectionModal modalState={modalState} connection={data} />);
|
||||
};
|
||||
const handleDelete = () => {
|
||||
axios.post('connections/delete', data);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={handleEdit}>Edit</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function connectionAppObject({ _id, server, displayName, engine }) {
|
||||
const title = displayName || server;
|
||||
const key = _id;
|
||||
const Icon = getIcon(engine);
|
||||
|
||||
return { title, key, Icon, Menu };
|
||||
}
|
||||
@@ -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 (
|
||||
<ModalBase modalState={modalState}>
|
||||
<h2>Add connection</h2>
|
||||
<Formik onSubmit={handleSubmit} initialValues={{ server: 'localhost', engine: 'mssql' }}>
|
||||
<h2>{connection ? 'Edit connection' : 'Add connection'}</h2>
|
||||
<Formik onSubmit={handleSubmit} initialValues={connection || { server: 'localhost', engine: 'mssql' }}>
|
||||
<Form>
|
||||
<FormSelectField label="Database engine" name="engine">
|
||||
<option value="mssql">Microsoft SQL Server</option>
|
||||
<option value="mysql">MySQL</option>
|
||||
<option value="postgre">Postgre SQL</option>
|
||||
<option value="postgres">Postgre SQL</option>
|
||||
</FormSelectField>
|
||||
<FormTextField label="Server" name="server" />
|
||||
<FormTextField label="Port" name="port" />
|
||||
|
||||
291
web/src/modals/DropDownMenu.js
Normal file
291
web/src/modals/DropDownMenu.js
Normal file
@@ -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 (
|
||||
<li onMouseEnter={handleMouseEnter}>
|
||||
<StyledLink onClick={onClick}>
|
||||
{children}
|
||||
{keyText && <KeyTextSpan>{keyText}</KeyTextSpan>}
|
||||
</StyledLink>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
// (DropDownMenuItem as any).contextTypes = {
|
||||
// parentMenu: PropTypes.any
|
||||
// };
|
||||
|
||||
// interface IDropDownMenuLinkProps {
|
||||
// href: string;
|
||||
// keyText?: string;
|
||||
// }
|
||||
|
||||
// export class DropDownMenuLink extends React.Component<IDropDownMenuLinkProps> {
|
||||
// render() {
|
||||
// return <li onMouseEnter={this.handleMouseEnter.bind(this)}><Link forceSimpleLink href={this.props.href}>{this.props.children}{this.props.keyText && <span className='context_menu_key_text'>{this.props.keyText}</span>}</Link></li>;
|
||||
// }
|
||||
|
||||
// handleMouseEnter() {
|
||||
// if (this.context.parentMenu) this.context.parentMenu.closeSubmenu();
|
||||
// }
|
||||
// }
|
||||
|
||||
// (DropDownMenuLink as any).contextTypes = {
|
||||
// parentMenu: PropTypes.any
|
||||
// };
|
||||
|
||||
// // export function DropDownMenu(props: { children?: any }) {
|
||||
// // return <div className="btn-group">
|
||||
// // <button type="button" className="btn btn-default dropdown-toggle btn-xs" data-toggle="dropdown"
|
||||
// // aria-haspopup="true" aria-expanded="false" tabIndex={-1}>
|
||||
// // <span className="caret"></span>
|
||||
// // </button>
|
||||
// // <ul className="dropdown-menu">
|
||||
// // {props.children}
|
||||
// // </ul>
|
||||
// // </div>
|
||||
// // }
|
||||
|
||||
// export function DropDownMenuDivider(props: {}) {
|
||||
// return <li className="dropdown-divider"></li>;
|
||||
// }
|
||||
|
||||
// export class DropDownSubmenuItem extends React.Component<IDropDownSubmenuItemProps> {
|
||||
// menuInstance: ContextMenu;
|
||||
// domObject: Element;
|
||||
|
||||
// render() {
|
||||
// return <li onMouseEnter={this.handleMouseEnter.bind(this)} ref={x => this.domObject = x}><Link onClick={() => null}>{this.props.title} <IconSpan icon='fa-caret-right' /></Link></li>;
|
||||
// }
|
||||
|
||||
// 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<IDropDownMenuProps, IDropDownMenuState> {
|
||||
// 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 <button id={this.props.buttonElementId} type="button" className={className} tabIndex={-1} onClick={this.menuButtonClick} ref={x => this.domButton = x}>
|
||||
// { this.props.title }
|
||||
// { this.props.iconSpan || <span className="caret"></span>}
|
||||
// </button>
|
||||
// }
|
||||
|
||||
// @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 <ContextMenuStyled style={{ left: `${left}px`, top: `${top}px` }}>{children}</ContextMenuStyled>;
|
||||
}
|
||||
|
||||
// export class ContextMenu extends React.Component<IContextMenuProps> {
|
||||
// domObject: Element;
|
||||
// submenu: DropDownSubmenuItem;
|
||||
|
||||
// render() {
|
||||
// return <ul className='context_menu' style={{ left: `${this.props.left}px`, top: `${this.props.top}px` }} ref={x => this.domObject = x} onContextMenu={e => e.preventDefault()}>
|
||||
// {this.props.children}
|
||||
// </ul>;
|
||||
// }
|
||||
|
||||
// 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(
|
||||
<ContextMenu left={left} top={top} container={container} closeCallback={closeCallback}>
|
||||
{contentHolder}
|
||||
</ContextMenu>,
|
||||
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`;
|
||||
}
|
||||
17
web/src/modals/showModal.js
Normal file
17
web/src/modals/showModal.js
Normal file
@@ -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(<ShowModalComponent renderModal={renderModal} container={container} />, container);
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
5
web/src/utility/axios.js
Normal file
5
web/src/utility/axios.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export default axios.create({
|
||||
baseURL: 'http://localhost:3000',
|
||||
});
|
||||
13
web/src/utility/common.js
Normal file
13
web/src/utility/common.js
Normal file
@@ -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));
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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 (
|
||||
<>
|
||||
<ConnectionModal modalState={modalState} />
|
||||
<button onClick={modalState.open}>Add connection</button>
|
||||
<AppObjectList list={connections} makeAppObj={connectionAppObject} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user