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 { export interface GridConfig {
hiddenColumns: string[]; 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 _ from 'lodash';
import { GridConfig } from './GridConfig'; import { GridConfig, GridCache } from './GridConfig';
import { ForeignKeyInfo } from '@dbgate/types'; import { ForeignKeyInfo, TableInfo } from '@dbgate/types';
import { filterName } from './filterName';
export interface DisplayColumn { export interface DisplayColumn {
schemaName: string;
pureName: string;
columnName: string; columnName: string;
headerText: string; headerText: string;
uniqueName: string; uniqueName: string;
@@ -15,7 +18,13 @@ export interface DisplayColumn {
} }
export abstract class GridDisplay { 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; abstract getPageQuery(offset: number, count: number): string;
columns: DisplayColumn[]; columns: DisplayColumn[];
setColumnVisibility(uniqueName, isVisible) { setColumnVisibility(uniqueName, isVisible) {
@@ -49,4 +58,71 @@ export abstract class GridDisplay {
get hiddenColumnIndexes() { get hiddenColumnIndexes() {
return (this.config.hiddenColumns || []).map(x => _.findIndex(this.columns, y => y.uniqueName == x)); 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 { GridDisplay } from './GridDisplay';
import { Select, treeToSql, dumpSqlSelect } from '@dbgate/sqltree'; import { Select, treeToSql, dumpSqlSelect } from '@dbgate/sqltree';
import { TableInfo, EngineDriver } from '@dbgate/types'; import { TableInfo, EngineDriver } from '@dbgate/types';
import { GridConfig } from './GridConfig'; import { GridConfig, GridCache } from './GridConfig';
export class TableGridDisplay extends GridDisplay { export class TableGridDisplay extends GridDisplay {
constructor( constructor(
public table: TableInfo, public table: TableInfo,
public driver: EngineDriver, public driver: EngineDriver,
config: GridConfig, config: GridConfig,
setConfig: (config: GridConfig) => void setConfig: (config: GridConfig) => void,
cache: GridCache,
setCache: (config: GridCache) => void,
getTableInfo: ({ schemaName, pureName }) => Promise<TableInfo>
) { ) {
super(config, setConfig); super(config, setConfig, cache, setCache, getTableInfo);
this.columns = table.columns.map(col => ({ this.columns = this.getDisplayColumns(table, []);
...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)),
}));
} }
createSelect() { createSelect() {

View File

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

View File

@@ -2,6 +2,7 @@ import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import ColumnLabel from './ColumnLabel'; import ColumnLabel from './ColumnLabel';
import { filterName } from '@dbgate/datalib'; import { filterName } from '@dbgate/datalib';
import { FontIcon } from '../icons';
const Wrapper = styled.div``; const Wrapper = styled.div``;
@@ -28,6 +29,45 @@ const Input = styled.input`
width: 80px; 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} */ /** @param props {import('./types').DataGridProps} */
export default function ColumnManager(props) { export default function ColumnManager(props) {
const { display } = props; const { display } = props;
@@ -39,17 +79,11 @@ export default function ColumnManager(props) {
<Button onClick={() => display.hideAllColumns()}>Hide</Button> <Button onClick={() => display.hideAllColumns()}>Hide</Button>
<Button onClick={() => display.showAllColumns()}>Show</Button> <Button onClick={() => display.showAllColumns()}>Show</Button>
</SearchBoxWrapper> </SearchBoxWrapper>
{display.columns {display
.filter(col => filterName(columnFilter, col.columnName)) .getColumns(columnFilter)
.map(col => ( .filter(column => filterName(columnFilter, column.columnName))
<Row key={col.columnName}> .map(column => (
<input <ColumnManagerRow key={column.uniqueName} display={display} column={column} />
type="checkbox"
checked={col.isChecked}
onChange={() => display.setColumnVisibility(col.uniqueName, !col.isChecked)}
></input>
<ColumnLabel {...col} />
</Row>
))} ))}
</Wrapper> </Wrapper>
); );

View File

@@ -110,7 +110,7 @@ export default function DataGridCore(props) {
// nextRows = []; // nextRows = [];
// } // }
const { rows: nextRows } = response.data; const { rows: nextRows } = response.data;
console.log('nextRows', nextRows); // console.log('nextRows', nextRows);
const loadedInfo = { const loadedInfo = {
loadedRows: [...loadedRows, ...nextRows], loadedRows: [...loadedRows, ...nextRows],
loadedTime, loadedTime,
@@ -142,7 +142,7 @@ export default function DataGridCore(props) {
const columnSizes = React.useMemo(() => countColumnSizes(), [loadedRows, containerWidth, display]); const columnSizes = React.useMemo(() => countColumnSizes(), [loadedRows, containerWidth, display]);
console.log('containerWidth', containerWidth); // console.log('containerWidth', containerWidth);
const gridScrollAreaHeight = containerHeight - 2 * rowHeight; const gridScrollAreaHeight = containerHeight - 2 * rowHeight;
const gridScrollAreaWidth = containerWidth - columnSizes.frozenSize; const gridScrollAreaWidth = containerWidth - columnSizes.frozenSize;
@@ -177,13 +177,13 @@ export default function DataGridCore(props) {
const columnSizes = new SeriesSizes(); const columnSizes = new SeriesSizes();
if (!loadedRows || !columns) return columnSizes; if (!loadedRows || !columns) return columnSizes;
console.log('countColumnSizes', loadedRows.length, containerWidth); // console.log('countColumnSizes', loadedRows.length, containerWidth);
columnSizes.maxSize = (containerWidth * 2) / 3; columnSizes.maxSize = (containerWidth * 2) / 3;
columnSizes.count = columns.length; columnSizes.count = columns.length;
// columnSizes.setExtraordinaryIndexes(this.getHiddenColumnIndexes(), this.getFrozenColumnIndexes()); // columnSizes.setExtraordinaryIndexes(this.getHiddenColumnIndexes(), this.getFrozenColumnIndexes());
console.log('display.hiddenColumnIndexes', display.hiddenColumnIndexes) // console.log('display.hiddenColumnIndexes', display.hiddenColumnIndexes)
columnSizes.setExtraordinaryIndexes(display.hiddenColumnIndexes, []); columnSizes.setExtraordinaryIndexes(display.hiddenColumnIndexes, []);
@@ -240,7 +240,7 @@ export default function DataGridCore(props) {
// console.log('containerHeight', containerHeight); // console.log('containerHeight', containerHeight);
const visibleColumnCount = columnSizes.getVisibleScrollCount(firstVisibleColumnScrollIndex, gridScrollAreaWidth); const visibleColumnCount = columnSizes.getVisibleScrollCount(firstVisibleColumnScrollIndex, gridScrollAreaWidth);
console.log('visibleColumnCount', visibleColumnCount); // console.log('visibleColumnCount', visibleColumnCount);
const visibleRealColumnIndexes = []; const visibleRealColumnIndexes = [];
const modelIndexes = {}; const modelIndexes = {};
@@ -274,7 +274,7 @@ export default function DataGridCore(props) {
}); });
} }
console.log('visibleRealColumnIndexes', visibleRealColumnIndexes); // console.log('visibleRealColumnIndexes', visibleRealColumnIndexes);
return ( return (
<GridContainer ref={containerRef}> <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} />; return <img width={size} height={size} src={src} style={style} className={className} title={title} />;
} }
export function getFontIcon(fontIconSpec, props = {}) { export function FontIcon({ icon, ...props }) {
let iconClass = fontIconSpec; let iconClass = icon;
if (!iconClass) return null; if (!iconClass) return null;
var parts = iconClass.split(' '); let parts = iconClass.split(' ');
var name = parts[0]; const type = parts[0];
parts = parts.slice(1); 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, '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'; 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') { if (last && last != 'spin') {
style['color'] = last; 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); 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 EmptyIcon = props => getIconImage('data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=', props);
export const TimesRedIcon = props => getFontIcon('fa-times red', props); export const TimesRedIcon = props => <FontIcon name='fas fa-times red' {...props} />;
export const TimesGreenCircleIcon = props => getFontIcon('fa-times-circle green', props); export const TimesGreenCircleIcon = props => <FontIcon icon='fas fa-times-circle green' {...props} />;
export const GrayFilterIcon = props => getFontIcon('fa-filter lightgray', props); export const GrayFilterIcon = props => <FontIcon icon='fas fa-filter lightgray' {...props} />;
export const ExclamationTriangleIcon = props => getFontIcon('fa-exclamation-triangle', props); export const ExclamationTriangleIcon = props => <FontIcon icon='fas fa-exclamation-triangle' {...props} />;
export const HourGlassIcon = props => getFontIcon('fa-hourglass', props); export const HourGlassIcon = props => <FontIcon icon='fas fa-hourglass' {...props} />;
export const InfoBlueCircleIcon = props => getFontIcon('fa-info-circle blue', props); export const InfoBlueCircleIcon = props => <FontIcon icon='fas fa-info-circle blue' {...props} />;
export const SpinnerIcon = props => getFontIcon('fa-spinner spin', props); export const SpinnerIcon = props => <FontIcon icon='fas fa-spinner spin' {...props} />;
export const FontIcon = ({ name }) => <i className={`fas ${name}`}></i>;

View File

@@ -3,17 +3,21 @@ 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'; import DataGrid from '../datagrid/DataGrid';
import { TableGridDisplay } from '@dbgate/datalib'; import { TableGridDisplay, createGridConfig, createGridCache } from '@dbgate/datalib';
import useTableInfo from '../utility/useTableInfo'; import useTableInfo from '../utility/useTableInfo';
import useConnectionInfo from '../utility/useConnectionInfo'; import useConnectionInfo from '../utility/useConnectionInfo';
import engines from '@dbgate/engines'; import engines from '@dbgate/engines';
import getTableInfo from '../utility/getTableInfo';
export default function TableDataTab({ conid, database, schemaName, pureName }) { export default function TableDataTab({ conid, database, schemaName, pureName }) {
const tableInfo = useTableInfo({ 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); const connection = useConnectionInfo(conid);
if (!tableInfo || !connection) return null; 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 ( return (
<DataGrid <DataGrid
// key={`${conid}, ${database}, ${schemaName}, ${pureName}`} // 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 React from 'react';
import theme from '../theme'; import theme from '../theme';
import styled from 'styled-components'; import styled from 'styled-components';
import { FontIcon } from '../icons';
import { useCurrentWidget, useSetCurrentWidget } from '../utility/globalState'; import { useCurrentWidget, useSetCurrentWidget } from '../utility/globalState';
const IconWrapper = styled.div` const IconWrapper = styled.div`
@@ -55,7 +54,7 @@ export default function WidgetIconPanel() {
isSelected={name === currentWidget} isSelected={name === currentWidget}
onClick={() => setCurrentWidget(name === currentWidget ? null : name)} onClick={() => setCurrentWidget(name === currentWidget ? null : name)}
> >
<FontIcon name={icon} /> <i className={`fas ${icon}`}/>
</IconWrapper> </IconWrapper>
))} ))}
</> </>