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 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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'));
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 };
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user