diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx index 828c798a4e..524df52229 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx @@ -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 = 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 ( - } - > - {({ AgGridReact, AllCommunityModules }) => ( - - {() => styled(styles)( - - - - )} - - )} - + useEffect(() => { + if (agGridContainerRef.current) { + controller.gridContainer = agGridContainerRef.current; + } + }, [controller]); + + return styled(styles)( + + + ); }); diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts index 712659d2e6..246ea46ae2 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts @@ -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) { diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx new file mode 100644 index 0000000000..081136184a --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx @@ -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 = observer(function AgGridTableLoader({ tableModel, className }) { + return ( + } + > + {({ AgGridTable }) => ( + + )} + + ); +}); diff --git a/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx b/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx index 02e60be55f..151ef2dccf 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx @@ -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; @@ -21,5 +21,5 @@ export function Spreadsheet({ }: Props) { const deprecated = (model as DataModelWrapper).deprecatedModel; - return ; + return ; }