introduced yarn workspace

This commit is contained in:
Jan Prochazka
2020-02-03 19:43:11 +01:00
parent 56e6777044
commit acf6a1ce74
151 changed files with 1515 additions and 8576 deletions

38
packages/web/src/App.css Normal file
View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

21
packages/web/src/App.js Normal file
View File

@@ -0,0 +1,21 @@
import React from 'react';
import './index.css';
import Screen from './Screen';
import { CurrentWidgetProvider, CurrentDatabaseProvider, OpenedTabsProvider } from './utility/globalState';
import { SocketProvider } from './utility/SocketProvider';
function App() {
return (
<CurrentWidgetProvider>
<CurrentDatabaseProvider>
<SocketProvider>
<OpenedTabsProvider>
<Screen />
</OpenedTabsProvider>
</SocketProvider>
</CurrentDatabaseProvider>
</CurrentWidgetProvider>
);
}
export default App;

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,79 @@
// @ts-nocheck
import React from 'react';
import theme from './theme';
import styled from 'styled-components';
import TabsPanel from './TabsPanel';
import TabContent from './TabContent';
import WidgetIconPanel from './widgets/WidgetIconPanel';
import { useCurrentWidget } from './utility/globalState';
import WidgetContainer from './widgets/WidgetContainer';
const BodyDiv = styled.div`
position: fixed;
top: ${theme.tabsPanel.height}px;
left: ${props => theme.widgetMenu.iconSize + props.leftPanelWidth}px;
bottom: ${theme.statusBar.height}px;
right: 0;
background-color: ${theme.mainArea.background};
`;
const IconBar = styled.div`
position: fixed;
top: 0;
left: 0;
bottom: ${theme.statusBar.height}px;
width: ${theme.widgetMenu.iconSize}px;
background-color: ${theme.widgetMenu.background};
`;
const LeftPanel = styled.div`
position: fixed;
top: 0;
left: ${theme.widgetMenu.iconSize}px;
bottom: ${theme.statusBar.height}px;
width: ${theme.leftPanel.width}px;
background-color: ${theme.leftPanel.background};
display: flex;
`;
const TabsPanelContainer = styled.div`
display: flex;
position: fixed;
top: 0;
left: ${props => theme.widgetMenu.iconSize + props.leftPanelWidth}px;
height: ${theme.tabsPanel.height}px;
right: 0;
background-color: ${theme.tabsPanel.background};
`;
const StausBar = styled.div`
position: fixed;
height: ${theme.statusBar.height}px;
left: 0;
right: 0;
bottom: 0;
background-color: ${theme.statusBar.background};
`;
export default function Screen() {
const currentWidget = useCurrentWidget();
const leftPanelWidth = currentWidget ? theme.leftPanel.width : 0;
return (
<>
<IconBar>
<WidgetIconPanel />
</IconBar>
{!!currentWidget && (
<LeftPanel>
<WidgetContainer />
</LeftPanel>
)}
<TabsPanelContainer leftPanelWidth={leftPanelWidth}>
<TabsPanel></TabsPanel>
</TabsPanelContainer>
<BodyDiv leftPanelWidth={leftPanelWidth}><TabContent/></BodyDiv>
<StausBar></StausBar>
</>
);
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
import styled from 'styled-components';
import theme from './theme';
import tabs from './tabs';
import { useOpenedTabs } from './utility/globalState';
export default function TabContent() {
const files = useOpenedTabs();
const selectedTab = files.find(x => x.selected);
if (!selectedTab) return null;
const TabComponent = tabs[selectedTab.tabComponent];
if (TabComponent) return <TabComponent {...selectedTab.props} />;
return null;
}

View File

@@ -0,0 +1,65 @@
import React from 'react';
import styled from 'styled-components';
import theme from './theme';
import { useOpenedTabs, useSetOpenedTabs } from './utility/globalState';
import { getIconImage } from './icons';
// const files = [
// { name: 'app.js' },
// { name: 'BranchCategory', type: 'table', selected: true },
// { name: 'ApplicationList' },
// ];
const FileTabItem = styled.div`
border-right: 1px solid white;
padding-left: 15px;
padding-right: 15px;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
color: ${theme.tabsPanel.hoverFont};
}
background-color: ${props =>
// @ts-ignore
props.selected ? theme.mainArea.background : 'inherit'};
`;
const FileNameWrapper = styled.span`
margin-left: 5px;
`;
export default function TabsPanel() {
const tabs = useOpenedTabs();
const setOpenedTabs = useSetOpenedTabs();
const handleTabClick = tabid => {
setOpenedTabs(files =>
files.map(x => ({
...x,
selected: x.tabid == tabid,
}))
);
};
const handleMouseUp = (e, tabid) => {
if (e.button == 1) {
setOpenedTabs(files => files.filter(x => x.tabid != tabid));
}
};
return (
<>
{tabs.map(tab => (
<FileTabItem
{...tab}
key={tab.tabid}
onClick={() => handleTabClick(tab.tabid)}
onMouseUp={e => handleMouseUp(e, tab.tabid)}
>
{getIconImage(tab.icon)}
<FileNameWrapper>{tab.title}</FileNameWrapper>
</FileTabItem>
))}
</>
);
}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import styled from 'styled-components';
import { showMenu } from '../modals/DropDownMenu';
import { AppObjectCore } from './AppObjects';
import { useSetOpenedTabs } from '../utility/globalState';
export function AppObjectList({ list, makeAppObj, SubItems = undefined, onObjectClick = undefined }) {
const setOpenedTabs = useSetOpenedTabs();
return (list || []).map(x => {
const appobj = makeAppObj(x, { setOpenedTabs });
if (onObjectClick) appobj.onClick = onObjectClick;
let res = <AppObjectCore key={appobj.key} data={x} makeAppObj={makeAppObj} {...appobj} />;
if (SubItems) {
res = (
<>
{res}
<SubItems data={x} />
</>
);
}
return res;
});
}

View File

@@ -0,0 +1,52 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { showMenu } from '../modals/DropDownMenu';
import { useSetOpenedTabs } from '../utility/globalState';
const AppObjectDiv = styled.div`
padding: 5px;
&:hover {
background-color: lightblue;
}
cursor: pointer;
white-space: nowrap;
`;
const AppObjectSpan = styled.span`
white-space: nowrap;
font-weight: ${props => (props.isBold ? 'bold' : 'normal')};
`;
const IconWrap = styled.span`
margin-right: 10px;
`;
export function AppObjectCore({ title, Icon, Menu, data, makeAppObj, onClick, isBold, component = 'div' }) {
const setOpenedTabs = useSetOpenedTabs();
const handleContextMenu = event => {
if (!Menu) return;
event.preventDefault();
showMenu(event.pageX, event.pageY, <Menu data={data} makeAppObj={makeAppObj} setOpenedTabs={setOpenedTabs} />);
};
const Component = component == 'div' ? AppObjectDiv : AppObjectSpan;
return (
<Component onContextMenu={handleContextMenu} onClick={onClick ? () => onClick(data) : undefined} isBold={isBold}>
<IconWrap>
<Icon />
</IconWrap>
{title}
</Component>
);
}
export function AppObjectControl({ data, makeAppObj, component = 'div' }) {
const setOpenedTabs = useSetOpenedTabs();
const appobj = makeAppObj(data, { setOpenedTabs });
return <AppObjectCore {...appobj} data={data} makeAppObj={makeAppObj} component={component} />;
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { ColumnIcon, SequenceIcon } from '../icons';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import showModal from '../modals/showModal';
import ConnectionModal from '../modals/ConnectionModal';
import axios from '../utility/axios';
import { openNewTab } from '../utility/common';
import { useSetOpenedTabs } from '../utility/globalState';
/** @param columnProps {import('dbgate').ColumnInfo} */
function getColumnIcon(columnProps) {
if (columnProps.autoIncrement) return SequenceIcon;
return ColumnIcon;
}
/** @param columnProps {import('dbgate').ColumnInfo} */
export default function columnAppObject(columnProps, { setOpenedTabs }) {
const title = columnProps.columnName;
const key = title;
const Icon = getColumnIcon(columnProps);
const isBold = columnProps.notNull;
return { title, key, Icon, isBold };
}

View 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 };
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { PrimaryKeyIcon, ForeignKeyIcon } from '../icons';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import showModal from '../modals/showModal';
import ConnectionModal from '../modals/ConnectionModal';
import axios from '../utility/axios';
import { openNewTab } from '../utility/common';
import { useSetOpenedTabs } from '../utility/globalState';
/** @param props {import('dbgate').ConstraintInfo} */
function getConstraintIcon(props) {
if (props.constraintType == 'primaryKey') return PrimaryKeyIcon;
if (props.constraintType == 'foreignKey') return ForeignKeyIcon;
return null;
}
/** @param props {import('dbgate').ConstraintInfo} */
export default function constraintAppObject(props, { setOpenedTabs }) {
const title = props.constraintName;
const key = title;
const Icon = getConstraintIcon(props);
return { title, key, Icon };
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { DatabaseIcon } from '../icons';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import showModal from '../modals/showModal';
import ConnectionModal from '../modals/ConnectionModal';
import axios from '../utility/axios';
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 databaseAppObject({ name }) {
const title = name;
const key = name;
const Icon = DatabaseIcon;
return { title, key, Icon, Menu };
}

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { TableIcon } from '../icons';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import showModal from '../modals/showModal';
import ConnectionModal from '../modals/ConnectionModal';
import axios from '../utility/axios';
import { openNewTab } from '../utility/common';
import { useSetOpenedTabs } from '../utility/globalState';
function openTableDetail(setOpenedTabs, tabComponent, { schemaName, pureName, conid, database }) {
openNewTab(setOpenedTabs, {
title: pureName,
icon: 'table2.svg',
tabComponent,
props: {
schemaName,
pureName,
conid,
database,
},
});
}
function Menu({ data, makeAppObj, setOpenedTabs }) {
const handleOpenData = () => {
openTableDetail(setOpenedTabs, 'TableDataTab', data);
};
const handleOpenStructure = () => {
openTableDetail(setOpenedTabs, 'TableStructureTab', data);
};
const handleOpenCreateScript = () => {
openTableDetail(setOpenedTabs, 'TableCreateScriptTab', data);
};
return (
<>
<DropDownMenuItem onClick={handleOpenData}>Open data</DropDownMenuItem>
<DropDownMenuItem onClick={handleOpenStructure}>Open structure</DropDownMenuItem>
<DropDownMenuItem onClick={handleOpenCreateScript}>Create SQL</DropDownMenuItem>
</>
);
}
export default function tableAppObject({ conid, database, pureName, schemaName }, { setOpenedTabs }) {
const title = schemaName ? `${schemaName}.${pureName}` : pureName;
const key = title;
const Icon = TableIcon;
const onClick = ({ schemaName, pureName }) => {
openTableDetail(setOpenedTabs, 'TableDataTab', {
schemaName,
pureName,
conid,
database,
});
};
return { title, key, Icon, Menu, onClick };
}

View File

@@ -0,0 +1,252 @@
import React from 'react';
import useFetch from '../utility/useFetch';
import styled from 'styled-components';
import theme from '../theme';
import { HorizontalScrollBar, VerticalScrollBar } from './ScrollBars';
import useDimensions from '../utility/useDimensions';
import { SeriesSizes } from './SeriesSizes';
const GridContainer = styled.div`
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
`;
const Table = styled.table`
position: absolute;
left: 0;
top: 0;
bottom: 20px;
right: 20px;
overflow: scroll;
border-collapse: collapse;
`;
const TableHead = styled.thead`
// display: block;
// width: 300px;
`;
const TableBody = styled.tbody`
// display: block;
// overflow: auto;
// height: 100px;
`;
const TableHeaderRow = styled.tr`
// height: 35px;
`;
const TableBodyRow = styled.tr`
// height: 35px;
background-color: #ffffff;
&:nth-child(6n + 4) {
background-color: #ebebeb;
}
&:nth-child(6n + 7) {
background-color: #ebf5ff;
}
`;
const TableHeaderCell = styled.td`
font-weight: bold;
border: 1px solid #c0c0c0;
// border-collapse: collapse;
text-align: left;
padding: 2px;
background-color: #f6f7f9;
overflow: hidden;
`;
const TableBodyCell = styled.td`
font-weight: normal;
border: 1px solid #c0c0c0;
// border-collapse: collapse;
padding: 2px;
white-space: nowrap;
overflow: hidden;
`;
export default function DataGrid({ params }) {
const data = useFetch({
url: 'tables/table-data',
params,
});
const { rows, columns } = data || {};
const [firstVisibleRowScrollIndex, setFirstVisibleRowScrollIndex] = React.useState(0);
const [firstVisibleColumnScrollIndex, setFirstVisibleColumnScrollIndex] = React.useState(0);
const [headerRowRef, { height: rowHeight }] = useDimensions();
const [tableBodyRef] = useDimensions();
const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions();
const columnSizes = React.useMemo(() => countColumnSizes(), [data, containerWidth]);
console.log('containerWidth', containerWidth);
const gridScrollAreaHeight = containerHeight - 2 * rowHeight;
const gridScrollAreaWidth = containerWidth - columnSizes.frozenSize;
const visibleRowCountUpperBound = Math.ceil(gridScrollAreaHeight / Math.floor(rowHeight));
const visibleRowCountLowerBound = Math.floor(gridScrollAreaHeight / Math.ceil(rowHeight));
// const visibleRowCountUpperBound = 20;
// const visibleRowCountLowerBound = 20;
if (!columns || !rows) return null;
const rowCountNewIncluded = rows.length;
const handleRowScroll = value => {
setFirstVisibleRowScrollIndex(value);
};
const handleColumnScroll = value => {
setFirstVisibleColumnScrollIndex(value);
};
function countColumnSizes() {
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
//return this.context.measureText(txt).width;
const columnSizes = new SeriesSizes();
if (!rows || !columns) return columnSizes;
console.log('countColumnSizes', rows.length, containerWidth);
columnSizes.maxSize = (containerWidth * 2) / 3;
columnSizes.count = columns.length;
// columnSizes.setExtraordinaryIndexes(this.getHiddenColumnIndexes(), this.getFrozenColumnIndexes());
columnSizes.setExtraordinaryIndexes([], []);
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
//this.columnSizes.PutSizeOverride(col, this.columns[col].Name.length * 8);
let column = columns[colIndex];
// if (column.columnClientObject != null && column.columnClientObject.notNull) context.font = "bold 14px Helvetica";
// else context.font = "14px Helvetica";
context.font = "bold 14px Helvetica";
let text = column.name;
let headerWidth = context.measureText(text).width + 32;
// if (column.columnClientObject != null && column.columnClientObject.icon != null) headerWidth += 16;
// if (this.getFilterOnColumn(column.uniquePath)) headerWidth += 16;
// if (this.getSortOrder(column.uniquePath)) headerWidth += 16;
columnSizes.putSizeOverride(colIndex, headerWidth);
}
// let headerWidth = this.rowHeaderWidthDefault;
// if (this.rowCount) headerWidth = context.measureText(this.rowCount.toString()).width + 8;
// this.rowHeaderWidth = this.rowHeaderWidthDefault;
// if (headerWidth > this.rowHeaderWidth) this.rowHeaderWidth = headerWidth;
context.font = '14px Helvetica';
for (let row of data.rows) {
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
let colName = columns[colIndex].name;
let text = row[colName];
let width = context.measureText(text).width + 8;
// console.log('colName', colName, text, width);
columnSizes.putSizeOverride(colIndex, width);
// let colName = this.columns[colIndex].uniquePath;
// let text: string = row[colName].gridText;
// let width = context.measureText(text).width + 8;
// if (row[colName].dataPrefix) width += context.measureText(row[colName].dataPrefix).width + 3;
// this.columnSizes.putSizeOverride(colIndex, width);
}
}
// for (let modelIndex = 0; modelIndex < this.columns.length; modelIndex++) {
// let width = getHashValue(this.widthHashPrefix + this.columns[modelIndex].uniquePath);
// if (width) this.columnSizes.putSizeOverride(modelIndex, _.toNumber(width), true);
// }
columnSizes.buildIndex();
return columnSizes;
}
// console.log('visibleRowCountUpperBound', visibleRowCountUpperBound);
// console.log('gridScrollAreaHeight', gridScrollAreaHeight);
// console.log('containerHeight', containerHeight);
const visibleColumnCount = columnSizes.getVisibleScrollCount(firstVisibleColumnScrollIndex, gridScrollAreaWidth);
console.log('visibleColumnCount', visibleColumnCount);
const visibleRealColumnIndexes = [];
const modelIndexes = {};
const realColumns = [];
// frozen columns
for (let colIndex = 0; colIndex < columnSizes.frozenCount; colIndex++) {
visibleRealColumnIndexes.push(colIndex);
}
// scroll columns
for (
let colIndex = firstVisibleColumnScrollIndex;
colIndex < firstVisibleColumnScrollIndex + visibleColumnCount;
colIndex++
) {
visibleRealColumnIndexes.push(colIndex + columnSizes.frozenCount);
}
// real columns
for (let colIndex of visibleRealColumnIndexes) {
let modelColumnIndex = columnSizes.realToModel(colIndex);
modelIndexes[colIndex] = modelColumnIndex;
let col = columns[modelColumnIndex];
if (!col) continue;
const widthNumber = columnSizes.getSizeByRealIndex(colIndex);
realColumns.push({
...col,
widthPx: `${widthNumber}px`,
});
}
console.log('visibleRealColumnIndexes', visibleRealColumnIndexes);
return (
<GridContainer ref={containerRef}>
<Table>
<TableHead>
<TableHeaderRow ref={headerRowRef}>
{realColumns.map(col => (
<TableHeaderCell
key={col.name}
style={{ width: col.widthPx, minWidth: col.widthPx, maxWidth: col.widthPx }}
>
{col.name}
</TableHeaderCell>
))}
</TableHeaderRow>
</TableHead>
<TableBody ref={tableBodyRef}>
{rows
.slice(firstVisibleRowScrollIndex, firstVisibleRowScrollIndex + visibleRowCountUpperBound)
.map((row, index) => (
<TableBodyRow key={firstVisibleRowScrollIndex + index}>
{realColumns.map(col => (
<TableBodyCell
key={col.name}
style={{ width: col.widthPx, minWidth: col.widthPx, maxWidth: col.widthPx }}
>
{row[col.name]}
</TableBodyCell>
))}
</TableBodyRow>
))}
</TableBody>
</Table>
<HorizontalScrollBar
minimum={0}
maximum={columns.length - 1}
viewportRatio={gridScrollAreaWidth / columnSizes.getVisibleScrollSizeSum()}
onScroll={handleColumnScroll}
/>
<VerticalScrollBar
minimum={0}
maximum={rowCountNewIncluded - visibleRowCountUpperBound + 2}
onScroll={handleRowScroll}
viewportRatio={visibleRowCountUpperBound / rowCountNewIncluded}
/>
</GridContainer>
);
}

View File

@@ -0,0 +1,222 @@
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import useDimensions from '../utility/useDimensions';
const StyledHorizontalScrollBar = styled.div`
overflow-x: scroll;
height: 16px;
position: absolute;
bottom: 0;
//left: 100px;
// right: 20px;
right: 0;
left: 0;
`;
const StyledHorizontalScrollContent = styled.div``;
const StyledVerticalScrollBar = styled.div`
overflow-y: scroll;
width: 20px;
position: absolute;
right: 0px;
width: 20px;
bottom: 16px;
// bottom: 0;
top: 0;
`;
const StyledVerticalScrollContent = styled.div``;
export function HorizontalScrollBar({
onScroll = undefined,
valueToSet = undefined,
valueToSetDate = undefined,
minimum,
maximum,
viewportRatio = 0.5,
}) {
const [ref, { width }, node] = useDimensions();
const contentSize = Math.round(width / viewportRatio);
React.useEffect(() => {
const position01 = (valueToSet - minimum) / (maximum - minimum + 1);
const position = position01 * (contentSize - width);
if (node) node.scrollLeft = Math.floor(position);
}, [valueToSetDate]);
const handleScroll = () => {
const position = node.scrollLeft;
const ratio = position / (contentSize - width);
if (ratio < 0) return 0;
let res = ratio * (maximum - minimum + 1) + minimum;
onScroll(Math.floor(res + 0.3));
};
return (
<StyledHorizontalScrollBar ref={ref} onScroll={handleScroll}>
<StyledHorizontalScrollContent style={{ width: `${contentSize}px` }}>&nbsp;</StyledHorizontalScrollContent>
</StyledHorizontalScrollBar>
);
}
export function VerticalScrollBar({
onScroll,
valueToSet = undefined,
valueToSetDate = undefined,
minimum,
maximum,
viewportRatio = 0.5,
}) {
const [ref, { height }, node] = useDimensions();
const contentSize = Math.round(height / viewportRatio);
React.useEffect(() => {
const position01 = (valueToSet - minimum) / (maximum - minimum + 1);
const position = position01 * (contentSize - height);
if (node) node.scrollTop = Math.floor(position);
}, [valueToSetDate]);
const handleScroll = () => {
const position = node.scrollTop;
const ratio = position / (contentSize - height);
if (ratio < 0) return 0;
let res = ratio * (maximum - minimum + 1) + minimum;
onScroll(Math.floor(res + 0.3));
};
return (
<StyledVerticalScrollBar ref={ref} onScroll={handleScroll}>
<StyledVerticalScrollContent style={{ height: `${contentSize}px` }}>&nbsp;</StyledVerticalScrollContent>
</StyledVerticalScrollBar>
);
}
// export interface IScrollBarProps {
// viewportRatio: number;
// minimum: number;
// maximum: number;
// containerStyle: any;
// onScroll?: any;
// }
// export abstract class ScrollBarBase extends React.Component<IScrollBarProps, {}> {
// domScrollContainer: Element;
// domScrollContent: Element;
// contentSize: number;
// containerResizedBind: any;
// constructor(props) {
// super(props);
// this.containerResizedBind = this.containerResized.bind(this);
// }
// componentDidMount() {
// $(this.domScrollContainer).scroll(this.onScroll.bind(this));
// createResizeDetector(this.domScrollContainer, this.containerResized.bind(this));
// window.addEventListener('resize', this.containerResizedBind);
// this.updateContentSize();
// }
// componentWillUnmount() {
// deleteResizeDetector(this.domScrollContainer);
// window.removeEventListener('resize', this.containerResizedBind);
// }
// onScroll() {
// if (this.props.onScroll) {
// this.props.onScroll(this.value);
// }
// }
// get value(): number {
// let position = this.getScrollPosition();
// let ratio = position / (this.contentSize - this.getContainerSize());
// if (ratio < 0) return 0;
// let res = ratio * (this.props.maximum - this.props.minimum + 1) + this.props.minimum;
// return Math.floor(res + 0.3);
// }
// set value(value: number) {
// let position01 = (value - this.props.minimum) / (this.props.maximum - this.props.minimum + 1);
// let position = position01 * (this.contentSize - this.getContainerSize());
// this.setScrollPosition(Math.floor(position));
// }
// containerResized() {
// this.setContentSizeField();
// this.updateContentSize();
// }
// setContentSizeField() {
// let lastContentSize = this.contentSize;
// this.contentSize = Math.round(this.getContainerSize() / this.props.viewportRatio);
// if (_.isNaN(this.contentSize)) this.contentSize = 0;
// if (this.contentSize > 1000000 && detectBrowser() == BrowserType.Firefox) this.contentSize = 1000000;
// if (lastContentSize != this.contentSize) {
// this.updateContentSize();
// }
// }
// abstract getContainerSize(): number;
// abstract updateContentSize();
// abstract getScrollPosition(): number;
// abstract setScrollPosition(value: number);
// }
// export class HorizontalScrollBar extends ScrollBarBase {
// render() {
// this.setContentSizeField();
// return <div className='ReactGridHorizontalScrollBar' ref={x => this.domScrollContainer = x} style={this.props.containerStyle}>
// <div className='ReactGridHorizontalScrollContent' ref={x => this.domScrollContent = x} style={{ width: this.contentSize }}>
// &nbsp;
// </div>
// </div>;
// }
// getContainerSize(): number {
// return $(this.domScrollContainer).width();
// }
// updateContentSize() {
// $(this.domScrollContent).width(this.contentSize);
// }
// getScrollPosition() {
// return $(this.domScrollContainer).scrollLeft();
// }
// setScrollPosition(value: number) {
// $(this.domScrollContainer).scrollLeft(value);
// }
// }
// export class VerticalScrollBar extends ScrollBarBase {
// render() {
// this.setContentSizeField();
// return <div className='ReactGridVerticalScrollBar' ref={x => this.domScrollContainer = x} style={this.props.containerStyle}>
// <div className='ReactGridVerticalScrollContent' ref={x => this.domScrollContent = x} style={{ height: this.contentSize }}>
// &nbsp;
// </div>
// </div>;
// }
// getContainerSize(): number {
// return $(this.domScrollContainer).height();
// }
// updateContentSize() {
// $(this.domScrollContent).height(this.contentSize);
// }
// getScrollPosition() {
// return $(this.domScrollContainer).scrollTop();
// }
// setScrollPosition(value: number) {
// $(this.domScrollContainer).scrollTop(value);
// }
// }

View File

@@ -0,0 +1,340 @@
import _ from 'lodash';
export class SeriesSizeItem {
constructor() {
this.scrollIndex = -1;
this.frozenIndex = -1;
this.modelIndex = 0;
this.size = 0;
this.position = 0;
}
// modelIndex;
// size;
// position;
get endPosition() {
return this.position + this.size;
}
}
export class SeriesSizes {
constructor() {
this.scrollItems = [];
this.sizeOverridesByModelIndex = {};
this.positions = [];
this.scrollIndexes = [];
this.frozenItems = [];
this.hiddenAndFrozenModelIndexes = null;
this.frozenModelIndexes = null;
this.count = 0;
this.maxSize = 1000;
this.defaultSize = 50;
}
// private sizeOverridesByModelIndex: { [id] } = {};
// count;
// defaultSize;
// maxSize;
// private hiddenAndFrozenModelIndexes[] = [];
// private frozenModelIndexes[] = [];
// private hiddenModelIndexes[] = [];
// private scrollItems: SeriesSizeItem[] = [];
// private positions[] = [];
// private scrollIndexes[] = [];
// private frozenItems: SeriesSizeItem[] = [];
get scrollCount() {
return this.count - (this.hiddenAndFrozenModelIndexes != null ? this.hiddenAndFrozenModelIndexes.length : 0);
}
get frozenCount() {
return this.frozenModelIndexes != null ? this.frozenModelIndexes.length : 0;
}
get frozenSize() {
return _.sumBy(this.frozenItems, x => x.size);
}
get realCount() {
return this.frozenCount + this.scrollCount;
}
putSizeOverride(modelIndex, size, sizeByUser = false) {
if (this.maxSize && size > this.maxSize && !sizeByUser) {
size = this.maxSize;
}
let currentSize = this.sizeOverridesByModelIndex[modelIndex];
if (sizeByUser || !currentSize || size > currentSize) {
this.sizeOverridesByModelIndex[modelIndex] = size;
}
// if (!_.has(this.sizeOverridesByModelIndex, modelIndex))
// this.sizeOverridesByModelIndex[modelIndex] = size;
// if (size > this.sizeOverridesByModelIndex[modelIndex])
// this.sizeOverridesByModelIndex[modelIndex] = size;
}
buildIndex() {
this.scrollItems = [];
this.scrollIndexes = _.filter(
_.map(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelToReal(x) - this.frozenCount),
x => x >= 0
);
this.scrollIndexes.sort();
let lastScrollIndex = -1;
let lastEndPosition = 0;
this.scrollIndexes.forEach(scrollIndex => {
let modelIndex = this.realToModel(scrollIndex + this.frozenCount);
let size = this.sizeOverridesByModelIndex[modelIndex];
let item = new SeriesSizeItem();
item.scrollIndex = scrollIndex;
item.modelIndex = modelIndex;
item.size = size;
item.position = lastEndPosition + (scrollIndex - lastScrollIndex - 1) * this.defaultSize;
this.scrollItems.push(item);
lastScrollIndex = scrollIndex;
lastEndPosition = item.endPosition;
});
this.positions = _.map(this.scrollItems, x => x.position);
this.frozenItems = [];
let lastpos = 0;
for (let i = 0; i < this.frozenCount; i++) {
let modelIndex = this.frozenModelIndexes[i];
let size = this.getSizeByModelIndex(modelIndex);
let item = new SeriesSizeItem();
item.frozenIndex = i;
item.modelIndex = modelIndex;
item.size = size;
item.position = lastpos;
this.frozenItems.push(item);
lastpos += size;
}
}
getScrollIndexOnPosition(position) {
let itemOrder = _.sortedIndex(this.positions, position);
if (this.positions[itemOrder] == position) return itemOrder;
if (itemOrder == 0) return Math.floor(position / this.defaultSize);
if (position <= this.scrollItems[itemOrder - 1].endPosition) return this.scrollItems[itemOrder - 1].scrollIndex;
return (
Math.floor((position - this.scrollItems[itemOrder - 1].position) / this.defaultSize) +
this.scrollItems[itemOrder - 1].scrollIndex
);
}
getFrozenIndexOnPosition(position) {
this.frozenItems.forEach(function(item) {
if (position >= item.position && position <= item.endPosition) return item.frozenIndex;
});
return -1;
}
// getSizeSum(startScrollIndex, endScrollIndex) {
// let order1 = _.sortedIndexOf(this.scrollIndexes, startScrollIndex);
// let order2 = _.sortedIndexOf(this.scrollIndexes, endScrollIndex);
// let count = endScrollIndex - startScrollIndex;
// if (order1 < 0)
// order1 = ~order1;
// if (order2 < 0)
// order2 = ~order2;
// let result = 0;
// for (let i = order1; i <= order2; i++) {
// if (i < 0)
// continue;
// if (i >= this.scrollItems.length)
// continue;
// let item = this.scrollItems[i];
// if (item.scrollIndex < startScrollIndex)
// continue;
// if (item.scrollIndex >= endScrollIndex)
// continue;
// result += item.size;
// count--;
// }
// result += count * this.defaultSize;
// return result;
// }
getSizeByModelIndex(modelIndex) {
if (_.has(this.sizeOverridesByModelIndex, modelIndex)) return this.sizeOverridesByModelIndex[modelIndex];
return this.defaultSize;
}
getSizeByScrollIndex(scrollIndex) {
return this.getSizeByRealIndex(scrollIndex + this.frozenCount);
}
getSizeByRealIndex(realIndex) {
let modelIndex = this.realToModel(realIndex);
return this.getSizeByModelIndex(modelIndex);
}
removeSizeOverride(realIndex) {
let modelIndex = this.realToModel(realIndex);
delete this.sizeOverridesByModelIndex[modelIndex];
}
getScroll(sourceScrollIndex, targetScrollIndex) {
if (sourceScrollIndex < targetScrollIndex) {
return -_.sum(
_.map(_.range(sourceScrollIndex, targetScrollIndex - sourceScrollIndex), x => this.getSizeByScrollIndex(x))
);
} else {
return _.sum(
_.map(_.range(targetScrollIndex, sourceScrollIndex - targetScrollIndex), x => this.getSizeByScrollIndex(x))
);
}
}
modelIndexIsInScrollArea(modelIndex) {
let realIndex = this.modelToReal(modelIndex);
return realIndex >= this.frozenCount;
}
getTotalScrollSizeSum() {
let scrollSizeOverrides = _.map(
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelIndexIsInScrollArea(x)),
x => this.sizeOverridesByModelIndex[x]
);
return _.sum(scrollSizeOverrides) + (this.count - scrollSizeOverrides.length) * this.defaultSize;
}
getVisibleScrollSizeSum() {
let scrollSizeOverrides = _.map(
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => !_.includes(this.hiddenAndFrozenModelIndexes, x)),
x => this.sizeOverridesByModelIndex[x]
);
return (
_.sum(scrollSizeOverrides) +
(this.count - this.hiddenModelIndexes.length - scrollSizeOverrides.length) * this.defaultSize
);
}
intKeys(value) {
return _.keys(value).map(x => _.parseInt(x));
}
getPositionByRealIndex(realIndex) {
if (realIndex < 0) return 0;
if (realIndex < this.frozenCount) return this.frozenItems[realIndex].position;
return this.getPositionByScrollIndex(realIndex - this.frozenCount);
}
getPositionByScrollIndex(scrollIndex) {
let order = _.sortedIndex(this.scrollIndexes, scrollIndex);
if (this.scrollIndexes[order] == scrollIndex) return this.scrollItems[order].position;
order--;
if (order < 0) return scrollIndex * this.defaultSize;
return (
this.scrollItems[order].endPosition + (scrollIndex - this.scrollItems[order].scrollIndex - 1) * this.defaultSize
);
}
getVisibleScrollCount(firstVisibleIndex, viewportSize) {
let res = 0;
let index = firstVisibleIndex;
let count = 0;
while (res < viewportSize && index <= this.scrollCount) {
console.log('this.getSizeByScrollIndex(index)', this.getSizeByScrollIndex(index));
res += this.getSizeByScrollIndex(index);
index++;
count++;
}
console.log('getVisibleScrollCount', firstVisibleIndex, viewportSize, count);
return count;
}
getVisibleScrollCountReversed(lastVisibleIndex, viewportSize) {
let res = 0;
let index = lastVisibleIndex;
let count = 0;
while (res < viewportSize && index >= 0) {
res += this.getSizeByScrollIndex(index);
index--;
count++;
}
return count;
}
invalidateAfterScroll(oldFirstVisible, newFirstVisible, invalidate, viewportSize) {
if (newFirstVisible > oldFirstVisible) {
let oldVisibleCount = this.getVisibleScrollCount(oldFirstVisible, viewportSize);
let newVisibleCount = this.getVisibleScrollCount(newFirstVisible, viewportSize);
for (let i = oldFirstVisible + oldVisibleCount - 1; i <= newFirstVisible + newVisibleCount; i++) {
invalidate(i + this.frozenCount);
}
} else {
for (let i = newFirstVisible; i <= oldFirstVisible; i++) {
invalidate(i + this.frozenCount);
}
}
}
isWholeInView(firstVisibleIndex, index, viewportSize) {
let res = 0;
let testedIndex = firstVisibleIndex;
while (res < viewportSize && testedIndex < this.count) {
res += this.getSizeByScrollIndex(testedIndex);
if (testedIndex == index) return res <= viewportSize;
testedIndex++;
}
return false;
}
scrollInView(firstVisibleIndex, scrollIndex, viewportSize) {
if (this.isWholeInView(firstVisibleIndex, scrollIndex, viewportSize)) {
return firstVisibleIndex;
}
if (scrollIndex < firstVisibleIndex) {
return scrollIndex;
}
let res = 0;
let testedIndex = scrollIndex;
while (res < viewportSize && testedIndex >= 0) {
let size = this.getSizeByScrollIndex(testedIndex);
if (res + size > viewportSize) return testedIndex + 1;
testedIndex--;
res += size;
}
if (res >= viewportSize && testedIndex < scrollIndex) return testedIndex + 1;
return firstVisibleIndex;
}
resize(realIndex, newSize) {
if (realIndex < 0) return;
let modelIndex = this.realToModel(realIndex);
if (modelIndex < 0) return;
this.sizeOverridesByModelIndex[modelIndex] = newSize;
this.buildIndex();
}
setExtraordinaryIndexes(hidden, frozen) {
//this._hiddenAndFrozenModelIndexes = _.clone(hidden);
hidden = hidden.filter(x => x >= 0);
frozen = frozen.filter(x => x >= 0);
hidden.sort((a, b) => a - b);
frozen.sort((a, b) => a - b);
this.frozenModelIndexes = _.filter(frozen, x => !_.includes(hidden, x));
this.hiddenModelIndexes = _.filter(hidden, x => !_.includes(frozen, x));
this.hiddenAndFrozenModelIndexes = _.concat(hidden, this.frozenModelIndexes);
this.frozenModelIndexes.sort((a, b) => a - b);
if (this.hiddenAndFrozenModelIndexes.length == 0) this.hiddenAndFrozenModelIndexes = null;
if (this.frozenModelIndexes.length == 0) this.frozenModelIndexes = null;
this.buildIndex();
}
realToModel(realIndex) {
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return realIndex;
if (realIndex < 0) return -1;
if (realIndex < this.frozenCount && this.frozenModelIndexes != null) return this.frozenModelIndexes[realIndex];
if (this.hiddenAndFrozenModelIndexes == null) return realIndex;
realIndex -= this.frozenCount;
for (let hidItem of this.hiddenAndFrozenModelIndexes) {
if (realIndex < hidItem) return realIndex;
realIndex++;
}
return realIndex;
}
modelToReal(modelIndex) {
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return modelIndex;
if (modelIndex < 0) return -1;
let frozenIndex = this.frozenModelIndexes != null ? _.indexOf(this.frozenModelIndexes, modelIndex) : -1;
if (frozenIndex >= 0) return frozenIndex;
if (this.hiddenAndFrozenModelIndexes == null) return modelIndex;
let hiddenIndex = _.sortedIndex(this.hiddenAndFrozenModelIndexes, modelIndex);
if (this.hiddenAndFrozenModelIndexes[hiddenIndex] == modelIndex) return -1;
if (hiddenIndex >= 0) return modelIndex - hiddenIndex + this.frozenCount;
return modelIndex;
}
getFrozenPosition(frozenIndex) {
return this.frozenItems[frozenIndex].position;
}
hasSizeOverride(modelIndex) {
return _.has(this.sizeOverridesByModelIndex, modelIndex);
}
isVisible(testedRealIndex, firstVisibleScrollIndex, viewportSize) {
if (testedRealIndex < 0) return false;
if (testedRealIndex >= 0 && testedRealIndex < this.frozenCount) return true;
let scrollIndex = testedRealIndex - this.frozenCount;
let onPageIndex = scrollIndex - firstVisibleScrollIndex;
return onPageIndex >= 0 && onPageIndex < this.getVisibleScrollCount(firstVisibleScrollIndex, viewportSize);
}
}

97
packages/web/src/icons.js Normal file
View File

@@ -0,0 +1,97 @@
import React from 'react';
import _ from 'lodash';
export function getIconImage(src, props) {
const { size = 16, style = {}, className, title } = props || {};
if (!src) return null;
if (src.endsWith('.svg')) {
src = '/icons/' + src;
}
// if (props.alignToLine) {
// style["position"] = "relative";
// style["top"] = "-2px";
// style["marginRight"] = "4px";
// }
return <img width={size} height={size} src={src} style={style} className={className} title={title} />;
}
export function getFontIcon(fontIconSpec, props = {}) {
let iconClass = fontIconSpec;
if (!iconClass) return null;
var parts = iconClass.split(' ');
var name = parts[0];
parts = parts.slice(1);
var className = props.className || '';
// if (_.startsWith(name, 'bs-')) className += ` glyphicon glyphicon-${name.substr(3)}`;
if (_.startsWith(name, 'fa-')) className += ` fas fa-${name.substr(3)}`;
if (_.includes(parts, 'spin')) className += ' fa-spin';
var style = props.style || {};
var last = parts[parts.length - 1];
if (last && last != 'spin') {
style['color'] = last;
}
return <i className={className} style={style} title={props.title} />;
}
export const TableIcon = props => getIconImage('table2.svg', props);
export const ViewIcon = props => getIconImage('view2.svg', props);
export const DatabaseIcon = props => getIconImage('database.svg', props);
export const ServerIcon = props => getIconImage('server.svg', props);
export const MicrosoftIcon = props => getIconImage('microsoft.svg', props);
export const MySqlIcon = props => getIconImage('mysql.svg', props);
export const PostgreSqlIcon = props => getIconImage('postgresql.svg', props);
export const SqliteIcon = props => getIconImage('sqlite.svg', props);
export const ProcedureIcon = props => getIconImage('procedure2.svg', props);
export const FunctionIcon = props => getIconImage('function.svg', props);
export const TriggerIcon = props => getIconImage('trigger.svg', props);
export const HomeIcon = props => getIconImage('home.svg', props);
export const PrimaryKeyIcon = props => getIconImage('primarykey.svg', props);
export const ForeignKeyIcon = props => getIconImage('foreignkey.svg', props);
export const ComplexKeyIcon = props => getIconImage('complexkey.svg', props);
export const VariableIcon = props => getIconImage('variable.svg', props);
export const UniqueIcon = props => getIconImage('unique.svg', props);
export const IndexIcon = props => getIconImage('index.svg', props);
export const StartIcon = props => getIconImage('start.svg', props);
export const DownCircleIcon = props => getIconImage('down_circle.svg', props);
export const ColumnIcon = props => getIconImage('column.svg', props);
export const SqlIcon = props => getIconImage('sql.svg', props);
export const ExcelIcon = props => getIconImage('excel.svg', props);
export const DiagramIcon = props => getIconImage('diagram.svg', props);
export const QueryDesignIcon = props => getIconImage('querydesign.svg', props);
export const LocalDbIcon = props => getIconImage('localdb.svg', props);
export const CsvIcon = props => getIconImage('csv.svg', props);
export const ChangeSetIcon = props => getIconImage('changeset.svg', props);
export const BinaryFileIcon = props => getIconImage('binaryfile.svg', props);
export const ReferenceIcon = props => getIconImage('reference.svg', props);
export const LinkIcon = props => getIconImage('link.svg', props);
export const SequenceIcon = props => getIconImage('sequence.svg', props);
export const CheckIcon = props => getIconImage('check.svg', props);
export const LinkedServerIcon = props => getIconImage('linkedserver.svg', props);
export const EmptyIcon = props => getIconImage('data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=', props);
export const TimesRedIcon = props => getFontIcon('fa-times red', props);
export const TimesGreenCircleIcon = props => getFontIcon('fa-times-circle green', props);
export const GrayFilterIcon = props => getFontIcon('fa-filter lightgray', props);
export const ExclamationTriangleIcon = props => getFontIcon('fa-exclamation-triangle', props);
export const HourGlassIcon = props => getFontIcon('fa-hourglass', props);
export const InfoBlueCircleIcon = props => getFontIcon('fa-info-circle blue', props);
export const SpinnerIcon = props => getFontIcon('fa-spinner spin', props);
export const FontIcon = ({ name }) => <i className={`fas ${name}`}></i>;

View File

@@ -0,0 +1,15 @@
body {
font-family: -apple-system,BlinkMacSystemFont,Segoe WPC,Segoe UI,HelveticaNeue-Light,Ubuntu,Droid Sans,sans-serif;
font-size: 14px;
/* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
*/
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

19
packages/web/src/index.js Normal file
View File

@@ -0,0 +1,19 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import "@fortawesome/fontawesome-free/css/all.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import "ace-builds/src-noconflict/mode-sql";
import "ace-builds/src-noconflict/mode-mysql";
import "ace-builds/src-noconflict/mode-pgsql";
import "ace-builds/src-noconflict/mode-sqlserver";
import "ace-builds/src-noconflict/theme-github";
ReactDOM.render(<App />, document.getElementById("root"));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

View File

@@ -0,0 +1,49 @@
import React from 'react';
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, connection = undefined }) {
const [sqlConnectResult, setSqlConnectResult] = React.useState('Not connected');
const handleTest = async values => {
const resp = await axios.post('connections/test', values);
const { error, version } = resp.data;
setSqlConnectResult(error || version);
};
const handleSubmit = async values => {
const resp = await axios.post('connections/save', values);
modalState.close();
};
return (
<ModalBase modalState={modalState}>
<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="postgres">Postgre SQL</option>
</FormSelectField>
<FormTextField label="Server" name="server" />
<FormTextField label="Port" name="port" />
<FormTextField label="User" name="user" />
<FormTextField label="Password" name="password" />
<FormTextField label="Display name" name="displayName" />
<FormRow>
<FormButton text="Test" onClick={handleTest} />
<FormSubmit text="Save" />
</FormRow>
</Form>
</Formik>
<div>Connect result: {sqlConnectResult}</div>
</ModalBase>
);
}

View 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 = undefined, 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}>
{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`;
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
import Modal from 'react-modal';
import styled from 'styled-components';
// const StyledModal = styled(Modal)`
// position: absolute;
// top: 40px;
// left: 40px;
// right: 40px;
// bottom: 40px;
// border: 1px solid #ccc;
// background: #fff;
// overflow: auto;
// webkitoverflowscrolling: touch;
// borderradius: 4px;
// outline: none;
// padding: 20px;
// `;
const StyledModal = styled(Modal)`
border: 1px solid #ccc;
background: #fff;
overflow: auto;
webkitoverflowscrolling: touch;
borderradius: 4px;
outline: none;
padding: 20px;
width: 50%;
margin: auto;
margin-top: 15vh;
`;
export default function ModalBase({ modalState, children }) {
return (
<StyledModal isOpen={modalState.isOpen} onRequestClose={modalState.close}>
{children}
</StyledModal>
);
}

View 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);
}

View File

@@ -0,0 +1,8 @@
import React from 'react';
export default function useModalState(isOpenDefault = false) {
const [isOpen, setOpen] = React.useState(isOpenDefault);
const close = () => setOpen(false);
const open = () => setOpen(true);
return { isOpen, open, close };
}

1
packages/web/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,137 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

View File

@@ -0,0 +1,42 @@
import React from 'react';
import useFetch from '../utility/useFetch';
import styled from 'styled-components';
import theme from '../theme';
import AceEditor from 'react-ace';
import useDimensions from '../utility/useDimensions';
const Wrapper = styled.div`
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
`;
export default function TableCreateScriptTab({ conid, database, schemaName, pureName }) {
const sql = `SELECT * FROM MOJE`;
const [containerRef, { height, width }] = useDimensions();
/** @type {import('dbgate').TableInfo} */
const tableInfo = useFetch({
url: 'tables/table-info',
params: { conid, database, schemaName, pureName },
});
return (
<Wrapper ref={containerRef}>
<AceEditor
mode="sql"
theme="github"
// onChange={onChange}
name="UNIQUE_ID_OF_DIV"
editorProps={{ $blockScrolling: true }}
value={sql}
readOnly
fontSize="11pt"
width={`${width}px`}
height={`${height}px`}
/>
</Wrapper>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import useFetch from '../utility/useFetch';
import styled from 'styled-components';
import theme from '../theme';
import DataGrid from '../datagrid/DataGrid';
export default function TableDataTab({ conid, database, schemaName, pureName }) {
return <DataGrid params={{ conid, database, schemaName, pureName }} />;
}

View File

@@ -0,0 +1,123 @@
import React from 'react';
import styled from 'styled-components';
import _ from 'lodash';
import theme from '../theme';
import useFetch from '../utility/useFetch';
import ObjectListControl from '../utility/ObjectListControl';
import { TableColumn } from '../utility/TableControl';
import columnAppObject from '../appobj/columnAppObject';
import constraintAppObject from '../appobj/constraintAppObject';
const WhitePage = styled.div`
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: white;
`;
export default function TableStructureTab({ conid, database, schemaName, pureName }) {
/** @type {import('dbgate').TableInfo} */
const tableInfo = useFetch({
url: 'tables/table-info',
params: { conid, database, schemaName, pureName },
});
if (!tableInfo) return null;
const { columns, primaryKey, foreignKeys, dependencies } = tableInfo;
return (
<WhitePage>
<ObjectListControl
collection={columns.map((x, index) => ({ ...x, ordinal: index + 1 }))}
makeAppObj={columnAppObject}
title="Columns"
>
<TableColumn
fieldName="notNull"
header="Not NULL"
sortable={true}
formatter={row => (row.notNull ? 'YES' : 'NO')}
/>
<TableColumn fieldName="dataType" header="Data Type" sortable={true} />
<TableColumn fieldName="defaultValue" header="Default value" sortable={true} />
<TableColumn
fieldName="isSparse"
header="Is Sparse"
sortable={true}
formatter={row => (row.isSparse ? 'YES' : 'NO')}
/>
<TableColumn fieldName="computedExpression" header="Computed Expression" sortable={true} />
<TableColumn
fieldName="isPersisted"
header="Is Persisted"
sortable={true}
formatter={row => (row.isPersisted ? 'YES' : 'NO')}
/>
{/* {_.includes(dbCaps.columnListOptionalColumns, 'referencedTableNamesFormatted') && (
<TableColumn fieldName="referencedTableNamesFormatted" header="References" sortable={true} />
)}
<TableColumn
fieldName="actions"
header=""
formatter={row => (
<span>
<Link
linkElementId={encodeHtmlId(`button_delete_column_${row.column.name}`)}
onClick={() => this.deleteColumn(row)}
>
Delete
</Link>{' '}
|{' '}
<Link
linkElementId={encodeHtmlId(`button_edit_column__${row.column.name}`)}
onClick={() => this.editColumn(row)}
>
Edit
</Link>
</span>
)}
/> */}
</ObjectListControl>
<ObjectListControl collection={_.compact([primaryKey])} makeAppObj={constraintAppObject} title="Primary key">
<TableColumn
fieldName="columns"
header="Columns"
formatter={row => row.columns.map(x => x.columnName).join(', ')}
/>
</ObjectListControl>
<ObjectListControl collection={foreignKeys} makeAppObj={constraintAppObject} title="Foreign keys">
<TableColumn
fieldName="baseColumns"
header="Base columns"
formatter={row => row.columns.map(x => x.columnName).join(', ')}
/>
<TableColumn fieldName="refTable" header="Referenced table" formatter={row => row.refTableName} />
<TableColumn
fieldName="refColumns"
header="Referenced columns"
formatter={row => row.columns.map(x => x.refColumnName).join(', ')}
/>
<TableColumn fieldName="updateAction" header="ON UPDATE" />
<TableColumn fieldName="deleteAction" header="ON DELETE" />
</ObjectListControl>
<ObjectListControl collection={dependencies} makeAppObj={constraintAppObject} title="Dependencies">
<TableColumn
fieldName="baseColumns"
header="Base columns"
formatter={row => row.columns.map(x => x.columnName).join(', ')}
/>
<TableColumn fieldName="baseTable" header="Base table" formatter={row => row.pureName} />
<TableColumn
fieldName="refColumns"
header="Referenced columns"
formatter={row => row.columns.map(x => x.refColumnName).join(', ')}
/>
<TableColumn fieldName="updateAction" header="ON UPDATE" />
<TableColumn fieldName="deleteAction" header="ON DELETE" />
</ObjectListControl>
</WhitePage>
);
}

View File

@@ -0,0 +1,9 @@
import TableDataTab from './TableDataTab';
import TableStructureTab from './TableStructureTab';
import TableCreateScriptTab from './TableCreateScriptTab'
export default {
TableDataTab,
TableStructureTab,
TableCreateScriptTab,
};

26
packages/web/src/theme.js Normal file
View File

@@ -0,0 +1,26 @@
export default {
widgetMenu: {
iconSize: 60,
background: "#222",
iconFontSize: "23pt",
iconFontColor: "#eee",
backgroundHover: "#555",
backgroundSelected: "#4CAF50",
},
leftPanel: {
width: 300,
background: "#ccc"
},
tabsPanel: {
height: 30,
background: "#ddd",
hoverFont: "#338"
},
statusBar: {
height: 20,
background: "#00c"
},
mainArea: {
background: "#eee"
}
};

View File

@@ -0,0 +1,50 @@
import React from 'react';
import useFetch from '../utility/useFetch';
import styled from 'styled-components';
import theme from '../theme';
import TableControl, { TableColumn } from './TableControl';
import { AppObjectControl } from '../appobj/AppObjects';
import columnAppObject from '../appobj/columnAppObject';
const ObjectListWrapper = styled.div`
margin-bottom: 20px;
`;
const ObjectListHeader = styled.div`
background-color: #ebedef;
padding: 5px;
`;
const ObjectListHeaderTitle = styled.span`
font-weight: bold;
margin-left: 5px;
`;
const ObjectListBody = styled.div`
margin: 20px;
// margin-left: 20px;
// margin-right: 20px;
// margin-top: 3px;
`;
export default function ObjectListControl({ collection = [], title, showIfEmpty = false, makeAppObj, children }) {
if (collection.length == 0 && !showIfEmpty) return null;
return (
<ObjectListWrapper>
<ObjectListHeader>
<ObjectListHeaderTitle>{title}</ObjectListHeaderTitle>
</ObjectListHeader>
<ObjectListBody>
<TableControl rows={collection}>
<TableColumn
fieldName="displayName"
header="Name"
formatter={col => <AppObjectControl data={col} makeAppObj={makeAppObj} component="span" />}
/>
{children}
</TableControl>
</ObjectListBody>
</ObjectListWrapper>
);
}

View File

@@ -0,0 +1,18 @@
import io from 'socket.io-client';
import React from 'react';
const SocketContext = React.createContext(null);
export function SocketProvider({ children }) {
const [socket, setSocket] = React.useState();
React.useEffect(() => {
// const newSocket = io('http://localhost:3000', { transports: ['websocket'] });
const newSocket = io('http://localhost:3000');
setSocket(newSocket);
}, []);
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
}
export default function useSocket() {
return React.useContext(SocketContext);
}

View File

@@ -0,0 +1,62 @@
import React from 'react';
import _ from 'lodash';
import useFetch from '../utility/useFetch';
import styled from 'styled-components';
import theme from '../theme';
const Table = styled.table`
border-collapse: collapse;
width: 100%;
`;
const TableHead = styled.thead``;
const TableBody = styled.tbody``;
const TableHeaderRow = styled.tr``;
const TableBodyRow = styled.tr``;
const TableHeaderCell = styled.td`
border: 1px solid #e8eef4;
background-color: #e8eef4;
padding: 5px;
`;
const TableBodyCell = styled.td`
border: 1px solid #e8eef4;
padding: 5px;
`;
export function TableColumn({ fieldName, header, sortable = false, formatter = undefined }) {
return <></>;
}
function format(row, col) {
const { formatter, fieldName } = col;
if (formatter) return formatter(row);
return row[fieldName];
}
export default function TableControl({ rows = [], children }) {
console.log('children', children);
const columns = (children instanceof Array ? _.flatten(children) : [children])
.filter(child => child && child.props && child.props.fieldName)
.map(child => child.props);
return (
<Table>
<TableHead>
<TableHeaderRow>
{columns.map(x => (
<TableHeaderCell key={x.fieldName}>{x.header}</TableHeaderCell>
))}
</TableHeaderRow>
</TableHead>
<TableBody>
{rows.map((row, index) => (
<TableBodyRow key={index}>
{columns.map(col => (
<TableBodyCell key={col.fieldName}>{format(row, col)}</TableBodyCell>
))}
</TableBodyRow>
))}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,5 @@
import axios from 'axios';
export default axios.create({
baseURL: 'http://localhost:3000',
});

View File

@@ -0,0 +1,27 @@
import uuidv1 from 'uuid/v1';
export class LoadingToken {
constructor() {
this.isCanceled = false;
}
cancel() {
this.isCanceled = true;
}
}
export function sleep(milliseconds) {
return new Promise(resolve => window.setTimeout(() => resolve(null), milliseconds));
}
export function openNewTab(setOpenedTabs, newTab) {
const tabid = uuidv1();
setOpenedTabs(files => [
...(files || []).map(x => ({ ...x, selected: false })),
{
tabid,
selected: true,
...newTab,
},
]);
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import styled from 'styled-components';
import { TextField, SelectField } from './inputs';
import { Field, useFormikContext } from 'formik';
export const FormRow = styled.div`
display: flex;
margin: 10px;
`;
export const FormLabel = styled.div`
width: 10vw;
font-weight: bold;
`;
export const FormValue = styled.div``;
export function FormTextField({ label, ...other }) {
return (
<FormRow>
<FormLabel>{label}</FormLabel>
<FormValue>
<Field {...other} as={TextField} />
</FormValue>
</FormRow>
);
}
export function FormSelectField({ label, children, ...other }) {
return (
<FormRow>
<FormLabel>{label}</FormLabel>
<FormValue>
<Field {...other} as={SelectField}>
{children}
</Field>
</FormValue>
</FormRow>
);
}
export function FormSubmit({ text }) {
return <input type="submit" value={text} />;
}
export function FormButton({ text, onClick, ...other }) {
const { values } = useFormikContext();
return <input type="button" value={text} onClick={() => onClick(values)} {...other} />;
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import useStorage from './useStorage';
function createGlobalState(defaultValue) {
const Context = React.createContext(null);
function Provider({ children }) {
const [currentvalue, setCurrentValue] = React.useState(defaultValue);
return <Context.Provider value={[currentvalue, setCurrentValue]}>{children}</Context.Provider>;
}
function useValue() {
return React.useContext(Context)[0];
}
function useSetValue() {
return React.useContext(Context)[1];
}
return [Provider, useValue, useSetValue];
}
function createStorageState(storageKey, defaultValue) {
const Context = React.createContext(null);
function Provider({ children }) {
const [currentvalue, setCurrentValue] = useStorage(storageKey, localStorage, defaultValue);
return <Context.Provider value={[currentvalue, setCurrentValue]}>{children}</Context.Provider>;
}
function useValue() {
return React.useContext(Context)[0];
}
function useSetValue() {
return React.useContext(Context)[1];
}
return [Provider, useValue, useSetValue];
}
const [CurrentWidgetProvider, useCurrentWidget, useSetCurrentWidget] = createGlobalState('database');
export { CurrentWidgetProvider, useCurrentWidget, useSetCurrentWidget };
const [CurrentDatabaseProvider, useCurrentDatabase, useSetCurrentDatabase] = createGlobalState(null);
export { CurrentDatabaseProvider, useCurrentDatabase, useSetCurrentDatabase };
const [OpenedTabsProvider, useOpenedTabs, useSetOpenedTabs] = createStorageState('openedTabs', []);
export { OpenedTabsProvider, useOpenedTabs, useSetOpenedTabs };

View File

@@ -0,0 +1,13 @@
import React from 'react';
export function TextField({ ...other }) {
return <input type="text" {...other}></input>;
}
export function SelectField({ children, ...other }) {
return (
<select {...other}>
{children}
</select>
);
}

View File

@@ -0,0 +1,7 @@
import styled from 'styled-components';
export const Grid = styled.div``;
export const Row = styled.div``;
export const Col = styled.div``;

View File

@@ -0,0 +1,107 @@
// import { useState, useCallback, useLayoutEffect } from 'react';
// function getDimensionObject(node) {
// const rect = node.getBoundingClientRect();
// return {
// width: rect.width,
// height: rect.height,
// top: 'x' in rect ? rect.x : rect.top,
// left: 'y' in rect ? rect.y : rect.left,
// x: 'x' in rect ? rect.x : rect.left,
// y: 'y' in rect ? rect.y : rect.top,
// right: rect.right,
// bottom: rect.bottom,
// };
// }
// function useDimensions({ liveMeasure = true } = {}) {
// const [dimensions, setDimensions] = useState({});
// const [node, setNode] = useState(null);
// const ref = useCallback(node => {
// setNode(node);
// }, []);
// useLayoutEffect(() => {
// if (node) {
// const measure = () => window.requestAnimationFrame(() => setDimensions(getDimensionObject(node)));
// measure();
// if (liveMeasure) {
// window.addEventListener('resize', measure);
// window.addEventListener('scroll', measure);
// return () => {
// window.removeEventListener('resize', measure);
// window.removeEventListener('scroll', measure);
// };
// }
// }
// }, [node]);
// return [ref, dimensions, node];
// }
// export default useDimensions;
import { useLayoutEffect, useState, useCallback } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
// Export hook
export default function useDimensions(dependencies = []) {
const [node, setNode] = useState(null);
const ref = useCallback(newNode => {
setNode(newNode);
}, []);
// Keep track of measurements
const [dimensions, setDimensions] = useState({
x: 0,
y: 0,
left: 0,
top: 0,
right: 0,
bottom: 0,
width: 0,
height: 0,
});
// Define measure function
const measure = useCallback(innerNode => {
const rect = innerNode.getBoundingClientRect();
setDimensions({
x: rect.left,
y: rect.top,
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
});
}, []);
useLayoutEffect(() => {
if (!node) {
return;
}
// Set initial measurements
measure(node);
// Observe resizing of element
const resizeObserver = new ResizeObserver(() => {
measure(node);
});
resizeObserver.observe(node);
// Cleanup
return () => {
resizeObserver.disconnect();
};
}, [node, measure, ...dependencies]);
return [ref, dimensions, node];
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import axios from './axios';
import useSocket from './SocketProvider';
import stableStringify from 'json-stable-stringify';
export default function useFetch({
url,
params = undefined,
defaultValue = undefined,
reloadTrigger = undefined,
...config
}) {
const [value, setValue] = React.useState(defaultValue);
const [loadCounter, setLoadCounter] = React.useState(0);
const socket = useSocket();
const handleReload = () => {
setLoadCounter(loadCounter + 1);
};
async function loadValue() {
const resp = await axios.request({
method: 'get',
params,
url,
...config,
});
setValue(resp.data);
}
React.useEffect(() => {
loadValue();
if (reloadTrigger && socket) {
socket.on(reloadTrigger, handleReload);
return () => {
socket.off(reloadTrigger, handleReload);
};
}
}, [url, stableStringify(params), socket, loadCounter]);
return value;
}

View File

@@ -0,0 +1,36 @@
import React from 'react';
export default function useStorage(key, storageObject, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = React.useState(() => {
try {
// Get from local storage by key
const item = storageObject.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = value => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
storageObject.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}

View File

@@ -0,0 +1,93 @@
import React from 'react';
import styled from 'styled-components';
import useModalState from '../modals/useModalState';
import ConnectionModal from '../modals/ConnectionModal';
import useFetch from '../utility/useFetch';
import { AppObjectList } from '../appobj/AppObjectList';
import connectionAppObject from '../appobj/connectionAppObject';
import databaseAppObject from '../appobj/databaseAppObject';
import { useSetCurrentDatabase, useCurrentDatabase } from '../utility/globalState';
import tableAppObject from '../appobj/tableAppObject';
import theme from '../theme';
const MainContainer = styled.div`
position: relative;
display: flex;
flex-flow: column wrap;
flex: 1;
`;
const InnerContainer = styled.div`
flex: 1 0;
overflow: scroll;
width: ${theme.leftPanel.width}px;
`;
function SubDatabaseList({ data }) {
const setDb = useSetCurrentDatabase();
const handleDatabaseClick = database => {
setDb({
...database,
connection: data,
});
};
const { _id } = data;
const databases = useFetch({
url: `server-connections/list-databases?conid=${_id}`,
reloadTrigger: `database-list-changed-${_id}`,
});
return <AppObjectList list={databases} makeAppObj={databaseAppObject} onObjectClick={handleDatabaseClick} />;
}
function ConnectionList() {
const modalState = useModalState();
const connections = useFetch({
url: 'connections/list',
reloadTrigger: 'connection-list-changed',
});
return (
<>
<ConnectionModal modalState={modalState} />
<button onClick={modalState.open}>Add connection</button>
<AppObjectList list={connections} makeAppObj={connectionAppObject} SubItems={SubDatabaseList} />
</>
);
}
function SqlObjectList({ conid, database }) {
const objects = useFetch({
url: `database-connections/list-objects?conid=${conid}&database=${database}`,
reloadTrigger: `database-structure-changed-${conid}-${database}`,
});
const { tables } = objects || {};
return (
<>
<AppObjectList list={(tables || []).map(x => ({ ...x, conid, database }))} makeAppObj={tableAppObject} />
</>
);
}
function SqlObjectListWrapper() {
const db = useCurrentDatabase();
if (!db) return <div>(Choose database)</div>;
const { name, connection } = db;
return <SqlObjectList conid={connection._id} database={name} />;
// return <div>tables of {db && db.name}</div>
// return <div>tables of {JSON.stringify(db)}</div>
}
export default function DatabaseWidget() {
return (
<MainContainer>
<InnerContainer>
<ConnectionList />
</InnerContainer>
<InnerContainer>
<SqlObjectListWrapper />
</InnerContainer>
</MainContainer>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { useCurrentWidget } from '../utility/globalState';
import DatabaseWidget from './DatabaseWidget';
export default function WidgetContainer() {
const currentWidget = useCurrentWidget();
if (currentWidget === 'database') return <DatabaseWidget />;
return null;
}

View File

@@ -0,0 +1,63 @@
import React from 'react';
import theme from '../theme';
import styled from 'styled-components';
import { FontIcon } from '../icons';
import { useCurrentWidget, useSetCurrentWidget } from '../utility/globalState';
const IconWrapper = styled.div`
color: ${theme.widgetMenu.iconFontColor};
font-size: ${theme.widgetMenu.iconFontSize};
height: ${theme.widgetMenu.iconSize}px;
display: flex;
align-items: center;
justify-content: center;
background-color: ${props =>
// @ts-ignore
props.isSelected ? theme.widgetMenu.backgroundSelected : 'inherit'};
&:hover {
background-color: ${theme.widgetMenu.backgroundHover};
}
`;
export default function WidgetIconPanel() {
const widgets = [
{
icon: 'fa-database',
name: 'database',
},
{
icon: 'fa-table',
name: 'table',
},
{
icon: 'fa-file-alt',
name: 'file',
},
{
icon: 'fa-cog',
name: 'settings',
},
// {
// icon: 'fa-check',
// name: 'settings',
// },
];
const currentWidget = useCurrentWidget();
const setCurrentWidget = useSetCurrentWidget();
return (
<>
{widgets.map(({ icon, name }) => (
<IconWrapper
key={icon}
// @ts-ignore
isSelected={name === currentWidget}
onClick={() => setCurrentWidget(name === currentWidget ? null : name)}
>
<FontIcon name={icon} />
</IconWrapper>
))}
</>
);
}