From 7f600e1bcdde14289b05a16fa67ece2d99e6507b Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 25 Nov 2020 16:42:43 +0300 Subject: [PATCH 1/7] feat(plugin-data-spreadsheet): CB-268 add auto-width columns --- .../src/AgGridTable/AgGridTable.tsx | 98 +++++------- .../src/AgGridTable/AgGridTableController.ts | 150 ++++++++++++------ .../src/AgGridTable/AgGridTableLoader.tsx | 55 +++++++ .../src/Spreadsheet.tsx | 4 +- 4 files changed, 199 insertions(+), 108 deletions(-) create mode 100644 webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx 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 ; } From f08821ae370d252c8d7be946bad5083a1e4a5d2a Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 25 Nov 2020 16:47:08 +0300 Subject: [PATCH 2/7] feat(plugin-data-spreadsheet): CB-268 component names fixed --- .../plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx | 6 +++--- .../src/AgGridTable/AgGridTableController.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx index 16544e635c..022bc7a5e5 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx @@ -27,15 +27,15 @@ const agGridComponents = { plainTextEditor: PlainTextEditor, indexCellRenderer: IndexCellRenderer, }; -interface IAgGridMarkupProps { +interface IAgGridTableProps { agGridReact: React.FC; tableModel: TableViewerModel; className?: string; allCommunityModules: Module[]; } -export const AgGridTable: React.FC = observer( - function AgGridMarkup({ +export const AgGridTable: React.FC = observer( + function AgGridTable({ agGridReact: AgGridReact, tableModel, className, diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts index a1d860343c..aa79d8ee9d 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts @@ -268,8 +268,8 @@ export const INDEX_COLUMN_DEF: ColDef = { headerName: '#', colId: `${Number.MAX_SAFE_INTEGER}`, field: `${Number.MAX_SAFE_INTEGER}`, - pinned: 'left', width: 70, + pinned: 'left', suppressNavigable: true, suppressMenu: true, editable: false, From 17f458a70f2f6f03caffcea12cee263b62fb60ce Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 25 Nov 2020 18:19:07 +0300 Subject: [PATCH 3/7] feat(plugin-data-spreadsheet): CB-268 return changes --- .../src/AgGridTable/AgGridTableController.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts index aa79d8ee9d..62b0538ee5 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts @@ -21,6 +21,7 @@ import { CellClassParams, SortChangedEvent, RowNode, + CellEditingStoppedEvent } from '@ag-grid-community/core'; import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di'; import { @@ -84,6 +85,7 @@ export class AgGridTableController implements IInitializableController, IDestruc onBodyScroll: this.handleBodyScroll.bind(this), onSortChanged: this.handleSortChanged.bind(this), + onCellEditingStopped: this.handleCellEditingStopped.bind(this), }; @observable columns: ColDef[] = []; @@ -138,7 +140,6 @@ export class AgGridTableController implements IInitializableController, IDestruc startRow, endRow, successCallback, - sortModel, failCallback, } = params; @@ -158,6 +159,10 @@ export class AgGridTableController implements IInitializableController, IDestruc } } + private handleCellEditingStopped(event: CellEditingStoppedEvent) { + this.gridModel.onCellEditingStopped(event.rowIndex, event.column.getId(), event.value, false); + } + private changeChunkSize(): void { this.gridOptions.cacheBlockSize = this.gridModel.getChunkSize(); // ag-grid is not able to change ca From 504b4f2c330e3000254b46ea855b71780ab5731b Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 25 Nov 2020 19:43:00 +0300 Subject: [PATCH 4/7] feat(plugin-data-spreadsheet): CB-268 now taking font values from grid container --- .../src/AgGridTable/AgGridTable.tsx | 74 +++++---- .../src/AgGridTable/AgGridTableController.ts | 150 +++++++++--------- .../src/AgGridTable/AgGridTableLoader.tsx | 14 +- 3 files changed, 117 insertions(+), 121 deletions(-) diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx index 022bc7a5e5..c754a88c91 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx @@ -10,15 +10,17 @@ 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 { 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 from '../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'; @@ -28,45 +30,41 @@ const agGridComponents = { indexCellRenderer: IndexCellRenderer, }; interface IAgGridTableProps { - agGridReact: React.FC; tableModel: TableViewerModel; className?: string; - allCommunityModules: Module[]; } -export const AgGridTable: React.FC = observer( - function AgGridTable({ - 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; +export const AgGridTable: React.FC = observer(function AgGridTable({ + tableModel, + className, + ...rest +}) { + const agGridTableStyles = useStyles(agGridStyles, styles); + const allCommunityModules = [InfiniteRowModelModule, RangeSelectionModule]; + 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; - useEffect(() => { - if (agGridContainerRef.current) { - controller.gridContainer = agGridContainerRef.current; - } - }, [controller]); + useEffect(() => { + if (agGridContainerRef.current) { + controller.gridContainer = agGridContainerRef.current; + } + }, [controller]); - return styled(styles)( - - - - ); - }); + return styled(agGridTableStyles)( + + + + ); +}); diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts index 62b0538ee5..c12e9c5a33 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts @@ -148,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.rows, this.gridContainer, requestedData.columns); + this.columns = this.mapDataToColumns(requestedData.rows, this.gridContainer, requestedData.columns); } successCallback( this.cloneRows(requestedData.rows), @@ -159,16 +159,16 @@ export class AgGridTableController implements IInitializableController, IDestruc } } - private handleCellEditingStopped(event: CellEditingStoppedEvent) { - this.gridModel.onCellEditingStopped(event.rowIndex, event.column.getId(), event.value, false); - } - private changeChunkSize(): void { this.gridOptions.cacheBlockSize = this.gridModel.getChunkSize(); // ag-grid is not able to change ca 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); } @@ -259,81 +259,64 @@ export class AgGridTableController implements IInitializableController, IDestruc private cloneRows(rows: AgGridRow[]): AgGridRow[] { return rows.map(row => [...row]); } -} -const defaultColumnDef: ColDef = { - sortable: true, - filter: true, - resizable: true, - editable: true, - cellEditor: 'plainTextEditor', -}; + private measureText(text: string, title = false) { + let font = '400 12px Roboto'; -export const INDEX_COLUMN_DEF: ColDef = { - headerName: '#', - colId: `${Number.MAX_SAFE_INTEGER}`, - field: `${Number.MAX_SAFE_INTEGER}`, - width: 70, - pinned: 'left', - suppressNavigable: true, - suppressMenu: true, - editable: false, - sortable: false, - valueGetter: props => props.node.rowIndex + 1, - cellRendererSelector: props => ({ component: !props.data ? 'indexCellRenderer' : undefined }), -}; - -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; + 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); } - return Math.max(columnTitleWidth, measureText(longestRowValue)); -} + 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 = ''; -function mapDataToColumns(rows: AgGridRow[], gridContainer: HTMLElement | null, columns?: IAgGridCol[]): ColDef[] { - if (!columns || !columns.length) { - return []; + 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)); } - return [ - INDEX_COLUMN_DEF, - ...columns.map((v, columnIdx) => { - const columnWidth = getColumnWidth(v, columnIdx, rows); - const columnMaxWidth = getMaxColumnWidth(gridContainer); + 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 ({ + return this.gridContainer.getBoundingClientRect().width * MAX_WIDTH_COLUMN_PERCENT / 100; + } + + private 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: `${columnIdx}`, - width: columnWidth, - maxWidth: columnMaxWidth, + field: `${i}`, + width: this.getColumnWidth(v, i, rows), + maxWidth: this.getMaxColumnWidth(), // type: v.dataKind, editable: (params: any) => { const context: AgGridContext = params.context; @@ -376,11 +359,34 @@ function mapDataToColumns(rows: AgGridRow[], gridContainer: HTMLElement | null, } return classes.join(' '); }, - }); - }), - ]; + }) + ), + ]; + } } +const defaultColumnDef: ColDef = { + sortable: true, + filter: true, + resizable: true, + editable: true, + cellEditor: 'plainTextEditor', +}; + +export const INDEX_COLUMN_DEF: ColDef = { + headerName: '#', + colId: `${Number.MAX_SAFE_INTEGER}`, + field: `${Number.MAX_SAFE_INTEGER}`, + width: 70, + pinned: 'left', + suppressNavigable: true, + suppressMenu: true, + editable: false, + sortable: false, + valueGetter: props => props.node.rowIndex + 1, + cellRendererSelector: props => ({ component: !props.data ? 'indexCellRenderer' : undefined }), +}; + 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 index b66b945989..c17641bc8d 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx @@ -13,8 +13,6 @@ 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; @@ -22,12 +20,9 @@ AgGridReactProps & { }>; 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'); + const { AgGridTable } = await import('./AgGridTable'); - return { AgGridReact, AllCommunityModules: [InfiniteRowModelModule, RangeSelectionModule] }; + return { AgGridTable }; } export const AgGridTableLoader = observer(function AgGridTableLoader({ @@ -40,15 +35,12 @@ export const AgGridTableLoader = observer(function AgGridTableLoader({ loader={loader} placeholder={} > - {({ AgGridReact, AllCommunityModules }) => ( + {({ AgGridTable }) => ( - )} ); From 07dc751132959edddb0077782730638ed102ed75 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 25 Nov 2020 19:46:03 +0300 Subject: [PATCH 5/7] feat(plugin-data-spreadsheet): CB-268 remove gridContainer argument from mapDataToColumns --- .../src/AgGridTable/AgGridTableController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts index c12e9c5a33..a76dd93aa0 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts @@ -148,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 = this.mapDataToColumns(requestedData.rows, this.gridContainer, requestedData.columns); + this.columns = this.mapDataToColumns(requestedData.rows, requestedData.columns); } successCallback( this.cloneRows(requestedData.rows), @@ -304,7 +304,7 @@ export class AgGridTableController implements IInitializableController, IDestruc return this.gridContainer.getBoundingClientRect().width * MAX_WIDTH_COLUMN_PERCENT / 100; } - private mapDataToColumns(rows: AgGridRow[], gridContainer: HTMLElement | null, columns?: IAgGridCol[]): ColDef[] { + private mapDataToColumns(rows: AgGridRow[], columns?: IAgGridCol[]): ColDef[] { if (!columns || !columns.length) { return []; } From 6c1abc5de3bd12afacb00ef03ef8084a51b44f73 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 25 Nov 2020 20:11:08 +0300 Subject: [PATCH 6/7] feat(plugin-data-spreadsheet): CB-268 code optimization, remove ...rest --- .../src/AgGridTable/AgGridTable.tsx | 10 +++------- .../src/AgGridTable/AgGridTableController.ts | 6 +++--- .../src/AgGridTable/AgGridTableLoader.tsx | 7 ++----- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx index c754a88c91..a91b4741a1 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx @@ -29,18 +29,15 @@ const agGridComponents = { plainTextEditor: PlainTextEditor, indexCellRenderer: IndexCellRenderer, }; + +const allCommunityModules = [InfiniteRowModelModule, RangeSelectionModule]; interface IAgGridTableProps { tableModel: TableViewerModel; className?: string; } -export const AgGridTable: React.FC = observer(function AgGridTable({ - tableModel, - className, - ...rest -}) { +export const AgGridTable: React.FC = observer(function AgGridTable({ tableModel, className }) { const agGridTableStyles = useStyles(agGridStyles, styles); - const allCommunityModules = [InfiniteRowModelModule, RangeSelectionModule]; const refreshRef = useRef(0); const agGridContainerRef = useRef(null); const controller = useController(AgGridTableController, tableModel); @@ -63,7 +60,6 @@ export const AgGridTable: React.FC = observer(function AgGrid frameworkComponents={agGridComponents} loadingCellRenderer="loadingCellRenderer" {...controller.dynamicOptions} - {...rest} /> ); diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts index a76dd93aa0..246ea46ae2 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts @@ -301,14 +301,14 @@ export class AgGridTableController implements IInitializableController, IDestruc return MAX_WIDTH_COLUMN_DEFALUT_VALUE; } - return this.gridContainer.getBoundingClientRect().width * MAX_WIDTH_COLUMN_PERCENT / 100; + 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) => ({ @@ -316,7 +316,7 @@ export class AgGridTableController implements IInitializableController, IDestruc headerName: v.label, field: `${i}`, width: this.getColumnWidth(v, i, rows), - maxWidth: this.getMaxColumnWidth(), + maxWidth: columnMaxWidth, // type: v.dataKind, editable: (params: any) => { const context: AgGridContext = params.context; diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx index c17641bc8d..2c4c4c5a57 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableLoader.tsx @@ -21,15 +21,13 @@ AgGridReactProps & { async function loader() { const { AgGridTable } = await import('./AgGridTable'); - return { AgGridTable }; } -export const AgGridTableLoader = observer(function AgGridTableLoader({ +export const AgGridTableLoader: React.FC = observer(function AgGridTableLoader({ tableModel, className, - ...rest -}: AgGridTableProps) { +}) { return ( )} From dc7afc80e4ae20826cbba0b17ad5d1dd12fd3cc3 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 25 Nov 2020 21:29:50 +0300 Subject: [PATCH 7/7] feat(plugin-data-spreadsheet): CB-268 type props to interface props --- .../src/AgGridTable/AgGridTable.tsx | 6 +++--- .../src/AgGridTable/AgGridTableLoader.tsx | 12 +++--------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx index a91b4741a1..524df52229 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTable.tsx @@ -16,7 +16,7 @@ import { useController } from '@cloudbeaver/core-di'; import { useStyles } from '@cloudbeaver/core-theming'; import { TableViewerModel } from '@cloudbeaver/plugin-data-viewer'; -import styles from '../styles/base.scss'; +import '../styles/base.scss'; import { agGridStyles } from '../styles/styles'; import { AgGridTableController } from './AgGridTableController'; import { IndexCellRenderer } from './IndexCellRenderer'; @@ -37,7 +37,7 @@ interface IAgGridTableProps { } export const AgGridTable: React.FC = observer(function AgGridTable({ tableModel, className }) { - const agGridTableStyles = useStyles(agGridStyles, styles); + const styles = useStyles(agGridStyles); const refreshRef = useRef(0); const agGridContainerRef = useRef(null); const controller = useController(AgGridTableController, tableModel); @@ -50,7 +50,7 @@ export const AgGridTable: React.FC = observer(function AgGrid } }, [controller]); - return styled(agGridTableStyles)( + return styled(styles)( ; +} async function loader() { const { AgGridTable } = await import('./AgGridTable'); return { AgGridTable }; } -export const AgGridTableLoader: React.FC = observer(function AgGridTableLoader({ - tableModel, - className, -}) { +export const AgGridTableLoader: React.FC = observer(function AgGridTableLoader({ tableModel, className }) { return (