From d902d9de1cc62bc016c075fb6a7789ab60c5a33e Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 7 Mar 2020 22:39:19 +0100 Subject: [PATCH] expandable FK columns --- packages/datalib/src/GridConfig.ts | 20 +++++ packages/datalib/src/GridDisplay.ts | 82 ++++++++++++++++++++- packages/datalib/src/TableGridDisplay.ts | 20 ++--- packages/datalib/src/index.ts | 1 + packages/web/src/datagrid/ColumnManager.js | 56 +++++++++++--- packages/web/src/datagrid/DataGridCore.js | 12 +-- packages/web/src/icons.js | 37 +++++----- packages/web/src/tabs/TableDataTab.js | 10 ++- packages/web/src/utility/getTableInfo.js | 12 +++ packages/web/src/widgets/WidgetIconPanel.js | 3 +- 10 files changed, 196 insertions(+), 57 deletions(-) create mode 100644 packages/web/src/utility/getTableInfo.js diff --git a/packages/datalib/src/GridConfig.ts b/packages/datalib/src/GridConfig.ts index d6faca21c..7620fe778 100644 --- a/packages/datalib/src/GridConfig.ts +++ b/packages/datalib/src/GridConfig.ts @@ -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: {}, + }; } diff --git a/packages/datalib/src/GridDisplay.ts b/packages/datalib/src/GridDisplay.ts index 075c1f06e..3b1745bb0 100644 --- a/packages/datalib/src/GridDisplay.ts +++ b/packages/datalib/src/GridDisplay.ts @@ -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 + ) {} 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], + }); + } + } } diff --git a/packages/datalib/src/TableGridDisplay.ts b/packages/datalib/src/TableGridDisplay.ts index 9d2e6e730..8beeb4365 100644 --- a/packages/datalib/src/TableGridDisplay.ts +++ b/packages/datalib/src/TableGridDisplay.ts @@ -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 ) { - 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() { diff --git a/packages/datalib/src/index.ts b/packages/datalib/src/index.ts index 547ce09aa..a37b6f16d 100644 --- a/packages/datalib/src/index.ts +++ b/packages/datalib/src/index.ts @@ -1,3 +1,4 @@ export * from "./GridDisplay"; +export * from "./GridConfig"; export * from "./TableGridDisplay"; export * from "./filterName"; diff --git a/packages/web/src/datagrid/ColumnManager.js b/packages/web/src/datagrid/ColumnManager.js index 54ad2980c..361e77f92 100644 --- a/packages/web/src/datagrid/ColumnManager.js +++ b/packages/web/src/datagrid/ColumnManager.js @@ -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 ( + + ); + } + return ; +} + +/** + * @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 ( + setIsHover(true)} onMouseLeave={() => setIsHover(false)}> + display.toggleExpandedColumn(column.uniqueName)} + /> + display.setColumnVisibility(column.uniqueName, !column.isChecked)} + > + + + ); +} + /** @param props {import('./types').DataGridProps} */ export default function ColumnManager(props) { const { display } = props; @@ -39,17 +79,11 @@ export default function ColumnManager(props) { - {display.columns - .filter(col => filterName(columnFilter, col.columnName)) - .map(col => ( - - display.setColumnVisibility(col.uniqueName, !col.isChecked)} - > - - + {display + .getColumns(columnFilter) + .filter(column => filterName(columnFilter, column.columnName)) + .map(column => ( + ))} ); diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js index 471074bb5..f2acb762a 100644 --- a/packages/web/src/datagrid/DataGridCore.js +++ b/packages/web/src/datagrid/DataGridCore.js @@ -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 ( diff --git a/packages/web/src/icons.js b/packages/web/src/icons.js index 6b4fd7840..25bf7e57f 100644 --- a/packages/web/src/icons.js +++ b/packages/web/src/icons.js @@ -15,28 +15,29 @@ export function getIconImage(src, props) { return ; } -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 ; + return ; } 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 => ; +export const TimesGreenCircleIcon = props => ; +export const GrayFilterIcon = props => ; +export const ExclamationTriangleIcon = props => ; +export const HourGlassIcon = props => ; +export const InfoBlueCircleIcon = props => ; -export const SpinnerIcon = props => getFontIcon('fa-spinner spin', props); - -export const FontIcon = ({ name }) => ; +export const SpinnerIcon = props => ; diff --git a/packages/web/src/tabs/TableDataTab.js b/packages/web/src/tabs/TableDataTab.js index 9b23dc153..ebf9433fb 100644 --- a/packages/web/src/tabs/TableDataTab.js +++ b/packages/web/src/tabs/TableDataTab.js @@ -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 ( setCurrentWidget(name === currentWidget ? null : name)} > - + ))}