feat(plugin-data-spreadsheet): CB-268 add auto-width columns

This commit is contained in:
Naumov Alexey
2020-11-25 16:42:43 +03:00
parent fb2500b7e6
commit 7f600e1bcd
4 changed files with 199 additions and 108 deletions
@@ -6,12 +6,12 @@
* 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 { Module } from '@ag-grid-community/core';
import { AgGridReactProps } from '@ag-grid-community/react';
import { ComplexLoader, Loader } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { useStyles } from '@cloudbeaver/core-theming';
import { TableViewerModel } from '@cloudbeaver/plugin-data-viewer';
@@ -22,61 +22,51 @@ import { IndexCellRenderer } from './IndexCellRenderer';
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,
};
interface IAgGridMarkupProps {
agGridReact: React.FC<AgGridReactProps>;
tableModel: TableViewerModel;
className?: string;
allCommunityModules: Module[];
}
export const AgGridTable = observer(function AgGridTable({
tableModel,
className,
...rest
}: AgGridTableProps) {
const refreshRef = useRef(0);
const styles = useStyles(agGridStyles);
const controller = useController(AgGridTableController, tableModel);
useMemo(() => refreshRef.current === controller.refreshId && controller.refresh(), [tableModel]);
refreshRef.current = controller.refreshId;
export const AgGridTable: React.FC<IAgGridMarkupProps> = observer(
function AgGridMarkup({
agGridReact: AgGridReact,
tableModel,
className,
allCommunityModules,
...rest
}) {
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}
{...rest}
/>
</ag-grid-theme>
);
});
@@ -21,7 +21,6 @@ import {
CellClassParams,
SortChangedEvent,
RowNode,
CellEditingStoppedEvent
} from '@ag-grid-community/core';
import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di';
import {
@@ -31,9 +30,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),
@@ -76,7 +84,6 @@ export class AgGridTableController implements IInitializableController, IDestruc
onBodyScroll: this.handleBodyScroll.bind(this),
onSortChanged: this.handleSortChanged.bind(this),
onCellEditingStopped: this.handleCellEditingStopped.bind(this),
};
@observable columns: ColDef[] = [];
@@ -131,6 +138,7 @@ export class AgGridTableController implements IInitializableController, IDestruc
startRow,
endRow,
successCallback,
sortModel,
failCallback,
} = params;
@@ -139,7 +147,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 = mapDataToColumns(requestedData.rows, this.gridContainer, requestedData.columns);
}
successCallback(
this.cloneRows(requestedData.rows),
@@ -233,10 +241,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 {
@@ -264,8 +268,8 @@ export const INDEX_COLUMN_DEF: ColDef = {
headerName: '#',
colId: `${Number.MAX_SAFE_INTEGER}`,
field: `${Number.MAX_SAFE_INTEGER}`,
width: 70,
pinned: 'left',
width: 70,
suppressNavigable: true,
suppressMenu: true,
editable: false,
@@ -274,59 +278,101 @@ export const INDEX_COLUMN_DEF: ColDef = {
cellRendererSelector: props => ({ component: !props.data ? 'indexCellRenderer' : undefined }),
};
function mapDataToColumns(columns?: IAgGridCol[]): ColDef[] {
function getMaxColumnWidth(gridContainer: HTMLElement | null) {
if (!gridContainer) {
console.info('Can"t get grid container width, default value will be used');
return MAX_WIDTH_COLUMN_DEFALUT_VALUE;
}
return gridContainer.getBoundingClientRect().width * MAX_WIDTH_COLUMN_PERCENT / 100;
}
function measureText(text: string, title = false) {
const font = '400 12px Roboto';
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);
}
function getColumnWidth(column: IAgGridCol, columnIdx: number, rows: AgGridRow) {
const columnTitleWidth = 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, measureText(longestRowValue));
}
function mapDataToColumns(rows: AgGridRow[], gridContainer: HTMLElement | null, 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'];
...columns.map((v, columnIdx) => {
const columnWidth = getColumnWidth(v, columnIdx, rows);
const columnMaxWidth = getMaxColumnWidth(gridContainer);
if (value !== null && typeof value === 'object') {
return JSON.stringify(value);
}
return ({
colId: v.name,
headerName: v.label,
field: `${columnIdx}`,
width: columnWidth,
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'];
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 (typeof value === 'object') {
return JSON.stringify(value);
}
if (params.value === null) {
return '[null]';
}
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('');
}
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(' ');
},
})),
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(' ');
},
});
}),
];
}
@@ -0,0 +1,55 @@
/*
* 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 { PropsWithChildren } from 'react';
import { AgGridReactProps } from '@ag-grid-community/react';
import { ComplexLoader, Loader } from '@cloudbeaver/core-blocks';
import { TableViewerModel } from '@cloudbeaver/plugin-data-viewer';
import { AgGridTable } from './AgGridTable';
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] };
}
export const AgGridTableLoader = observer(function AgGridTableLoader({
tableModel,
className,
...rest
}: AgGridTableProps) {
return (
<ComplexLoader
loader={loader}
placeholder={<Loader />}
>
{({ AgGridReact, AllCommunityModules }) => (
<AgGridTable
agGridReact={AgGridReact as any}
allCommunityModules={AllCommunityModules}
tableModel={tableModel}
className={className}
{...rest}
/>
)}
</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} />;
}