expandable FK columns

This commit is contained in:
Jan Prochazka
2020-03-07 22:39:19 +01:00
parent d64ae4b688
commit d902d9de1c
10 changed files with 196 additions and 57 deletions

View File

@@ -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: {},
};
}

View File

@@ -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],
});
}
}
}

View File

@@ -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() {

View File

@@ -1,3 +1,4 @@
export * from "./GridDisplay";
export * from "./GridConfig";
export * from "./TableGridDisplay";
export * from "./filterName";

View File

@@ -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>
);

View File

@@ -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}>

View File

@@ -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} />;

View File

@@ -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}`}

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

View File

@@ -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>
))}
</>