mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-05-02 16:03:59 +00:00
resizable widgets
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
WidgetsOuterContainer,
|
WidgetsOuterContainer,
|
||||||
WidgetTitle,
|
WidgetTitle,
|
||||||
} from './WidgetStyles';
|
} from './WidgetStyles';
|
||||||
|
import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar';
|
||||||
import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject';
|
import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject';
|
||||||
import { useArchiveFiles, useArchiveFolders } from '../utility/metadataLoaders';
|
import { useArchiveFiles, useArchiveFolders } from '../utility/metadataLoaders';
|
||||||
import archiveFolderAppObject from '../appobj/archiveFolderAppObject';
|
import archiveFolderAppObject from '../appobj/archiveFolderAppObject';
|
||||||
@@ -22,7 +23,6 @@ import axios from '../utility/axios';
|
|||||||
|
|
||||||
function ArchiveFolderList() {
|
function ArchiveFolderList() {
|
||||||
const folders = useArchiveFolders();
|
const folders = useArchiveFolders();
|
||||||
const inputRef = React.useRef(null);
|
|
||||||
const [filter, setFilter] = React.useState('');
|
const [filter, setFilter] = React.useState('');
|
||||||
|
|
||||||
const setArchive = useSetCurrentArchive();
|
const setArchive = useSetCurrentArchive();
|
||||||
@@ -33,9 +33,8 @@ function ArchiveFolderList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WidgetTitle inputRef={inputRef}>Archive folder</WidgetTitle>
|
|
||||||
<SearchBoxWrapper>
|
<SearchBoxWrapper>
|
||||||
<SearchInput inputRef={inputRef} placeholder="Search archive folders" filter={filter} setFilter={setFilter} />
|
<SearchInput placeholder="Search archive folders" filter={filter} setFilter={setFilter} />
|
||||||
<InlineButton onClick={handleRefreshFolders}>Refresh</InlineButton>
|
<InlineButton onClick={handleRefreshFolders}>Refresh</InlineButton>
|
||||||
</SearchBoxWrapper>
|
</SearchBoxWrapper>
|
||||||
<WidgetsInnerContainer>
|
<WidgetsInnerContainer>
|
||||||
@@ -53,7 +52,6 @@ function ArchiveFolderList() {
|
|||||||
function ArchiveFilesList() {
|
function ArchiveFilesList() {
|
||||||
const folder = useCurrentArchive();
|
const folder = useCurrentArchive();
|
||||||
const files = useArchiveFiles({ folder });
|
const files = useArchiveFiles({ folder });
|
||||||
const inputRef = React.useRef(null);
|
|
||||||
const [filter, setFilter] = React.useState('');
|
const [filter, setFilter] = React.useState('');
|
||||||
const handleRefreshFiles = () => {
|
const handleRefreshFiles = () => {
|
||||||
axios.post('archive/refresh-files', { folder });
|
axios.post('archive/refresh-files', { folder });
|
||||||
@@ -61,9 +59,8 @@ function ArchiveFilesList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WidgetTitle inputRef={inputRef}>Archive files</WidgetTitle>
|
|
||||||
<SearchBoxWrapper>
|
<SearchBoxWrapper>
|
||||||
<SearchInput inputRef={inputRef} placeholder="Search archive files" filter={filter} setFilter={setFilter} />
|
<SearchInput placeholder="Search archive files" filter={filter} setFilter={setFilter} />
|
||||||
<InlineButton onClick={handleRefreshFiles}>Refresh</InlineButton>
|
<InlineButton onClick={handleRefreshFiles}>Refresh</InlineButton>
|
||||||
</SearchBoxWrapper>
|
</SearchBoxWrapper>
|
||||||
<WidgetsInnerContainer>
|
<WidgetsInnerContainer>
|
||||||
@@ -82,13 +79,13 @@ function ArchiveFilesList() {
|
|||||||
|
|
||||||
export default function ArchiveWidget() {
|
export default function ArchiveWidget() {
|
||||||
return (
|
return (
|
||||||
<WidgetsMainContainer>
|
<WidgetColumnBar>
|
||||||
<WidgetsOuterContainer>
|
<WidgetColumnBarItem title="Archive folders" name="folders" height="50%">
|
||||||
<ArchiveFolderList />
|
<ArchiveFolderList />
|
||||||
</WidgetsOuterContainer>
|
</WidgetColumnBarItem>
|
||||||
<WidgetsOuterContainer>
|
<WidgetColumnBarItem title="Archive files" name="files">
|
||||||
<ArchiveFilesList />
|
<ArchiveFilesList />
|
||||||
</WidgetsOuterContainer>
|
</WidgetColumnBarItem>
|
||||||
</WidgetsMainContainer>
|
</WidgetColumnBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import axios from '../utility/axios';
|
|||||||
import LoadingInfo from './LoadingInfo';
|
import LoadingInfo from './LoadingInfo';
|
||||||
import SearchInput from './SearchInput';
|
import SearchInput from './SearchInput';
|
||||||
import ErrorInfo from './ErrorInfo';
|
import ErrorInfo from './ErrorInfo';
|
||||||
|
import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar';
|
||||||
|
|
||||||
function SubDatabaseList({ data }) {
|
function SubDatabaseList({ data }) {
|
||||||
const setDb = useSetCurrentDatabase();
|
const setDb = useSetCurrentDatabase();
|
||||||
@@ -60,14 +61,12 @@ function ConnectionList() {
|
|||||||
axios.post('server-connections/refresh', { conid });
|
axios.post('server-connections/refresh', { conid });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const inputRef = React.useRef(null);
|
|
||||||
|
|
||||||
const [filter, setFilter] = React.useState('');
|
const [filter, setFilter] = React.useState('');
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WidgetTitle inputRef={inputRef}>Connections</WidgetTitle>
|
|
||||||
<SearchBoxWrapper>
|
<SearchBoxWrapper>
|
||||||
<SearchInput placeholder="Search connection" filter={filter} setFilter={setFilter} inputRef={inputRef} />
|
<SearchInput placeholder="Search connection" filter={filter} setFilter={setFilter} />
|
||||||
<InlineButton onClick={handleRefreshConnections}>Refresh</InlineButton>
|
<InlineButton onClick={handleRefreshConnections}>Refresh</InlineButton>
|
||||||
</SearchBoxWrapper>
|
</SearchBoxWrapper>
|
||||||
|
|
||||||
@@ -103,7 +102,6 @@ function SqlObjectList({ conid, database }) {
|
|||||||
const inputRef = React.useRef(null);
|
const inputRef = React.useRef(null);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WidgetTitle inputRef={inputRef}>Tables, views, functions</WidgetTitle>
|
|
||||||
<SearchBoxWrapper>
|
<SearchBoxWrapper>
|
||||||
<SearchInput inputRef={inputRef} placeholder="Search tables or objects" filter={filter} setFilter={setFilter} />
|
<SearchInput inputRef={inputRef} placeholder="Search tables or objects" filter={filter} setFilter={setFilter} />
|
||||||
<InlineButton onClick={handleRefreshDatabase}>Refresh</InlineButton>
|
<InlineButton onClick={handleRefreshDatabase}>Refresh</InlineButton>
|
||||||
@@ -128,12 +126,7 @@ function SqlObjectListWrapper() {
|
|||||||
const db = useCurrentDatabase();
|
const db = useCurrentDatabase();
|
||||||
|
|
||||||
if (!db) {
|
if (!db) {
|
||||||
return (
|
return <ErrorInfo message="Database not selected" icon="fas fa-exclamation-circle blue" />;
|
||||||
<>
|
|
||||||
<WidgetTitle>Tables, views, functions</WidgetTitle>
|
|
||||||
<ErrorInfo message="Database not selected" icon="fas fa-exclamation-circle blue" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const { name, connection } = db;
|
const { name, connection } = db;
|
||||||
|
|
||||||
@@ -144,13 +137,13 @@ function SqlObjectListWrapper() {
|
|||||||
|
|
||||||
export default function DatabaseWidget() {
|
export default function DatabaseWidget() {
|
||||||
return (
|
return (
|
||||||
<WidgetsMainContainer>
|
<WidgetColumnBar>
|
||||||
<WidgetsOuterContainer>
|
<WidgetColumnBarItem title="Connections" name="connections" height="50%">
|
||||||
<ConnectionList />
|
<ConnectionList />
|
||||||
</WidgetsOuterContainer>
|
</WidgetColumnBarItem>
|
||||||
<WidgetsOuterContainer>
|
<WidgetColumnBarItem title="Tables, views, functions" name="dbObjects">
|
||||||
<SqlObjectListWrapper />
|
<SqlObjectListWrapper />
|
||||||
</WidgetsOuterContainer>
|
</WidgetColumnBarItem>
|
||||||
</WidgetsMainContainer>
|
</WidgetColumnBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ import {
|
|||||||
WidgetTitle,
|
WidgetTitle,
|
||||||
} from './WidgetStyles';
|
} from './WidgetStyles';
|
||||||
import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject';
|
import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject';
|
||||||
|
import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar';
|
||||||
|
|
||||||
function ClosedTabsList() {
|
function ClosedTabsList() {
|
||||||
const tabs = useOpenedTabs();
|
const tabs = useOpenedTabs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WidgetTitle>Recently closed tabs</WidgetTitle>
|
|
||||||
<WidgetsInnerContainer>
|
<WidgetsInnerContainer>
|
||||||
<AppObjectList
|
<AppObjectList
|
||||||
list={_.sortBy(
|
list={_.sortBy(
|
||||||
@@ -38,7 +38,6 @@ function SavedSqlFilesList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WidgetTitle>Saved SQL files</WidgetTitle>
|
|
||||||
<WidgetsInnerContainer>
|
<WidgetsInnerContainer>
|
||||||
<AppObjectList list={files} makeAppObj={savedSqlFileAppObject()} />
|
<AppObjectList list={files} makeAppObj={savedSqlFileAppObject()} />
|
||||||
</WidgetsInnerContainer>
|
</WidgetsInnerContainer>
|
||||||
@@ -48,13 +47,13 @@ function SavedSqlFilesList() {
|
|||||||
|
|
||||||
export default function FilesWidget() {
|
export default function FilesWidget() {
|
||||||
return (
|
return (
|
||||||
<WidgetsMainContainer>
|
<WidgetColumnBar>
|
||||||
<WidgetsOuterContainer>
|
<WidgetColumnBarItem title="Recently closed tabs" name="closedTabs" height="50%">
|
||||||
<ClosedTabsList />
|
<ClosedTabsList />
|
||||||
</WidgetsOuterContainer>
|
</WidgetColumnBarItem>
|
||||||
<WidgetsOuterContainer>
|
<WidgetColumnBarItem title="Saved SQL files" name="sqlFiles">
|
||||||
<SavedSqlFilesList />
|
<SavedSqlFilesList />
|
||||||
</WidgetsOuterContainer>
|
</WidgetColumnBarItem>
|
||||||
</WidgetsMainContainer>
|
</WidgetColumnBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const SplitterMainBase = styled.div`
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const VerticalMainContainer = styled(SplitterMainBase)`
|
const VerticalMainContainer = styled(SplitterMainBase)`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
72
packages/web/src/widgets/WidgetColumnBar.js
Normal file
72
packages/web/src/widgets/WidgetColumnBar.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {
|
||||||
|
SearchBoxWrapper,
|
||||||
|
WidgetsInnerContainer,
|
||||||
|
WidgetsMainContainer,
|
||||||
|
WidgetsOuterContainer,
|
||||||
|
WidgetTitle,
|
||||||
|
} from './WidgetStyles';
|
||||||
|
import { VerticalSplitHandle, useSplitterDrag } from './Splitter';
|
||||||
|
import useDimensions from '../utility/useDimensions';
|
||||||
|
|
||||||
|
export function WidgetColumnBarItem({ title, children, name, height = undefined }) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function WidgetContainer({ widget, visible, splitterVisible, parentHeight, initialSize = undefined }) {
|
||||||
|
const [size, setSize] = React.useState(null);
|
||||||
|
|
||||||
|
const handleResizeDown = useSplitterDrag('clientY', (diff) => setSize((v) => v + diff));
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (_.isString(initialSize) && initialSize.endsWith('px')) setSize(parseInt(initialSize.slice(0, -2)));
|
||||||
|
else if (_.isString(initialSize) && initialSize.endsWith('%'))
|
||||||
|
setSize((parentHeight * parseFloat(initialSize.slice(0, -1))) / 100);
|
||||||
|
else setSize(parentHeight / 3);
|
||||||
|
}, [parentHeight]);
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WidgetsOuterContainer style={splitterVisible ? { height: size } : null}>
|
||||||
|
{widget.props.children}
|
||||||
|
</WidgetsOuterContainer>
|
||||||
|
{splitterVisible && <VerticalSplitHandle onMouseDown={handleResizeDown} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WidgetColumnBar({ children }) {
|
||||||
|
const childArray = _.isArray(children) ? children : [children];
|
||||||
|
const [refNode, dimensions] = useDimensions();
|
||||||
|
const [collapsedWidgets, setCollapsedWidgets] = React.useState(() =>
|
||||||
|
childArray.filter((x) => x.props.collapsed).map((x) => x.props.key)
|
||||||
|
);
|
||||||
|
const toggleCollapsed = (name) => {
|
||||||
|
if (collapsedWidgets.includes(name)) setCollapsedWidgets(collapsedWidgets.filter((x) => x != name));
|
||||||
|
else setCollapsedWidgets([...collapsedWidgets, name]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WidgetsMainContainer ref={refNode}>
|
||||||
|
{childArray.map((widget, index) => {
|
||||||
|
if (!widget) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WidgetTitle onClick={() => toggleCollapsed(widget.props.name)}>{widget.props.title}</WidgetTitle>
|
||||||
|
<WidgetContainer
|
||||||
|
parentHeight={dimensions && dimensions.height}
|
||||||
|
visible={!collapsedWidgets.includes(widget.props.name)}
|
||||||
|
widget={widget}
|
||||||
|
key={widget.props.name}
|
||||||
|
initialSize={widget.props.height}
|
||||||
|
splitterVisible={!!childArray.slice(index + 1).find((x) => !collapsedWidgets.includes(x.props.name))}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</WidgetsMainContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@ export const WidgetsMainContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledWidgetsOuterContainer = styled.div`
|
const StyledWidgetsOuterContainer = styled.div`
|
||||||
flex: 1 1 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: ${(props) => props.leftPanelWidth}px;
|
width: ${(props) => props.leftPanelWidth}px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -27,9 +26,20 @@ const StyledWidgetsOuterContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function WidgetsOuterContainer({ children }) {
|
export function WidgetsOuterContainer({ children, style = undefined, refNode = undefined }) {
|
||||||
const leftPanelWidth = useLeftPanelWidth();
|
const leftPanelWidth = useLeftPanelWidth();
|
||||||
return <StyledWidgetsOuterContainer leftPanelWidth={leftPanelWidth}>{children}</StyledWidgetsOuterContainer>;
|
return (
|
||||||
|
<StyledWidgetsOuterContainer
|
||||||
|
ref={refNode}
|
||||||
|
leftPanelWidth={leftPanelWidth}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
flex: style && (style.height != null || style.width != null) ? undefined : '1 1 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StyledWidgetsOuterContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledWidgetsInnerContainer = styled.div`
|
export const StyledWidgetsInnerContainer = styled.div`
|
||||||
@@ -53,14 +63,16 @@ const StyledWidgetTitle = styled.div`
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
|
border: 1px solid #aaa;
|
||||||
// background-color: #CEC;
|
// background-color: #CEC;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function WidgetTitle({ children, inputRef = undefined }) {
|
export function WidgetTitle({ children, inputRef = undefined, onClick = undefined }) {
|
||||||
return (
|
return (
|
||||||
<StyledWidgetTitle
|
<StyledWidgetTitle
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (inputRef && inputRef.current) inputRef.current.focus();
|
if (inputRef && inputRef.current) inputRef.current.focus();
|
||||||
|
if (onClick) onClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user