mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-17 23:45:59 +00:00
expandable FK columns
This commit is contained in:
@@ -1,3 +1,23 @@
|
||||
import { DisplayColumn } from './GridDisplay';
|
||||
|
||||
export interface GridConfig {
|
||||
hiddenColumns: string[];
|
||||
expandedColumns: string[];
|
||||
}
|
||||
|
||||
export interface GridCache {
|
||||
subcolumns: { [column: string]: DisplayColumn[] };
|
||||
}
|
||||
|
||||
export function createGridConfig(): GridConfig {
|
||||
return {
|
||||
hiddenColumns: [],
|
||||
expandedColumns: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function createGridCache(): GridCache {
|
||||
return {
|
||||
subcolumns: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import _ from 'lodash';
|
||||
import { GridConfig } from './GridConfig';
|
||||
import { ForeignKeyInfo } from '@dbgate/types';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
import { ForeignKeyInfo, TableInfo } from '@dbgate/types';
|
||||
import { filterName } from './filterName';
|
||||
|
||||
export interface DisplayColumn {
|
||||
schemaName: string;
|
||||
pureName: string;
|
||||
columnName: string;
|
||||
headerText: string;
|
||||
uniqueName: string;
|
||||
@@ -15,7 +18,13 @@ export interface DisplayColumn {
|
||||
}
|
||||
|
||||
export abstract class GridDisplay {
|
||||
constructor(public config: GridConfig, protected setConfig: (config: GridConfig) => void) {}
|
||||
constructor(
|
||||
public config: GridConfig,
|
||||
protected setConfig: (config: GridConfig) => void,
|
||||
protected cache: GridCache,
|
||||
protected setCache: (config: GridCache) => void,
|
||||
protected getTableInfo: ({ schemaName, pureName }) => Promise<TableInfo>
|
||||
) {}
|
||||
abstract getPageQuery(offset: number, count: number): string;
|
||||
columns: DisplayColumn[];
|
||||
setColumnVisibility(uniqueName, isVisible) {
|
||||
@@ -49,4 +58,71 @@ export abstract class GridDisplay {
|
||||
get hiddenColumnIndexes() {
|
||||
return (this.config.hiddenColumns || []).map(x => _.findIndex(this.columns, y => y.uniqueName == x));
|
||||
}
|
||||
|
||||
enrichExpandedColumns(list: DisplayColumn[]): DisplayColumn[] {
|
||||
const res = [];
|
||||
for (const item of list) {
|
||||
res.push(item);
|
||||
if (this.isExpandedColumn(item.uniqueName)) res.push(...this.getExpandedColumns(item, item.uniqueName));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
getExpandedColumns(column: DisplayColumn, uniqueName: string) {
|
||||
const list = this.cache.subcolumns[uniqueName];
|
||||
if (list) {
|
||||
return this.enrichExpandedColumns(list);
|
||||
} else {
|
||||
// load expanded columns
|
||||
const { foreignKey } = column;
|
||||
this.getTableInfo({ schemaName: foreignKey.refSchemaName, pureName: foreignKey.refTableName }).then(table => {
|
||||
this.setCache({
|
||||
...this.cache,
|
||||
subcolumns: {
|
||||
...this.cache.subcolumns,
|
||||
[uniqueName]: this.getDisplayColumns(table, column.uniquePath),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getDisplayColumns(table: TableInfo, parentPath: string[]) {
|
||||
return table.columns.map(col => ({
|
||||
...col,
|
||||
pureName: table.pureName,
|
||||
schemaName: table.schemaName,
|
||||
headerText: col.columnName,
|
||||
uniqueName: [...parentPath, col.columnName].join(','),
|
||||
uniquePath: [...parentPath, col.columnName],
|
||||
isPrimaryKey: table.primaryKey && !!table.primaryKey.columns.find(x => x.columnName == col.columnName),
|
||||
foreignKey:
|
||||
table.foreignKeys &&
|
||||
table.foreignKeys.find(fk => fk.columns.length == 1 && fk.columns[0].columnName == col.columnName),
|
||||
isChecked: !(this.config.hiddenColumns && this.config.hiddenColumns.includes(col.columnName)),
|
||||
}));
|
||||
}
|
||||
|
||||
getColumns(columnFilter) {
|
||||
return this.enrichExpandedColumns(this.columns.filter(col => filterName(columnFilter, col.columnName)));
|
||||
}
|
||||
|
||||
isExpandedColumn(uniqueName: string) {
|
||||
return this.config.expandedColumns.includes(uniqueName);
|
||||
}
|
||||
|
||||
toggleExpandedColumn(uniqueName: string) {
|
||||
if (this.isExpandedColumn(uniqueName)) {
|
||||
this.setConfig({
|
||||
...this.config,
|
||||
expandedColumns: (this.config.expandedColumns || []).filter(x => x != uniqueName),
|
||||
});
|
||||
} else {
|
||||
this.setConfig({
|
||||
...this.config,
|
||||
expandedColumns: [...this.config.expandedColumns, uniqueName],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
import { GridDisplay } from './GridDisplay';
|
||||
import { Select, treeToSql, dumpSqlSelect } from '@dbgate/sqltree';
|
||||
import { TableInfo, EngineDriver } from '@dbgate/types';
|
||||
import { GridConfig } from './GridConfig';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
|
||||
export class TableGridDisplay extends GridDisplay {
|
||||
constructor(
|
||||
public table: TableInfo,
|
||||
public driver: EngineDriver,
|
||||
config: GridConfig,
|
||||
setConfig: (config: GridConfig) => void
|
||||
setConfig: (config: GridConfig) => void,
|
||||
cache: GridCache,
|
||||
setCache: (config: GridCache) => void,
|
||||
getTableInfo: ({ schemaName, pureName }) => Promise<TableInfo>
|
||||
) {
|
||||
super(config, setConfig);
|
||||
this.columns = table.columns.map(col => ({
|
||||
...col,
|
||||
headerText: col.columnName,
|
||||
uniqueName: col.columnName,
|
||||
uniquePath: [col.columnName],
|
||||
isPrimaryKey: table.primaryKey && !!table.primaryKey.columns.find(x => x.columnName == col.columnName),
|
||||
foreignKey:
|
||||
table.foreignKeys && table.foreignKeys.find(fk => fk.columns.find(x => x.columnName == col.columnName)),
|
||||
isChecked: !(config.hiddenColumns && config.hiddenColumns.includes(col.columnName)),
|
||||
}));
|
||||
super(config, setConfig, cache, setCache, getTableInfo);
|
||||
this.columns = this.getDisplayColumns(table, []);
|
||||
}
|
||||
|
||||
createSelect() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./GridDisplay";
|
||||
export * from "./GridConfig";
|
||||
export * from "./TableGridDisplay";
|
||||
export * from "./filterName";
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ColumnLabel from './ColumnLabel';
|
||||
import { filterName } from '@dbgate/datalib';
|
||||
import { FontIcon } from '../icons';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
|
||||
@@ -28,6 +29,45 @@ const Input = styled.input`
|
||||
width: 80px;
|
||||
`;
|
||||
|
||||
function ExpandIcon({ display, column, isHover, ...other }) {
|
||||
if (column.foreignKey) {
|
||||
return (
|
||||
<FontIcon
|
||||
icon={`far ${display.isExpandedColumn(column.uniqueName) ? 'fa-minus-square' : 'fa-plus-square'} `}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <FontIcon icon={`fas fa-square ${isHover ? 'lightblue' : 'white'}`} {...other} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} props
|
||||
* @param {import('@dbgate/datalib').GridDisplay} props.display
|
||||
* @param {import('@dbgate/datalib').DisplayColumn} props.column
|
||||
*/
|
||||
function ColumnManagerRow(props) {
|
||||
const { display, column } = props;
|
||||
const [isHover, setIsHover] = React.useState(false);
|
||||
return (
|
||||
<Row onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
|
||||
<ExpandIcon
|
||||
display={display}
|
||||
column={column}
|
||||
isHover={isHover}
|
||||
onClick={() => display.toggleExpandedColumn(column.uniqueName)}
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{ marginLeft: `${5 + (column.uniquePath.length - 1) * 10}px` }}
|
||||
checked={column.isChecked}
|
||||
onChange={() => display.setColumnVisibility(column.uniqueName, !column.isChecked)}
|
||||
></input>
|
||||
<ColumnLabel {...column} />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
export default function ColumnManager(props) {
|
||||
const { display } = props;
|
||||
@@ -39,17 +79,11 @@ export default function ColumnManager(props) {
|
||||
<Button onClick={() => display.hideAllColumns()}>Hide</Button>
|
||||
<Button onClick={() => display.showAllColumns()}>Show</Button>
|
||||
</SearchBoxWrapper>
|
||||
{display.columns
|
||||
.filter(col => filterName(columnFilter, col.columnName))
|
||||
.map(col => (
|
||||
<Row key={col.columnName}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={col.isChecked}
|
||||
onChange={() => display.setColumnVisibility(col.uniqueName, !col.isChecked)}
|
||||
></input>
|
||||
<ColumnLabel {...col} />
|
||||
</Row>
|
||||
{display
|
||||
.getColumns(columnFilter)
|
||||
.filter(column => filterName(columnFilter, column.columnName))
|
||||
.map(column => (
|
||||
<ColumnManagerRow key={column.uniqueName} display={display} column={column} />
|
||||
))}
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function DataGridCore(props) {
|
||||
// nextRows = [];
|
||||
// }
|
||||
const { rows: nextRows } = response.data;
|
||||
console.log('nextRows', nextRows);
|
||||
// console.log('nextRows', nextRows);
|
||||
const loadedInfo = {
|
||||
loadedRows: [...loadedRows, ...nextRows],
|
||||
loadedTime,
|
||||
@@ -142,7 +142,7 @@ export default function DataGridCore(props) {
|
||||
|
||||
const columnSizes = React.useMemo(() => countColumnSizes(), [loadedRows, containerWidth, display]);
|
||||
|
||||
console.log('containerWidth', containerWidth);
|
||||
// console.log('containerWidth', containerWidth);
|
||||
|
||||
const gridScrollAreaHeight = containerHeight - 2 * rowHeight;
|
||||
const gridScrollAreaWidth = containerWidth - columnSizes.frozenSize;
|
||||
@@ -177,13 +177,13 @@ export default function DataGridCore(props) {
|
||||
const columnSizes = new SeriesSizes();
|
||||
if (!loadedRows || !columns) return columnSizes;
|
||||
|
||||
console.log('countColumnSizes', loadedRows.length, containerWidth);
|
||||
// console.log('countColumnSizes', loadedRows.length, containerWidth);
|
||||
|
||||
columnSizes.maxSize = (containerWidth * 2) / 3;
|
||||
columnSizes.count = columns.length;
|
||||
|
||||
// columnSizes.setExtraordinaryIndexes(this.getHiddenColumnIndexes(), this.getFrozenColumnIndexes());
|
||||
console.log('display.hiddenColumnIndexes', display.hiddenColumnIndexes)
|
||||
// console.log('display.hiddenColumnIndexes', display.hiddenColumnIndexes)
|
||||
|
||||
columnSizes.setExtraordinaryIndexes(display.hiddenColumnIndexes, []);
|
||||
|
||||
@@ -240,7 +240,7 @@ export default function DataGridCore(props) {
|
||||
// console.log('containerHeight', containerHeight);
|
||||
|
||||
const visibleColumnCount = columnSizes.getVisibleScrollCount(firstVisibleColumnScrollIndex, gridScrollAreaWidth);
|
||||
console.log('visibleColumnCount', visibleColumnCount);
|
||||
// console.log('visibleColumnCount', visibleColumnCount);
|
||||
|
||||
const visibleRealColumnIndexes = [];
|
||||
const modelIndexes = {};
|
||||
@@ -274,7 +274,7 @@ export default function DataGridCore(props) {
|
||||
});
|
||||
}
|
||||
|
||||
console.log('visibleRealColumnIndexes', visibleRealColumnIndexes);
|
||||
// console.log('visibleRealColumnIndexes', visibleRealColumnIndexes);
|
||||
|
||||
return (
|
||||
<GridContainer ref={containerRef}>
|
||||
|
||||
@@ -15,28 +15,29 @@ export function getIconImage(src, props) {
|
||||
return <img width={size} height={size} src={src} style={style} className={className} title={title} />;
|
||||
}
|
||||
|
||||
export function getFontIcon(fontIconSpec, props = {}) {
|
||||
let iconClass = fontIconSpec;
|
||||
export function FontIcon({ icon, ...props }) {
|
||||
let iconClass = icon;
|
||||
if (!iconClass) return null;
|
||||
var parts = iconClass.split(' ');
|
||||
var name = parts[0];
|
||||
parts = parts.slice(1);
|
||||
let parts = iconClass.split(' ');
|
||||
const type = parts[0];
|
||||
const name = parts[1];
|
||||
parts = parts.slice(2);
|
||||
|
||||
var className = props.className || '';
|
||||
let className = props.className || '';
|
||||
|
||||
// if (_.startsWith(name, 'bs-')) className += ` glyphicon glyphicon-${name.substr(3)}`;
|
||||
if (_.startsWith(name, 'fa-')) className += ` fas fa-${name.substr(3)}`;
|
||||
if (type == 'fas' || type == 'far') className += `${type} ${name}`;
|
||||
|
||||
if (_.includes(parts, 'spin')) className += ' fa-spin';
|
||||
|
||||
var style = props.style || {};
|
||||
const style = props.style || {};
|
||||
|
||||
var last = parts[parts.length - 1];
|
||||
const last = parts[parts.length - 1];
|
||||
if (last && last != 'spin') {
|
||||
style['color'] = last;
|
||||
}
|
||||
|
||||
return <i className={className} style={style} title={props.title} />;
|
||||
return <i {...props} className={className} style={style} title={props.title} />;
|
||||
}
|
||||
|
||||
export const TableIcon = props => getIconImage('table2.svg', props);
|
||||
@@ -85,13 +86,11 @@ 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 TimesRedIcon = props => <FontIcon name='fas fa-times red' {...props} />;
|
||||
export const TimesGreenCircleIcon = props => <FontIcon icon='fas fa-times-circle green' {...props} />;
|
||||
export const GrayFilterIcon = props => <FontIcon icon='fas fa-filter lightgray' {...props} />;
|
||||
export const ExclamationTriangleIcon = props => <FontIcon icon='fas fa-exclamation-triangle' {...props} />;
|
||||
export const HourGlassIcon = props => <FontIcon icon='fas fa-hourglass' {...props} />;
|
||||
export const InfoBlueCircleIcon = props => <FontIcon icon='fas fa-info-circle blue' {...props} />;
|
||||
|
||||
export const SpinnerIcon = props => getFontIcon('fa-spinner spin', props);
|
||||
|
||||
export const FontIcon = ({ name }) => <i className={`fas ${name}`}></i>;
|
||||
export const SpinnerIcon = props => <FontIcon icon='fas fa-spinner spin' {...props} />;
|
||||
|
||||
@@ -3,17 +3,21 @@ import useFetch from '../utility/useFetch';
|
||||
import styled from 'styled-components';
|
||||
import theme from '../theme';
|
||||
import DataGrid from '../datagrid/DataGrid';
|
||||
import { TableGridDisplay } from '@dbgate/datalib';
|
||||
import { TableGridDisplay, createGridConfig, createGridCache } from '@dbgate/datalib';
|
||||
import useTableInfo from '../utility/useTableInfo';
|
||||
import useConnectionInfo from '../utility/useConnectionInfo';
|
||||
import engines from '@dbgate/engines';
|
||||
import getTableInfo from '../utility/getTableInfo';
|
||||
|
||||
export default function TableDataTab({ conid, database, schemaName, pureName }) {
|
||||
const tableInfo = useTableInfo({ conid, database, schemaName, pureName });
|
||||
const [config, setConfig] = React.useState({ hiddenColumns: [] });
|
||||
const [config, setConfig] = React.useState(createGridConfig());
|
||||
const [cache, setCache] = React.useState(createGridCache());
|
||||
const connection = useConnectionInfo(conid);
|
||||
if (!tableInfo || !connection) return null;
|
||||
const display = new TableGridDisplay(tableInfo, engines(connection), config, setConfig);
|
||||
const display = new TableGridDisplay(tableInfo, engines(connection), config, setConfig, cache, setCache, name =>
|
||||
getTableInfo({ conid, database, ...name })
|
||||
);
|
||||
return (
|
||||
<DataGrid
|
||||
// key={`${conid}, ${database}, ${schemaName}, ${pureName}`}
|
||||
|
||||
12
packages/web/src/utility/getTableInfo.js
Normal file
12
packages/web/src/utility/getTableInfo.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import axios from './axios';
|
||||
|
||||
export default async function getTableInfo({ conid, database, schemaName, pureName }) {
|
||||
const resp = await axios.request({
|
||||
method: 'get',
|
||||
url: 'tables/table-info',
|
||||
params: { conid, database, schemaName, pureName },
|
||||
});
|
||||
/** @type {import('@dbgate/types').TableInfo} */
|
||||
const res = resp.data;
|
||||
return res;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
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`
|
||||
@@ -55,7 +54,7 @@ export default function WidgetIconPanel() {
|
||||
isSelected={name === currentWidget}
|
||||
onClick={() => setCurrentWidget(name === currentWidget ? null : name)}
|
||||
>
|
||||
<FontIcon name={icon} />
|
||||
<i className={`fas ${icon}`}/>
|
||||
</IconWrapper>
|
||||
))}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user