diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx index 828c798a4e..16544e635c 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx @@ -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; + 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 = 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 ( - } - > - {({ 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..a1d860343c 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts @@ -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(' '); + }, + }); + }), ]; } 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..b66b945989 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx @@ -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 ( + } + > + {({ AgGridReact, AllCommunityModules }) => ( + + + )} + + ); +}); 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 ; }