mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-30 01:03:58 +00:00
grid scollbar
This commit is contained in:
120
web/src/datagrid/DataGrid.js
Normal file
120
web/src/datagrid/DataGrid.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React, { useState } 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';
|
||||||
|
|
||||||
|
const GridContainer = styled.div``;
|
||||||
|
|
||||||
|
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] = useState(0);
|
||||||
|
|
||||||
|
const [headerRowRef, { height: rowHeight }] = useDimensions();
|
||||||
|
const [tableBodyRef, { height: gridScrollAreaHeight }] = useDimensions();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('visibleRowCountUpperBound', visibleRowCountUpperBound);
|
||||||
|
console.log('gridScrollAreaHeight', gridScrollAreaHeight);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableHeaderRow ref={headerRowRef}>
|
||||||
|
{columns.map(col => (
|
||||||
|
<TableHeaderCell key={col.name} style={{ width: '60px' }}>
|
||||||
|
{col.name}
|
||||||
|
</TableHeaderCell>
|
||||||
|
))}
|
||||||
|
</TableHeaderRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody ref={tableBodyRef}>
|
||||||
|
{rows
|
||||||
|
.slice(firstVisibleRowScrollIndex, firstVisibleRowScrollIndex + visibleRowCountUpperBound)
|
||||||
|
.map((row, index) => (
|
||||||
|
<TableBodyRow key={firstVisibleRowScrollIndex + index}>
|
||||||
|
{columns.map(col => (
|
||||||
|
<TableBodyCell key={col.name} style={{ width: '60px' }}>
|
||||||
|
{row[col.name]}
|
||||||
|
</TableBodyCell>
|
||||||
|
))}
|
||||||
|
</TableBodyRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<HorizontalScrollBar minimum={0} maximum={columns.length - 1} />
|
||||||
|
<VerticalScrollBar
|
||||||
|
minimum={0}
|
||||||
|
maximum={rowCountNewIncluded - visibleRowCountUpperBound + 2}
|
||||||
|
onScroll={handleRowScroll}
|
||||||
|
viewportRatio={visibleRowCountUpperBound / rowCountNewIncluded}
|
||||||
|
/>
|
||||||
|
</GridContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
214
web/src/datagrid/ScrollBars.js
Normal file
214
web/src/datagrid/ScrollBars.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
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 }] = useDimensions();
|
||||||
|
const contentSize = Math.round(width / viewportRatio);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const position01 = (valueToSet - minimum) / (maximum - minimum + 1);
|
||||||
|
const position = position01 * (contentSize - width);
|
||||||
|
if (ref.current) ref.current.scrollLeft = Math.floor(position);
|
||||||
|
}, [valueToSetDate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledHorizontalScrollBar ref={ref}>
|
||||||
|
<StyledHorizontalScrollContent style={{ width: `${contentSize}px` }}> </StyledHorizontalScrollContent>
|
||||||
|
</StyledHorizontalScrollBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VerticalScrollBar({
|
||||||
|
onScroll,
|
||||||
|
valueToSet = undefined,
|
||||||
|
valueToSetDate = undefined,
|
||||||
|
minimum,
|
||||||
|
maximum,
|
||||||
|
viewportRatio = 0.5,
|
||||||
|
}) {
|
||||||
|
const [ref, { height }] = useDimensions();
|
||||||
|
const contentSize = Math.round(height / viewportRatio);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const position01 = (valueToSet - minimum) / (maximum - minimum + 1);
|
||||||
|
const position = position01 * (contentSize - height);
|
||||||
|
if (ref.current) ref.current.scrollTop = Math.floor(position);
|
||||||
|
}, [valueToSetDate]);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const position = ref.current.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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -2,109 +2,8 @@ import React from 'react';
|
|||||||
import useFetch from '../utility/useFetch';
|
import useFetch from '../utility/useFetch';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import theme from '../theme';
|
import theme from '../theme';
|
||||||
|
import DataGrid from '../datagrid/DataGrid';
|
||||||
// const Table = styled.table`
|
|
||||||
// position: absolute;
|
|
||||||
// left:0;
|
|
||||||
// top:0:
|
|
||||||
// bottom:0;
|
|
||||||
// right:0;
|
|
||||||
// overflow: scroll;
|
|
||||||
// `;
|
|
||||||
// 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;
|
|
||||||
// `;
|
|
||||||
// const TableHeaderCell = styled.td`
|
|
||||||
// font-weight: bold;
|
|
||||||
// `;
|
|
||||||
// const TableBodyCell = styled.td`
|
|
||||||
// white-space: nowrap;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
const Table = styled.div`
|
|
||||||
overflow-x: scroll;
|
|
||||||
width: 500px;
|
|
||||||
position: absolute;
|
|
||||||
left:0;
|
|
||||||
top:0:
|
|
||||||
bottom:0;
|
|
||||||
right:0;
|
|
||||||
`;
|
|
||||||
const TableHead = styled.div`
|
|
||||||
// display: block;
|
|
||||||
// width: 300px;
|
|
||||||
// width:700px;
|
|
||||||
`;
|
|
||||||
const TableBody = styled.div`
|
|
||||||
// display: block;
|
|
||||||
overflow-y: scroll;
|
|
||||||
height: 200px;
|
|
||||||
`;
|
|
||||||
const TableHeaderRow = styled.div`
|
|
||||||
display: flex;
|
|
||||||
height: 35px;
|
|
||||||
`;
|
|
||||||
const TableBodyRow = styled.div`
|
|
||||||
display: flex;
|
|
||||||
height: 35px;
|
|
||||||
`;
|
|
||||||
const TableHeaderCell = styled.div`
|
|
||||||
font-weight: bold;
|
|
||||||
width: 160px;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
const TableBodyCell = styled.div`
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 160px;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function TableDataTab({ conid, database, schemaName, pureName }) {
|
export default function TableDataTab({ conid, database, schemaName, pureName }) {
|
||||||
const data = useFetch({
|
return <DataGrid params={{ conid, database, schemaName, pureName }} />;
|
||||||
url: 'tables/table-data',
|
|
||||||
params: {
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
schemaName,
|
|
||||||
pureName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { rows, columns } = data || {};
|
|
||||||
if (!columns || !rows) return null;
|
|
||||||
return (
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableHeaderRow>
|
|
||||||
{columns.map(col => (
|
|
||||||
<TableHeaderCell key={col.name} style={{ width: '60px' }}>
|
|
||||||
{col.name}
|
|
||||||
</TableHeaderCell>
|
|
||||||
))}
|
|
||||||
</TableHeaderRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{rows.map((row, index) => (
|
|
||||||
<TableBodyRow key={index}>
|
|
||||||
{columns.map(col => (
|
|
||||||
<TableBodyCell key={col.name} style={{ width: '60px' }}>
|
|
||||||
{row[col.name]}
|
|
||||||
</TableBodyCell>
|
|
||||||
))}
|
|
||||||
</TableBodyRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
48
web/src/utility/useDimensions.js
Normal file
48
web/src/utility/useDimensions.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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);
|
||||||
|
}, []);
|
||||||
|
// @ts-ignore
|
||||||
|
ref.current = 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;
|
||||||
Reference in New Issue
Block a user