From 0e1e3b9ed72fa9a94a7e86db6a1e4562b19b0165 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sun, 21 Jun 2020 10:36:43 +0200 Subject: [PATCH] grid - grouping --- packages/api/src/shell/tableReader.js | 4 +- packages/datalib/src/GridConfig.ts | 4 + packages/datalib/src/GridDisplay.ts | 83 +++++++++++++++++-- packages/datalib/src/TableGridDisplay.ts | 8 +- packages/sqltree/src/dumpSqlExpression.ts | 6 ++ packages/sqltree/src/types.ts | 9 +- .../web/src/datagrid/ColumnHeaderControl.js | 19 ++++- packages/web/src/datagrid/DataGridCore.js | 22 +++++ packages/web/src/datagrid/TableDataGrid.js | 9 +- 9 files changed, 148 insertions(+), 16 deletions(-) diff --git a/packages/api/src/shell/tableReader.js b/packages/api/src/shell/tableReader.js index 0660f5f1..6396675a 100644 --- a/packages/api/src/shell/tableReader.js +++ b/packages/api/src/shell/tableReader.js @@ -13,12 +13,12 @@ async function queryReader({ connection, pureName, schemaName }) { const table = await driver.analyseSingleObject(pool, fullName, 'tables'); const query = `select * from ${quoteFullName(driver.dialect, fullName)}`; if (table) { - console.log(`Reading table ${table}`); + console.log(`Reading table ${table.pureName}`); return await driver.readQuery(pool, query, table); } const view = await driver.analyseSingleObject(pool, fullName, 'views'); if (view) { - console.log(`Reading view ${table}`); + console.log(`Reading view ${view.pureName}`); return await driver.readQuery(pool, query, view); } diff --git a/packages/datalib/src/GridConfig.ts b/packages/datalib/src/GridConfig.ts index 3f054a7e..ec98d5f9 100644 --- a/packages/datalib/src/GridConfig.ts +++ b/packages/datalib/src/GridConfig.ts @@ -16,6 +16,8 @@ export interface GridReferenceDefinition { }[]; } +export type GroupFunc = 'GROUP' | 'MAX' | 'MIN' | 'SUM' | 'AVG' | 'COUNT' | 'COUNT DISTINCT' + export interface GridConfig extends GridConfigColumns { filters: { [uniqueName: string]: string }; focusedColumn?: string; @@ -24,6 +26,7 @@ export interface GridConfig extends GridConfigColumns { uniqueName: string; order: 'ASC' | 'DESC'; }[]; + grouping: { [uniqueName: string]: GroupFunc }; } export interface GridCache { @@ -39,6 +42,7 @@ export function createGridConfig(): GridConfig { columnWidths: {}, sort: [], focusedColumn: null, + grouping: {}, }; } diff --git a/packages/datalib/src/GridDisplay.ts b/packages/datalib/src/GridDisplay.ts index 36268972..2fd885a4 100644 --- a/packages/datalib/src/GridDisplay.ts +++ b/packages/datalib/src/GridDisplay.ts @@ -1,10 +1,11 @@ import _ from 'lodash'; -import { GridConfig, GridCache, GridConfigColumns, createGridCache } from './GridConfig'; +import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig'; import { ForeignKeyInfo, TableInfo, ColumnInfo, EngineDriver, NamedObjectInfo, DatabaseInfo } from '@dbgate/types'; import { parseFilter, getFilterType } from '@dbgate/filterparser'; import { filterName } from './filterName'; import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet'; import { Expression, Select, treeToSql, dumpSqlSelect } from '@dbgate/sqltree'; +import { group } from 'console'; export interface DisplayColumn { schemaName: string; @@ -174,16 +175,59 @@ export abstract class GridDisplay { if (this.config.sort?.length > 0) { select.orderBy = this.config.sort .map((col) => ({ ...col, dispInfo: displayedColumnInfo[col.uniqueName] })) - .filter((col) => col.dispInfo) + .map((col) => ({ ...col, expr: select.columns.find((x) => x.alias == col.uniqueName) })) + .filter((col) => col.dispInfo && col.expr) .map((col) => ({ - exprType: 'column', - columnName: col.dispInfo.columnName, + ...col.expr, direction: col.order, - source: { alias: col.dispInfo.sourceAlias }, })); } } + get isGrouped() { + return !_.isEmpty(this.config.grouping); + } + + get groupColumns() { + return this.isGrouped ? _.keys(_.pickBy(this.config.grouping, (v) => v == 'GROUP')) : null; + } + + applyGroupOnSelect(select: Select, displayedColumnInfo: DisplayedColumnInfo) { + const groupColumns = this.groupColumns; + if (groupColumns && groupColumns.length > 0) { + select.groupBy = groupColumns.map((col) => ({ + exprType: 'column', + columnName: displayedColumnInfo[col].columnName, + source: { alias: displayedColumnInfo[col].sourceAlias }, + })); + } + if (!_.isEmpty(this.config.grouping)) { + for (let i = 0; i < select.columns.length; i++) { + const uniqueName = select.columns[i].alias; + if (groupColumns && groupColumns.includes(uniqueName)) continue; + const grouping = this.getGrouping(uniqueName); + let func = 'MAX'; + let argsPrefix = ''; + if (grouping) { + if (grouping == 'COUNT DISTINCT') { + func = 'COUNT'; + argsPrefix = 'DISTINCT '; + } else { + func = grouping; + } + } + select.columns[i] = { + alias: select.columns[i].alias, + exprType: 'call', + func, + argsPrefix, + args: [select.columns[i]], + }; + } + select.columns = select.columns.filter((x) => x.alias); + } + } + getColumns(columnFilter) { return this.columns.filter((col) => filterName(columnFilter, col.columnName)); } @@ -223,6 +267,34 @@ export abstract class GridDisplay { this.reload(); } + setGrouping(uniqueName, groupFunc: GroupFunc) { + this.setConfig((cfg) => ({ + ...cfg, + grouping: groupFunc + ? { + ...cfg.grouping, + [uniqueName]: groupFunc, + } + : _.omitBy(cfg.grouping, (v, k) => k == uniqueName), + })); + this.reload(); + } + + getGrouping(uniqueName): GroupFunc { + if (this.isGrouped) { + return this.config.grouping[uniqueName] || 'MAX'; + } + return null; + } + + clearGrouping() { + this.setConfig((cfg) => ({ + ...cfg, + grouping: {}, + })); + this.reload(); + } + getSortOrder(uniqueName) { return this.config.sort.find((x) => x.uniqueName == uniqueName)?.order; } @@ -298,6 +370,7 @@ export abstract class GridDisplay { ); this.processReferences(select, displayedColumnInfo); this.applyFilterOnSelect(select, displayedColumnInfo); + this.applyGroupOnSelect(select, displayedColumnInfo); this.applySortOnSelect(select, displayedColumnInfo); return select; } diff --git a/packages/datalib/src/TableGridDisplay.ts b/packages/datalib/src/TableGridDisplay.ts index 62ab1f15..7d2e68ab 100644 --- a/packages/datalib/src/TableGridDisplay.ts +++ b/packages/datalib/src/TableGridDisplay.ts @@ -70,8 +70,8 @@ export class TableGridDisplay extends GridDisplay { this.addReferenceToSelect(select, parentAlias, column); - this.addJoinsFromExpandedColumns(select, subcolumns, childAlias, columnSources) - this.addAddedColumnsToSelect(select, subcolumns, childAlias, columnSources) + this.addJoinsFromExpandedColumns(select, subcolumns, childAlias, columnSources); + this.addAddedColumnsToSelect(select, subcolumns, childAlias, columnSources); } } } @@ -111,8 +111,12 @@ export class TableGridDisplay extends GridDisplay { addHintsToSelect(select: Select): boolean { let res = false; + const groupColumns = this.groupColumns; for (const column of this.getGridColumns()) { if (column.foreignKey) { + if (groupColumns && !groupColumns.includes(column.uniqueName)) { + continue; + } const table = this.getFkTarget(column); if (table && table.columns && table.columns.length > 0) { const hintColumn = table.columns.find((x) => x?.dataType?.toLowerCase()?.includes('char')); diff --git a/packages/sqltree/src/dumpSqlExpression.ts b/packages/sqltree/src/dumpSqlExpression.ts index 337c7908..aa385b6e 100644 --- a/packages/sqltree/src/dumpSqlExpression.ts +++ b/packages/sqltree/src/dumpSqlExpression.ts @@ -27,5 +27,11 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) { case 'raw': dmp.put('%s', expr.sql); break; + + case 'call': + dmp.put('%s(%s', expr.func, expr.argsPrefix); + dmp.putCollection(',', expr.args, (x) => dumpSqlExpression(dmp, x)); + dmp.put(')'); + break; } } diff --git a/packages/sqltree/src/types.ts b/packages/sqltree/src/types.ts index 02f8d5de..683a5749 100644 --- a/packages/sqltree/src/types.ts +++ b/packages/sqltree/src/types.ts @@ -123,7 +123,14 @@ export interface RawExpression { sql: string; } -export type Expression = ColumnRefExpression | ValueExpression | PlaceholderExpression | RawExpression; +export interface CallExpression { + exprType: 'call'; + func: string; + args: Expression[]; + argsPrefix?: string; // DISTINCT in case of COUNT DISTINCT +} + +export type Expression = ColumnRefExpression | ValueExpression | PlaceholderExpression | RawExpression | CallExpression; export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' }; export type ResultField = Expression & { alias?: string }; diff --git a/packages/web/src/datagrid/ColumnHeaderControl.js b/packages/web/src/datagrid/ColumnHeaderControl.js index 28a659fe..78eb3085 100644 --- a/packages/web/src/datagrid/ColumnHeaderControl.js +++ b/packages/web/src/datagrid/ColumnHeaderControl.js @@ -2,7 +2,7 @@ import React from 'react'; import styled from 'styled-components'; import ColumnLabel from './ColumnLabel'; import DropDownButton from '../widgets/DropDownButton'; -import { DropDownMenuItem } from '../modals/DropDownMenu'; +import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu'; import { useSplitterDrag } from '../widgets/Splitter'; import { FontIcon } from '../icons'; @@ -31,11 +31,18 @@ const ResizeHandle = styled.div` z-index: 1; `; -export default function ColumnHeaderControl({ column, setSort, onResize, order }) { +const GroupingLabel = styled.span` + color: green; + white-space: nowrap; +`; + +export default function ColumnHeaderControl({ column, setSort, onResize, order, setGrouping, grouping }) { const onResizeDown = useSplitterDrag('clientX', onResize); return ( + {grouping && {grouping.toLowerCase()}:} + {order == 'ASC' && ( @@ -52,6 +59,14 @@ export default function ColumnHeaderControl({ column, setSort, onResize, order } setSort('ASC')}>Sort ascending setSort('DESC')}>Sort descending + + setGrouping('GROUP')}>Group by + setGrouping('MAX')}>MAX + setGrouping('MIN')}>MIN + setGrouping('SUM')}>SUM + setGrouping('AVG')}>AVG + setGrouping('COUNT')}>COUNT + setGrouping('COUNT DISTINCT')}>COUNT DISTINCT )} diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js index 56d8e083..0b6b2963 100644 --- a/packages/web/src/datagrid/DataGridCore.js +++ b/packages/web/src/datagrid/DataGridCore.js @@ -6,6 +6,7 @@ import { HorizontalScrollBar, VerticalScrollBar } from './ScrollBars'; import useDimensions from '../utility/useDimensions'; import axios from '../utility/axios'; import DataFilterControl from './DataFilterControl'; +import stableStringify from 'json-stable-stringify'; import { getFilterType } from '@dbgate/filterparser'; import { cellFromEvent, getCellRange, topLeftCell, isRegularCell, nullCell, emptyCellArray } from './selection'; import keycodes from '../utility/keycodes'; @@ -481,6 +482,21 @@ export default function DataGridCore(props) { } }, [display && display.focusedColumn]); + React.useEffect(() => { + if (display.groupColumns) { + console.log('SET REFERENCE'); + + props.onReferenceClick({ + schemaName: display.baseTable.schemaName, + pureName: display.baseTable.pureName, + columns: display.groupColumns.map((col) => ({ + baseName: col, + refName: col, + })), + }); + } + }, [stableStringify(display && display.groupColumns)]); + const rowCountInfo = React.useMemo(() => { if (selectedCells.length > 1 && selectedCells.every((x) => _.isNumber(x[0]) && _.isNumber(x[1]))) { let sum = _.sumBy(selectedCells, (cell) => { @@ -1101,6 +1117,10 @@ export default function DataGridCore(props) { } } + function setGrouping(uniqueName, groupFunc) { + display.setGrouping(uniqueName, groupFunc); + } + // console.log('visibleRowCountUpperBound', visibleRowCountUpperBound); // console.log('gridScrollAreaHeight', gridScrollAreaHeight); // console.log('containerHeight', containerHeight); @@ -1155,6 +1175,8 @@ export default function DataGridCore(props) { setSort={display.sortable ? (order) => display.setSort(col.uniqueName, order) : null} order={display.getSortOrder(col.uniqueName)} onResize={(diff) => display.resizeColumn(col.uniqueName, col.widthNumber, diff)} + setGrouping={display.sortable ? (groupFunc) => setGrouping(col.uniqueName, groupFunc) : null} + grouping={display.getGrouping(col.uniqueName)} /> ))} diff --git a/packages/web/src/datagrid/TableDataGrid.js b/packages/web/src/datagrid/TableDataGrid.js index acfcc812..dd70a186 100644 --- a/packages/web/src/datagrid/TableDataGrid.js +++ b/packages/web/src/datagrid/TableDataGrid.js @@ -53,10 +53,6 @@ export default function TableDataGrid({ const dbinfo = useDatabaseInfo({ conid, database }); const [reference, setReference] = React.useState(null); - React.useEffect(() => { - setRefReloadToken((v) => v + 1); - }, [reference]); - function createDisplay() { return connection ? new TableGridDisplay( @@ -73,6 +69,11 @@ export default function TableDataGrid({ const [display, setDisplay] = React.useState(createDisplay()); + React.useEffect(() => { + setRefReloadToken((v) => v + 1); + if (!reference && display && display.isGrouped) display.clearGrouping(); + }, [reference]); + React.useEffect(() => { const newDisplay = createDisplay(); if (display && display.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return;