mirror of
https://github.com/dbgate/dbgate
synced 2024-11-07 20:26:23 +00:00
grid - grouping
This commit is contained in:
parent
425e58627f
commit
0e1e3b9ed7
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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'));
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 };
|
||||
|
@ -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 (
|
||||
<HeaderDiv>
|
||||
<LabelDiv>
|
||||
{grouping && <GroupingLabel>{grouping.toLowerCase()}:</GroupingLabel>}
|
||||
|
||||
<ColumnLabel {...column} />
|
||||
{order == 'ASC' && (
|
||||
<IconWrapper>
|
||||
@ -52,6 +59,14 @@ export default function ColumnHeaderControl({ column, setSort, onResize, order }
|
||||
<DropDownButton>
|
||||
<DropDownMenuItem onClick={() => setSort('ASC')}>Sort ascending</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>
|
||||
)}
|
||||
<ResizeHandle className="resizeHandleControl" onMouseDown={onResizeDown} />
|
||||
|
@ -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)}
|
||||
/>
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user