grid - grouping

This commit is contained in:
Jan Prochazka 2020-06-21 10:36:43 +02:00
parent 425e58627f
commit 0e1e3b9ed7
9 changed files with 148 additions and 16 deletions

View File

@ -13,12 +13,12 @@ async function queryReader({ connection, pureName, schemaName }) {
const table = await driver.analyseSingleObject(pool, fullName, 'tables'); const table = await driver.analyseSingleObject(pool, fullName, 'tables');
const query = `select * from ${quoteFullName(driver.dialect, fullName)}`; const query = `select * from ${quoteFullName(driver.dialect, fullName)}`;
if (table) { if (table) {
console.log(`Reading table ${table}`); console.log(`Reading table ${table.pureName}`);
return await driver.readQuery(pool, query, table); return await driver.readQuery(pool, query, table);
} }
const view = await driver.analyseSingleObject(pool, fullName, 'views'); const view = await driver.analyseSingleObject(pool, fullName, 'views');
if (view) { if (view) {
console.log(`Reading view ${table}`); console.log(`Reading view ${view.pureName}`);
return await driver.readQuery(pool, query, view); return await driver.readQuery(pool, query, view);
} }

View File

@ -16,6 +16,8 @@ export interface GridReferenceDefinition {
}[]; }[];
} }
export type GroupFunc = 'GROUP' | 'MAX' | 'MIN' | 'SUM' | 'AVG' | 'COUNT' | 'COUNT DISTINCT'
export interface GridConfig extends GridConfigColumns { export interface GridConfig extends GridConfigColumns {
filters: { [uniqueName: string]: string }; filters: { [uniqueName: string]: string };
focusedColumn?: string; focusedColumn?: string;
@ -24,6 +26,7 @@ export interface GridConfig extends GridConfigColumns {
uniqueName: string; uniqueName: string;
order: 'ASC' | 'DESC'; order: 'ASC' | 'DESC';
}[]; }[];
grouping: { [uniqueName: string]: GroupFunc };
} }
export interface GridCache { export interface GridCache {
@ -39,6 +42,7 @@ export function createGridConfig(): GridConfig {
columnWidths: {}, columnWidths: {},
sort: [], sort: [],
focusedColumn: null, focusedColumn: null,
grouping: {},
}; };
} }

View File

@ -1,10 +1,11 @@
import _ from 'lodash'; 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 { ForeignKeyInfo, TableInfo, ColumnInfo, EngineDriver, NamedObjectInfo, DatabaseInfo } from '@dbgate/types';
import { parseFilter, getFilterType } from '@dbgate/filterparser'; import { parseFilter, getFilterType } from '@dbgate/filterparser';
import { filterName } from './filterName'; import { filterName } from './filterName';
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet'; import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
import { Expression, Select, treeToSql, dumpSqlSelect } from '@dbgate/sqltree'; import { Expression, Select, treeToSql, dumpSqlSelect } from '@dbgate/sqltree';
import { group } from 'console';
export interface DisplayColumn { export interface DisplayColumn {
schemaName: string; schemaName: string;
@ -174,16 +175,59 @@ export abstract class GridDisplay {
if (this.config.sort?.length > 0) { if (this.config.sort?.length > 0) {
select.orderBy = this.config.sort select.orderBy = this.config.sort
.map((col) => ({ ...col, dispInfo: displayedColumnInfo[col.uniqueName] })) .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) => ({ .map((col) => ({
exprType: 'column', ...col.expr,
columnName: col.dispInfo.columnName,
direction: col.order, 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) { getColumns(columnFilter) {
return this.columns.filter((col) => filterName(columnFilter, col.columnName)); return this.columns.filter((col) => filterName(columnFilter, col.columnName));
} }
@ -223,6 +267,34 @@ export abstract class GridDisplay {
this.reload(); 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) { getSortOrder(uniqueName) {
return this.config.sort.find((x) => x.uniqueName == uniqueName)?.order; return this.config.sort.find((x) => x.uniqueName == uniqueName)?.order;
} }
@ -298,6 +370,7 @@ export abstract class GridDisplay {
); );
this.processReferences(select, displayedColumnInfo); this.processReferences(select, displayedColumnInfo);
this.applyFilterOnSelect(select, displayedColumnInfo); this.applyFilterOnSelect(select, displayedColumnInfo);
this.applyGroupOnSelect(select, displayedColumnInfo);
this.applySortOnSelect(select, displayedColumnInfo); this.applySortOnSelect(select, displayedColumnInfo);
return select; return select;
} }

View File

@ -70,8 +70,8 @@ export class TableGridDisplay extends GridDisplay {
this.addReferenceToSelect(select, parentAlias, column); this.addReferenceToSelect(select, parentAlias, column);
this.addJoinsFromExpandedColumns(select, subcolumns, childAlias, columnSources) this.addJoinsFromExpandedColumns(select, subcolumns, childAlias, columnSources);
this.addAddedColumnsToSelect(select, subcolumns, childAlias, columnSources) this.addAddedColumnsToSelect(select, subcolumns, childAlias, columnSources);
} }
} }
} }
@ -111,8 +111,12 @@ export class TableGridDisplay extends GridDisplay {
addHintsToSelect(select: Select): boolean { addHintsToSelect(select: Select): boolean {
let res = false; let res = false;
const groupColumns = this.groupColumns;
for (const column of this.getGridColumns()) { for (const column of this.getGridColumns()) {
if (column.foreignKey) { if (column.foreignKey) {
if (groupColumns && !groupColumns.includes(column.uniqueName)) {
continue;
}
const table = this.getFkTarget(column); const table = this.getFkTarget(column);
if (table && table.columns && table.columns.length > 0) { if (table && table.columns && table.columns.length > 0) {
const hintColumn = table.columns.find((x) => x?.dataType?.toLowerCase()?.includes('char')); const hintColumn = table.columns.find((x) => x?.dataType?.toLowerCase()?.includes('char'));

View File

@ -27,5 +27,11 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
case 'raw': case 'raw':
dmp.put('%s', expr.sql); dmp.put('%s', expr.sql);
break; break;
case 'call':
dmp.put('%s(%s', expr.func, expr.argsPrefix);
dmp.putCollection(',', expr.args, (x) => dumpSqlExpression(dmp, x));
dmp.put(')');
break;
} }
} }

View File

@ -123,7 +123,14 @@ export interface RawExpression {
sql: string; 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 OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
export type ResultField = Expression & { alias?: string }; export type ResultField = Expression & { alias?: string };

View File

@ -2,7 +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 DropDownButton from '../widgets/DropDownButton'; import DropDownButton from '../widgets/DropDownButton';
import { DropDownMenuItem } from '../modals/DropDownMenu'; import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
import { useSplitterDrag } from '../widgets/Splitter'; import { useSplitterDrag } from '../widgets/Splitter';
import { FontIcon } from '../icons'; import { FontIcon } from '../icons';
@ -31,11 +31,18 @@ const ResizeHandle = styled.div`
z-index: 1; 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); const onResizeDown = useSplitterDrag('clientX', onResize);
return ( return (
<HeaderDiv> <HeaderDiv>
<LabelDiv> <LabelDiv>
{grouping && <GroupingLabel>{grouping.toLowerCase()}:</GroupingLabel>}
<ColumnLabel {...column} /> <ColumnLabel {...column} />
{order == 'ASC' && ( {order == 'ASC' && (
<IconWrapper> <IconWrapper>
@ -52,6 +59,14 @@ export default function ColumnHeaderControl({ column, setSort, onResize, order }
<DropDownButton> <DropDownButton>
<DropDownMenuItem onClick={() => setSort('ASC')}>Sort ascending</DropDownMenuItem> <DropDownMenuItem onClick={() => setSort('ASC')}>Sort ascending</DropDownMenuItem>
<DropDownMenuItem onClick={() => setSort('DESC')}>Sort descending</DropDownMenuItem> <DropDownMenuItem onClick={() => setSort('DESC')}>Sort descending</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={() => setGrouping('GROUP')}>Group by</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('MAX')}>MAX</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('MIN')}>MIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('SUM')}>SUM</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('AVG')}>AVG</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('COUNT')}>COUNT</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('COUNT DISTINCT')}>COUNT DISTINCT</DropDownMenuItem>
</DropDownButton> </DropDownButton>
)} )}
<ResizeHandle className="resizeHandleControl" onMouseDown={onResizeDown} /> <ResizeHandle className="resizeHandleControl" onMouseDown={onResizeDown} />

View File

@ -6,6 +6,7 @@ import { HorizontalScrollBar, VerticalScrollBar } from './ScrollBars';
import useDimensions from '../utility/useDimensions'; import useDimensions from '../utility/useDimensions';
import axios from '../utility/axios'; import axios from '../utility/axios';
import DataFilterControl from './DataFilterControl'; import DataFilterControl from './DataFilterControl';
import stableStringify from 'json-stable-stringify';
import { getFilterType } from '@dbgate/filterparser'; import { getFilterType } from '@dbgate/filterparser';
import { cellFromEvent, getCellRange, topLeftCell, isRegularCell, nullCell, emptyCellArray } from './selection'; import { cellFromEvent, getCellRange, topLeftCell, isRegularCell, nullCell, emptyCellArray } from './selection';
import keycodes from '../utility/keycodes'; import keycodes from '../utility/keycodes';
@ -481,6 +482,21 @@ export default function DataGridCore(props) {
} }
}, [display && display.focusedColumn]); }, [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(() => { const rowCountInfo = React.useMemo(() => {
if (selectedCells.length > 1 && selectedCells.every((x) => _.isNumber(x[0]) && _.isNumber(x[1]))) { if (selectedCells.length > 1 && selectedCells.every((x) => _.isNumber(x[0]) && _.isNumber(x[1]))) {
let sum = _.sumBy(selectedCells, (cell) => { 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('visibleRowCountUpperBound', visibleRowCountUpperBound);
// console.log('gridScrollAreaHeight', gridScrollAreaHeight); // console.log('gridScrollAreaHeight', gridScrollAreaHeight);
// console.log('containerHeight', containerHeight); // console.log('containerHeight', containerHeight);
@ -1155,6 +1175,8 @@ export default function DataGridCore(props) {
setSort={display.sortable ? (order) => display.setSort(col.uniqueName, order) : null} setSort={display.sortable ? (order) => display.setSort(col.uniqueName, order) : null}
order={display.getSortOrder(col.uniqueName)} order={display.getSortOrder(col.uniqueName)}
onResize={(diff) => display.resizeColumn(col.uniqueName, col.widthNumber, diff)} onResize={(diff) => display.resizeColumn(col.uniqueName, col.widthNumber, diff)}
setGrouping={display.sortable ? (groupFunc) => setGrouping(col.uniqueName, groupFunc) : null}
grouping={display.getGrouping(col.uniqueName)}
/> />
</TableHeaderCell> </TableHeaderCell>
))} ))}

View File

@ -53,10 +53,6 @@ export default function TableDataGrid({
const dbinfo = useDatabaseInfo({ conid, database }); const dbinfo = useDatabaseInfo({ conid, database });
const [reference, setReference] = React.useState(null); const [reference, setReference] = React.useState(null);
React.useEffect(() => {
setRefReloadToken((v) => v + 1);
}, [reference]);
function createDisplay() { function createDisplay() {
return connection return connection
? new TableGridDisplay( ? new TableGridDisplay(
@ -73,6 +69,11 @@ export default function TableDataGrid({
const [display, setDisplay] = React.useState(createDisplay()); const [display, setDisplay] = React.useState(createDisplay());
React.useEffect(() => {
setRefReloadToken((v) => v + 1);
if (!reference && display && display.isGrouped) display.clearGrouping();
}, [reference]);
React.useEffect(() => { React.useEffect(() => {
const newDisplay = createDisplay(); const newDisplay = createDisplay();
if (display && display.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return; if (display && display.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return;