mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-26 16:46:00 +00:00
introduced yarn workspace
This commit is contained in:
38
packages/web/src/App.css
Normal file
38
packages/web/src/App.css
Normal 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
21
packages/web/src/App.js
Normal 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;
|
||||
11
packages/web/src/App.test.js
Normal file
11
packages/web/src/App.test.js
Normal 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();
|
||||
});
|
||||
79
packages/web/src/Screen.js
Normal file
79
packages/web/src/Screen.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
18
packages/web/src/TabContent.js
Normal file
18
packages/web/src/TabContent.js
Normal 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;
|
||||
}
|
||||
65
packages/web/src/TabsPanel.js
Normal file
65
packages/web/src/TabsPanel.js
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
23
packages/web/src/appobj/AppObjectList.js
Normal file
23
packages/web/src/appobj/AppObjectList.js
Normal 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;
|
||||
});
|
||||
}
|
||||
52
packages/web/src/appobj/AppObjects.js
Normal file
52
packages/web/src/appobj/AppObjects.js
Normal 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} />;
|
||||
}
|
||||
24
packages/web/src/appobj/columnAppObject.js
Normal file
24
packages/web/src/appobj/columnAppObject.js
Normal 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 };
|
||||
}
|
||||
43
packages/web/src/appobj/connectionAppObject.js
Normal file
43
packages/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 };
|
||||
}
|
||||
24
packages/web/src/appobj/constraintAppObject.js
Normal file
24
packages/web/src/appobj/constraintAppObject.js
Normal 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 };
|
||||
}
|
||||
29
packages/web/src/appobj/databaseAppObject.js
Normal file
29
packages/web/src/appobj/databaseAppObject.js
Normal 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 };
|
||||
}
|
||||
57
packages/web/src/appobj/tableAppObject.js
Normal file
57
packages/web/src/appobj/tableAppObject.js
Normal 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 };
|
||||
}
|
||||
252
packages/web/src/datagrid/DataGrid.js
Normal file
252
packages/web/src/datagrid/DataGrid.js
Normal 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>
|
||||
);
|
||||
}
|
||||
222
packages/web/src/datagrid/ScrollBars.js
Normal file
222
packages/web/src/datagrid/ScrollBars.js
Normal 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` }}> </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` }}> </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 }}>
|
||||
//
|
||||
// </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 }}>
|
||||
//
|
||||
// </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);
|
||||
// }
|
||||
// }
|
||||
340
packages/web/src/datagrid/SeriesSizes.js
Normal file
340
packages/web/src/datagrid/SeriesSizes.js
Normal 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
97
packages/web/src/icons.js
Normal 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>;
|
||||
15
packages/web/src/index.css
Normal file
15
packages/web/src/index.css
Normal 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
19
packages/web/src/index.js
Normal 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();
|
||||
49
packages/web/src/modals/ConnectionModal.js
Normal file
49
packages/web/src/modals/ConnectionModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
291
packages/web/src/modals/DropDownMenu.js
Normal file
291
packages/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 = 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`;
|
||||
}
|
||||
40
packages/web/src/modals/ModalBase.js
Normal file
40
packages/web/src/modals/ModalBase.js
Normal 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>
|
||||
);
|
||||
}
|
||||
17
packages/web/src/modals/showModal.js
Normal file
17
packages/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);
|
||||
}
|
||||
8
packages/web/src/modals/useModalState.js
Normal file
8
packages/web/src/modals/useModalState.js
Normal 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
1
packages/web/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
137
packages/web/src/serviceWorker.js
Normal file
137
packages/web/src/serviceWorker.js
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
5
packages/web/src/setupTests.js
Normal file
5
packages/web/src/setupTests.js
Normal 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';
|
||||
42
packages/web/src/tabs/TableCreateScriptTab.js
Normal file
42
packages/web/src/tabs/TableCreateScriptTab.js
Normal 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>
|
||||
);
|
||||
}
|
||||
9
packages/web/src/tabs/TableDataTab.js
Normal file
9
packages/web/src/tabs/TableDataTab.js
Normal 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 }} />;
|
||||
}
|
||||
123
packages/web/src/tabs/TableStructureTab.js
Normal file
123
packages/web/src/tabs/TableStructureTab.js
Normal 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>
|
||||
);
|
||||
}
|
||||
9
packages/web/src/tabs/index.js
Normal file
9
packages/web/src/tabs/index.js
Normal 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
26
packages/web/src/theme.js
Normal 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"
|
||||
}
|
||||
};
|
||||
50
packages/web/src/utility/ObjectListControl.js
Normal file
50
packages/web/src/utility/ObjectListControl.js
Normal 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>
|
||||
);
|
||||
}
|
||||
18
packages/web/src/utility/SocketProvider.js
Normal file
18
packages/web/src/utility/SocketProvider.js
Normal 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);
|
||||
}
|
||||
62
packages/web/src/utility/TableControl.js
Normal file
62
packages/web/src/utility/TableControl.js
Normal 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>
|
||||
);
|
||||
}
|
||||
5
packages/web/src/utility/axios.js
Normal file
5
packages/web/src/utility/axios.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export default axios.create({
|
||||
baseURL: 'http://localhost:3000',
|
||||
});
|
||||
27
packages/web/src/utility/common.js
Normal file
27
packages/web/src/utility/common.js
Normal 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,
|
||||
},
|
||||
]);
|
||||
}
|
||||
49
packages/web/src/utility/forms.js
Normal file
49
packages/web/src/utility/forms.js
Normal 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} />;
|
||||
}
|
||||
49
packages/web/src/utility/globalState.js
Normal file
49
packages/web/src/utility/globalState.js
Normal 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 };
|
||||
13
packages/web/src/utility/inputs.js
Normal file
13
packages/web/src/utility/inputs.js
Normal 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>
|
||||
);
|
||||
}
|
||||
7
packages/web/src/utility/layout.js
Normal file
7
packages/web/src/utility/layout.js
Normal 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``;
|
||||
107
packages/web/src/utility/useDimensions.js
Normal file
107
packages/web/src/utility/useDimensions.js
Normal 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];
|
||||
}
|
||||
41
packages/web/src/utility/useFetch.js
Normal file
41
packages/web/src/utility/useFetch.js
Normal 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;
|
||||
}
|
||||
36
packages/web/src/utility/useStorage.js
Normal file
36
packages/web/src/utility/useStorage.js
Normal 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];
|
||||
}
|
||||
93
packages/web/src/widgets/DatabaseWidget.js
Normal file
93
packages/web/src/widgets/DatabaseWidget.js
Normal 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>
|
||||
);
|
||||
}
|
||||
9
packages/web/src/widgets/WidgetContainer.js
Normal file
9
packages/web/src/widgets/WidgetContainer.js
Normal 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;
|
||||
}
|
||||
63
packages/web/src/widgets/WidgetIconPanel.js
Normal file
63
packages/web/src/widgets/WidgetIconPanel.js
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user