Merge pull request #226 from dbeaver/feat/column-auto-width

Feat/column auto width
This commit is contained in:
Alexey
2020-11-25 22:16:17 +03:00
committed by GitHub
4 changed files with 191 additions and 112 deletions
@@ -6,77 +6,61 @@
* you may not use this file except in compliance with the License.
*/
import { observer, Observer } from 'mobx-react';
import { PropsWithChildren, useMemo, useRef } from 'react';
import { observer } from 'mobx-react';
import { useEffect, useMemo, useRef } from 'react';
import styled from 'reshadow';
import { AgGridReactProps } from '@ag-grid-community/react';
import { ComplexLoader, Loader } from '@cloudbeaver/core-blocks';
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
import { AgGridReact } from '@ag-grid-community/react';
import { useController } from '@cloudbeaver/core-di';
import { useStyles } from '@cloudbeaver/core-theming';
import { TableViewerModel } from '@cloudbeaver/plugin-data-viewer';
import '../styles/base.scss';
import { agGridStyles } from '../styles/styles';
import { AgGridTableController } from './AgGridTableController';
import { IndexCellRenderer } from './IndexCellRenderer';
import { RangeSelectionModule } from './modules/RangeSelection/rangeSelectionModule';
import { PlainTextEditor } from './PlainTextEditor/PlainTextEditor';
import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader';
export type AgGridTableProps = PropsWithChildren<
AgGridReactProps & {
tableModel: TableViewerModel;
className?: string;
}>;
async function loader() {
const styles = await import('../styles/base.scss');
const { AgGridReact } = await import('@ag-grid-community/react');
const { InfiniteRowModelModule } = await import('@ag-grid-community/infinite-row-model');
const { RangeSelectionModule } = await import('./modules/RangeSelection/rangeSelectionModule');
return { AgGridReact, AllCommunityModules: [InfiniteRowModelModule, RangeSelectionModule] };
}
const agGridComponents = {
agColumnHeader: TableColumnHeader,
plainTextEditor: PlainTextEditor,
indexCellRenderer: IndexCellRenderer,
};
export const AgGridTable = observer(function AgGridTable({
tableModel,
className,
...rest
}: AgGridTableProps) {
const refreshRef = useRef(0);
const allCommunityModules = [InfiniteRowModelModule, RangeSelectionModule];
interface IAgGridTableProps {
tableModel: TableViewerModel;
className?: string;
}
export const AgGridTable: React.FC<IAgGridTableProps> = observer(function AgGridTable({ tableModel, className }) {
const styles = useStyles(agGridStyles);
const refreshRef = useRef(0);
const agGridContainerRef = useRef(null);
const controller = useController(AgGridTableController, tableModel);
useMemo(() => refreshRef.current === controller.refreshId && controller.refresh(), [tableModel]);
refreshRef.current = controller.refreshId;
return (
<ComplexLoader
loader={loader}
placeholder={<Loader />}
>
{({ AgGridReact, AllCommunityModules }) => (
<Observer>
{() => styled(styles)(
<ag-grid-theme as="div" className={`cb-ag-grid-theme ${className}`}>
<AgGridReact
key={controller.refreshId}
columnDefs={controller.columns}
gridOptions={controller.getGridOptions()}
modules={AllCommunityModules}
frameworkComponents={agGridComponents}
loadingCellRenderer="loadingCellRenderer"
{...controller.dynamicOptions}
{...rest}
/>
</ag-grid-theme>
)}
</Observer>
)}
</ComplexLoader>
useEffect(() => {
if (agGridContainerRef.current) {
controller.gridContainer = agGridContainerRef.current;
}
}, [controller]);
return styled(styles)(
<ag-grid-theme ref={agGridContainerRef} as="div" className={`cb-ag-grid-theme ${className}`}>
<AgGridReact
key={controller.refreshId}
columnDefs={controller.columns}
gridOptions={controller.getGridOptions()}
modules={allCommunityModules}
frameworkComponents={agGridComponents}
loadingCellRenderer="loadingCellRenderer"
{...controller.dynamicOptions}
/>
</ag-grid-theme>
);
});
@@ -31,9 +31,18 @@ import {
import { AgGridContext } from './AgGridContext';
import { TableSelection } from './TableSelection/TableSelection';
/** title margin + type icon width + sort icon width + title margin right + box padding */
const COLUMN_TITLE_BOX_WIDTH = 8 + 16 + 24 + 20 + 24;
/** row padding + sort шcon width + right column title padding */
const ROW_VALUE_BOX_WIDTH = 22 + 20 + 12;
/** how many rows we want to include to get column width */
const ROWS_INCLUDE_IN_MEASUREMENTS = 100;
const MAX_WIDTH_COLUMN_PERCENT = 33;
const MAX_WIDTH_COLUMN_DEFALUT_VALUE = 300;
@injectable()
export class AgGridTableController implements IInitializableController, IDestructibleController {
@observable refreshId = 0;
gridContainer: HTMLElement | null = null;
private readonly datasource: IDatasource = {
getRows: this.getRows.bind(this),
@@ -139,7 +148,7 @@ export class AgGridTableController implements IInitializableController, IDestruc
const requestedData = await this.gridModel.onRequestData(startRow, length);
// update columns only once after first data fetching
if (isColumnsChanged(this.columns, requestedData.columns)) {
this.columns = mapDataToColumns(requestedData.columns);
this.columns = this.mapDataToColumns(requestedData.rows, requestedData.columns);
}
successCallback(
this.cloneRows(requestedData.rows),
@@ -156,6 +165,10 @@ export class AgGridTableController implements IInitializableController, IDestruc
this.refresh();
}
private handleCellEditingStopped(event: CellEditingStoppedEvent) {
this.gridModel.onCellEditingStopped(event.rowIndex, event.column.getId(), event.value, false);
}
private revertCellValue(rowIndex: number, colId: string) {
this.gridModel.revertCellValue(rowIndex, colId);
}
@@ -233,10 +246,6 @@ export class AgGridTableController implements IInitializableController, IDestruc
}
}
private handleCellEditingStopped(event: CellEditingStoppedEvent) {
this.gridModel.onCellEditingStopped(event.rowIndex, event.column.getId(), event.value, false);
}
/* Actions */
private resetData(): void {
@@ -250,6 +259,110 @@ export class AgGridTableController implements IInitializableController, IDestruc
private cloneRows(rows: AgGridRow[]): AgGridRow[] {
return rows.map(row => [...row]);
}
private measureText(text: string, title = false) {
let font = '400 12px Roboto';
if (this.gridContainer) {
const fontTags = ['font-weight', 'font-size', 'font-family'];
const styleDeclaration = window.getComputedStyle(this.gridContainer);
const fontValues = fontTags.map(fontValue => fontValue === 'font-family' ? styleDeclaration.getPropertyValue(fontValue).split(',')[0] : styleDeclaration.getPropertyValue(fontValue));
if (fontValues.filter(v => v !== '').length === fontTags.length) {
font = fontValues.join(' ');
}
}
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
context.font = font;
return context.measureText(text.toUpperCase()).width + (title ? COLUMN_TITLE_BOX_WIDTH : ROW_VALUE_BOX_WIDTH);
}
private getColumnWidth(column: IAgGridCol, columnIdx: number, rows: AgGridRow) {
const columnTitleWidth = this.measureText(column.name || '', true);
const rowsToIncludeInMeasurements = Math.min(rows.length, ROWS_INCLUDE_IN_MEASUREMENTS);
let longestRowValue = '';
for (let i = 0; i < rowsToIncludeInMeasurements; i++) {
// we need String to process NULL value
const rowValue = String(rows[i][columnIdx]);
if (rowValue.length > longestRowValue.length) {
longestRowValue = rowValue;
}
}
return Math.max(columnTitleWidth, this.measureText(longestRowValue));
}
private getMaxColumnWidth() {
if (!this.gridContainer) {
console.info('Can"t get grid container width, default value will be used');
return MAX_WIDTH_COLUMN_DEFALUT_VALUE;
}
return Math.round(this.gridContainer.getBoundingClientRect().width * MAX_WIDTH_COLUMN_PERCENT / 100);
}
private mapDataToColumns(rows: AgGridRow[], columns?: IAgGridCol[]): ColDef[] {
if (!columns || !columns.length) {
return [];
}
const columnMaxWidth = this.getMaxColumnWidth();
return [
INDEX_COLUMN_DEF,
...columns.map((v, i) => ({
colId: v.name,
headerName: v.label,
field: `${i}`,
width: this.getColumnWidth(v, i, rows),
maxWidth: columnMaxWidth,
// type: v.dataKind,
editable: (params: any) => {
const context: AgGridContext = params.context;
return !(context.isReadonly() || v.readOnly);
},
valueGetter: (params: ValueGetterParams) => {
if (!params.data) {
return '';
}
const value = params.data[params.colDef.field || 'node.id'];
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
},
headerComponentParams: {
icon: v.icon,
},
cellRenderer: (params: CellClassParams) => {
if (typeof params.value === 'string' && params.value.length > 1000) {
return params.value.split('').map(v => (v.charCodeAt(0) < 32 ? ' ' : v)).join('');
}
if (params.value === 'null') {
return '[null]';
}
return params.value;
},
cellClass: (params: any) => {
const classes: string[] = [];
const context: AgGridContext = params.context;
if (context.isCellEdited(params.node.rowIndex, params.colDef.colId)) {
classes.push('cell-edited');
}
if (params.value === 'null') {
classes.push('cell-null');
}
return classes.join(' ');
},
})
),
];
}
}
const defaultColumnDef: ColDef = {
@@ -274,62 +387,6 @@ export const INDEX_COLUMN_DEF: ColDef = {
cellRendererSelector: props => ({ component: !props.data ? 'indexCellRenderer' : undefined }),
};
function mapDataToColumns(columns?: IAgGridCol[]): ColDef[] {
if (!columns || !columns.length) {
return [];
}
return [
INDEX_COLUMN_DEF,
...columns.map((v, i) => ({
colId: v.name,
headerName: v.label,
field: `${i}`,
// type: v.dataKind,
editable: (params: any) => {
const context: AgGridContext = params.context;
return !(context.isReadonly() || v.readOnly);
},
valueGetter: (params: ValueGetterParams) => {
if (!params.data) {
return '';
}
const value = params.data[params.colDef.field || 'node.id'];
if (value !== null && typeof value === 'object') {
return JSON.stringify(value);
}
return value;
},
headerComponentParams: {
icon: v.icon,
},
cellRenderer: (params: CellClassParams) => {
if (typeof params.value === 'string' && params.value.length > 1000) {
return params.value.split('').map(v => (v.charCodeAt(0) < 32 ? ' ' : v)).join('');
}
if (params.value === null) {
return '[null]';
}
return params.value;
},
cellClass: (params: any) => {
const classes: string[] = [];
const context: AgGridContext = params.context;
if (context.isCellEdited(params.node.rowIndex, params.colDef.colId)) {
classes.push('cell-edited');
}
if (params.value === null) {
classes.push('cell-null');
}
return classes.join(' ');
},
})),
];
}
function isColumnsChanged(oldColumns: ColDef[], newColumns: IAgGridCol[] = []): boolean {
const [indexColumn, ...withoutIndexColumn] = oldColumns;
if (withoutIndexColumn.length !== newColumns.length) {
@@ -0,0 +1,38 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react';
import { ComplexLoader, Loader } from '@cloudbeaver/core-blocks';
import { TableViewerModel } from '@cloudbeaver/plugin-data-viewer';
interface Props {
tableModel: TableViewerModel;
className?: string;
}
async function loader() {
const { AgGridTable } = await import('./AgGridTable');
return { AgGridTable };
}
export const AgGridTableLoader: React.FC<Props> = observer(function AgGridTableLoader({ tableModel, className }) {
return (
<ComplexLoader
loader={loader}
placeholder={<Loader />}
>
{({ AgGridTable }) => (
<AgGridTable
tableModel={tableModel}
className={className}
/>
)}
</ComplexLoader>
);
});
@@ -8,7 +8,7 @@
import { DataModelWrapper, IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer';
import { AgGridTable } from './AgGridTable/AgGridTable';
import { AgGridTableLoader } from './AgGridTable/AgGridTableLoader';
interface Props {
model: IDatabaseDataModel<any, any>;
@@ -21,5 +21,5 @@ export function Spreadsheet({
}: Props) {
const deprecated = (model as DataModelWrapper).deprecatedModel;
return <AgGridTable tableModel={deprecated} className={className} />;
return <AgGridTableLoader tableModel={deprecated} className={className} />;
}