From eaf199d8bf7bebb53940b07037753203d5dc350e Mon Sep 17 00:00:00 2001 From: mufazalov Date: Thu, 25 Jan 2024 17:06:21 +0300 Subject: [PATCH 1/6] feat(VirtualTable): enable columns resize --- src/components/VirtualTable/TableHead.tsx | 137 ++++++++++++++---- src/components/VirtualTable/TableRow.tsx | 22 ++- src/components/VirtualTable/VirtualTable.scss | 137 ++++++++---------- src/components/VirtualTable/VirtualTable.tsx | 12 +- src/components/VirtualTable/types.ts | 1 + src/containers/Nodes/VirtualNodes.tsx | 6 +- src/containers/Nodes/getNodesColumns.tsx | 1 + src/utils/hooks/useTableResize.ts | 53 +++++++ 8 files changed, 262 insertions(+), 107 deletions(-) create mode 100644 src/utils/hooks/useTableResize.ts diff --git a/src/components/VirtualTable/TableHead.tsx b/src/components/VirtualTable/TableHead.tsx index a75d5b1975..be39ce7dc9 100644 --- a/src/components/VirtualTable/TableHead.tsx +++ b/src/components/VirtualTable/TableHead.tsx @@ -1,14 +1,21 @@ -import {useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; + +import type { + HandleTableColumnsResize, + TableColumnsWidthSetup, +} from '../../utils/hooks/useTableResize'; import type {Column, OnSort, SortOrderType, SortParams} from './types'; import {ASCENDING, DEFAULT_SORT_ORDER, DEFAULT_TABLE_ROW_HEIGHT, DESCENDING} from './constants'; import {b} from './shared'; +const COLUMN_NAME_HTML_ATTRIBUTE = 'data-columnname'; + // Icon similar to original DataTable icons to keep the same tables across diferent pages and tabs const SortIcon = ({order}: {order?: SortOrderType}) => { return ( { if (sortable) { return ( - + ); @@ -36,9 +43,82 @@ const ColumnSortIcon = ({sortOrder, sortable, defaultSortOrder}: ColumnSortIconP } }; +interface TableHeadCellProps { + column: Column; + sortOrder?: SortOrderType; + defaultSortOrder: SortOrderType; + onSort?: (columnName: string) => void; + rowHeight: number; + resizeObserver?: ResizeObserver; +} + +export const TableHeadCell = ({ + column, + sortOrder, + defaultSortOrder, + onSort, + rowHeight, + resizeObserver, +}: TableHeadCellProps) => { + const cellWrapperRef = useRef(null); + + useEffect(() => { + const cellWrapper = cellWrapperRef.current; + if (cellWrapper) { + resizeObserver?.observe(cellWrapper); + } + return () => { + if (cellWrapper) { + resizeObserver?.unobserve(cellWrapper); + } + }; + }, [resizeObserver]); + + const content = column.header ?? column.name; + + return ( + +
+
{ + if (column.sortable) { + onSort?.(column.name); + } + }} + > +
{content}
+ +
+
+ + ); +}; + interface TableHeadProps { columns: Column[]; onSort?: OnSort; + onColumnsResize?: HandleTableColumnsResize; defaultSortOrder?: SortOrderType; rowHeight?: number; } @@ -46,11 +126,31 @@ interface TableHeadProps { export const TableHead = ({ columns, onSort, + onColumnsResize, defaultSortOrder = DEFAULT_SORT_ORDER, rowHeight = DEFAULT_TABLE_ROW_HEIGHT, }: TableHeadProps) => { const [sortParams, setSortParams] = useState({}); + const resizeObserver = useRef(); + const isTableResizeable = Boolean(onColumnsResize); + + useEffect(() => { + if (!isTableResizeable) { + return; + } + resizeObserver.current = new ResizeObserver((entries) => { + const columnsWidth: TableColumnsWidthSetup = {}; + entries.forEach((entry) => { + // @ts-ignore ignore custrom property usage + const id = entry.target.attributes[COLUMN_NAME_HTML_ATTRIBUTE]?.value; + columnsWidth[id] = entry.contentRect.width; + }); + + onColumnsResize?.(columnsWidth); + }); + }, [onColumnsResize, isTableResizeable]); + const handleSort = (columnId: string) => { let newSortParams: SortParams = {}; @@ -95,34 +195,19 @@ export const TableHead = ({ {columns.map((column) => { - const content = column.header ?? column.name; const sortOrder = sortParams.columnId === column.name ? sortParams.sortOrder : undefined; return ( - { - handleSort(column.name); - }} - > -
- {content} - -
- + column={column} + sortOrder={sortOrder} + defaultSortOrder={defaultSortOrder} + onSort={handleSort} + rowHeight={rowHeight} + resizeObserver={resizeObserver.current} + /> ); })} diff --git a/src/components/VirtualTable/TableRow.tsx b/src/components/VirtualTable/TableRow.tsx index 35e8abaeaa..a33fdf8227 100644 --- a/src/components/VirtualTable/TableRow.tsx +++ b/src/components/VirtualTable/TableRow.tsx @@ -8,15 +8,29 @@ import {b} from './shared'; interface TableCellProps { height: number; + width: number; align?: AlignType; children: ReactNode; className?: string; } -const TableRowCell = ({children, className, height, align = DEFAULT_ALIGN}: TableCellProps) => { +const TableRowCell = ({ + children, + className, + height, + width, + align = DEFAULT_ALIGN, +}: TableCellProps) => { + // Additional wrapper
with explicit width to ensure proper overflow:hidden + // since overflow works poorly with return ( - - {children} + +
+ {children} +
); }; @@ -35,6 +49,7 @@ export const LoadingTableRow = ({index, columns, height}: LoadingTableRowPro @@ -64,6 +79,7 @@ export const TableRow = ({row, index, columns, getRowClassName, height}: Tab diff --git a/src/components/VirtualTable/VirtualTable.scss b/src/components/VirtualTable/VirtualTable.scss index 690e39a76b..d9435c353e 100644 --- a/src/components/VirtualTable/VirtualTable.scss +++ b/src/components/VirtualTable/VirtualTable.scss @@ -6,8 +6,6 @@ --virtual-table-cell-vertical-padding: 5px; --virtual-table-cell-horizontal-padding: 10px; - --virtual-table-sort-icon-space: 18px; - --virtual-table-border-color: var(--g-color-base-generic-hover); --virtual-table-hover-color: var(--g-color-base-float-hover); @@ -21,6 +19,11 @@ table-layout: fixed; border-spacing: 0; border-collapse: separate; + + th, + td { + padding: 0; + } } &__row { @@ -40,107 +43,89 @@ @include sticky-top(); } - &__th { - position: relative; - - padding: var(--virtual-table-cell-vertical-padding) - var(--virtual-table-cell-horizontal-padding); - - font-weight: bold; - cursor: default; - text-align: left; + &__sort-icon-container { + display: flex; + justify-content: center; - border-bottom: $cell-border; + color: inherit; - &_sortable { - cursor: pointer; + &_shadow { + opacity: 0.15; + } + } - #{$block}__head-cell { - padding-right: var(--virtual-table-sort-icon-space); - } + &__sort-icon { + &_desc { + transform: rotate(180deg); + } + } - &#{$block}__th_align_right { - #{$block}__head-cell { - padding-right: 0; - padding-left: var(--virtual-table-sort-icon-space); - } + &__head-cell-wrapper { + display: flex; + overflow-x: hidden; - #{$block}__sort-icon { - right: auto; - left: 0; + border-bottom: $cell-border; - transform: translate(0, -50%) scaleX(-1); - } - } + &_resizeable { + resize: horizontal; } } - &__head-cell { - position: relative; - - display: inline-block; - overflow: hidden; + &__head-cell, + &__row-cell { + display: flex; + flex-direction: row; + align-items: center; - box-sizing: border-box; + width: 100%; max-width: 100%; + padding: var(--virtual-table-cell-vertical-padding) + var(--virtual-table-cell-horizontal-padding); - vertical-align: top; - white-space: nowrap; - text-overflow: ellipsis; + &_align { + &_left { + justify-content: left; + } + &_center { + justify-content: center; + } + &_right { + justify-content: right; + } + } } - &__sort-icon { - position: absolute; - top: 50%; - right: 0; + &__row-cell { + overflow-x: hidden; - display: inline-flex; + white-space: nowrap; + text-overflow: ellipsis; - color: inherit; + border-bottom: $cell-border; + } - transform: translate(0, -50%); + &__head-cell { + gap: 8px; - &_shadow { - opacity: 0.15; - } - } + font-weight: bold; + cursor: default; - &__icon { - vertical-align: top; + &_sortable { + cursor: pointer; - &_desc { - transform: rotate(180deg); + &#{$block}__head-cell_align_right { + flex-direction: row-reverse; + } } } - &__td { + // Separate head cell content class for correct text ellipsis overflow + &__head-cell-content { overflow: hidden; - padding: var(--virtual-table-cell-vertical-padding) - var(--virtual-table-cell-horizontal-padding); + width: min-content; white-space: nowrap; text-overflow: ellipsis; - - border-bottom: $cell-border; - } - - &__td, - &__th { - height: 40px; - - vertical-align: middle; - - &_align { - &_left { - text-align: left; - } - &_center { - text-align: center; - } - &_right { - text-align: right; - } - } } } diff --git a/src/components/VirtualTable/VirtualTable.tsx b/src/components/VirtualTable/VirtualTable.tsx index d360dd42ba..c126188401 100644 --- a/src/components/VirtualTable/VirtualTable.tsx +++ b/src/components/VirtualTable/VirtualTable.tsx @@ -1,5 +1,7 @@ import {useState, useReducer, useRef, useCallback, useEffect} from 'react'; +import type {HandleTableColumnsResize} from '../../utils/hooks/useTableResize'; + import type {IResponseError} from '../../types/api/error'; import {getArray} from '../../utils'; @@ -45,9 +47,12 @@ interface VirtualTableProps { rowHeight?: number; parentContainer?: Element | null; initialSortParams?: SortParams; + onColumnsResize?: HandleTableColumnsResize; + renderControls?: RenderControls; renderEmptyDataMessage?: RenderEmptyDataMessage; renderErrorMessage?: RenderErrorMessage; + dependencyArray?: unknown[]; // Fully reload table on params change } @@ -59,6 +64,7 @@ export const VirtualTable = ({ rowHeight = DEFAULT_TABLE_ROW_HEIGHT, parentContainer, initialSortParams, + onColumnsResize, renderControls, renderEmptyDataMessage, renderErrorMessage, @@ -258,7 +264,11 @@ export const VirtualTable = ({ const renderTable = () => { return ( - + {renderData()}
); diff --git a/src/components/VirtualTable/types.ts b/src/components/VirtualTable/types.ts index 549a18e5c3..e129c84fa4 100644 --- a/src/components/VirtualTable/types.ts +++ b/src/components/VirtualTable/types.ts @@ -28,6 +28,7 @@ export interface Column { header?: ReactNode; className?: string; sortable?: boolean; + resizeable?: boolean; render: (props: {row: T; index: number}) => ReactNode; width: number; align: AlignType; diff --git a/src/containers/Nodes/VirtualNodes.tsx b/src/containers/Nodes/VirtualNodes.tsx index d2e110d9c7..9899cae574 100644 --- a/src/containers/Nodes/VirtualNodes.tsx +++ b/src/containers/Nodes/VirtualNodes.tsx @@ -13,6 +13,7 @@ import { isSortableNodesProperty, isUnavailableNode, } from '../../utils/nodes'; +import {updateColumnsWidth, useTableResize} from '../../utils/hooks/useTableResize'; import {Search} from '../../components/Search'; import {ProblemFilter} from '../../components/ProblemFilter'; @@ -50,6 +51,8 @@ export const VirtualNodes = ({path, parentContainer, additionalNodesProps}: Node NodesUptimeFilterValues.All, ); + const [tableColumnsWidthSetup, setTableColumnsWidth] = useTableResize('nodesTableColumnsWidth'); + const filters = useMemo(() => { return [path, searchValue, problemFilter, uptimeFilter]; }, [path, searchValue, problemFilter, uptimeFilter]); @@ -118,7 +121,7 @@ export const VirtualNodes = ({path, parentContainer, additionalNodesProps}: Node getNodeRef: additionalNodesProps?.getNodeRef, }); - const columns = rawColumns.map((column) => { + const columns = updateColumnsWidth(rawColumns, tableColumnsWidthSetup).map((column) => { return {...column, sortable: isSortableNodesProperty(column.name)}; }); @@ -133,6 +136,7 @@ export const VirtualNodes = ({path, parentContainer, additionalNodesProps}: Node renderEmptyDataMessage={renderEmptyDataMessage} dependencyArray={filters} getRowClassName={getRowClassName} + onColumnsResize={setTableColumnsWidth} /> ); }; diff --git a/src/containers/Nodes/getNodesColumns.tsx b/src/containers/Nodes/getNodesColumns.tsx index 1a2a781a5f..ff87456c42 100644 --- a/src/containers/Nodes/getNodesColumns.tsx +++ b/src/containers/Nodes/getNodesColumns.tsx @@ -89,6 +89,7 @@ const versionColumn: NodesColumn = { return {row.Version}; }, sortable: false, + resizeable: true, }; const uptimeColumn: NodesColumn = { diff --git a/src/utils/hooks/useTableResize.ts b/src/utils/hooks/useTableResize.ts new file mode 100644 index 0000000000..c6948f76ce --- /dev/null +++ b/src/utils/hooks/useTableResize.ts @@ -0,0 +1,53 @@ +import {useCallback, useState} from 'react'; +import type {Column as DataTableColumn} from '@gravity-ui/react-data-table'; +import type {Column as VirtualTableColumn} from '../../components/VirtualTable'; +import {settingsManager} from '../../services/settings'; + +export type Column = VirtualTableColumn & DataTableColumn; + +export type TableColumnsWidthSetup = Record; + +export type HandleTableColumnsResize = (newSetup: TableColumnsWidthSetup) => void; + +export const updateColumnsWidth = ( + columns: Column[], + columnsWidthSetup: TableColumnsWidthSetup, +) => { + return columns.map((column) => { + if (!column.resizeable) { + return column; + } + return {...column, width: columnsWidthSetup[column.name] ?? column.width}; + }); +}; + +export const useTableResize = ( + localStorageKey: string, +): [TableColumnsWidthSetup, HandleTableColumnsResize] => { + const [tableColumnsWidthSetup, setTableColumnsWidth] = useState(() => { + const setupFromLS = settingsManager.readUserSettingsValue( + localStorageKey, + {}, + ) as TableColumnsWidthSetup; + + return setupFromLS; + }); + + const handleSetupChange: HandleTableColumnsResize = useCallback( + (newSetup) => { + setTableColumnsWidth((previousSetup) => { + // ResizeObserver callback may be triggered only for currently resized column + // or for the whole set of columns + const setup = { + ...previousSetup, + ...newSetup, + }; + settingsManager.setUserSettingsValue(localStorageKey, setup); + return setup; + }); + }, + [localStorageKey], + ); + + return [tableColumnsWidthSetup, handleSetupChange]; +}; From dbe2f65c1f64f4a9038627b8ee5ab662dfded50b Mon Sep 17 00:00:00 2001 From: mufazalov Date: Mon, 5 Feb 2024 12:58:01 +0300 Subject: [PATCH 2/6] fix(CellWithPopover): proper content overflow --- src/components/CellWithPopover/CellWithPopover.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/CellWithPopover/CellWithPopover.scss b/src/components/CellWithPopover/CellWithPopover.scss index 6c06efa76d..7b07524640 100644 --- a/src/components/CellWithPopover/CellWithPopover.scss +++ b/src/components/CellWithPopover/CellWithPopover.scss @@ -1,13 +1,20 @@ .ydb-cell-with-popover { display: flex; + max-width: 100%; + &__popover { display: inline-block; overflow: hidden; max-width: 100%; + vertical-align: middle; white-space: nowrap; text-overflow: ellipsis; + + .yc-popover__handler { + display: inline; + } } } From 20c1fa8e3a83b9ff69242fc6d0e7cfe0f09ae8aa Mon Sep 17 00:00:00 2001 From: mufazalov Date: Mon, 5 Feb 2024 13:28:27 +0300 Subject: [PATCH 3/6] refactor(VirtualTable): remove row cell div wrapper --- src/components/VirtualTable/TableRow.tsx | 15 +++---- src/components/VirtualTable/VirtualTable.scss | 43 +++++++++++++------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/components/VirtualTable/TableRow.tsx b/src/components/VirtualTable/TableRow.tsx index a33fdf8227..b9aa717818 100644 --- a/src/components/VirtualTable/TableRow.tsx +++ b/src/components/VirtualTable/TableRow.tsx @@ -21,16 +21,13 @@ const TableRowCell = ({ width, align = DEFAULT_ALIGN, }: TableCellProps) => { - // Additional wrapper
with explicit width to ensure proper overflow:hidden - // since overflow works poorly with + // Additional maxWidth to ensure overflow hidden for return ( - -
- {children} -
+ + {children} ); }; diff --git a/src/components/VirtualTable/VirtualTable.scss b/src/components/VirtualTable/VirtualTable.scss index d9435c353e..77441fc6c5 100644 --- a/src/components/VirtualTable/VirtualTable.scss +++ b/src/components/VirtualTable/VirtualTable.scss @@ -20,8 +20,7 @@ border-spacing: 0; border-collapse: separate; - th, - td { + th { padding: 0; } } @@ -71,8 +70,7 @@ } } - &__head-cell, - &__row-cell { + &__head-cell { display: flex; flex-direction: row; align-items: center; @@ -95,15 +93,6 @@ } } - &__row-cell { - overflow-x: hidden; - - white-space: nowrap; - text-overflow: ellipsis; - - border-bottom: $cell-border; - } - &__head-cell { gap: 8px; @@ -128,4 +117,32 @@ white-space: nowrap; text-overflow: ellipsis; } + + &__row-cell { + display: table-cell; + overflow-x: hidden; + + width: 100%; + max-width: 100%; + padding: var(--virtual-table-cell-vertical-padding) + var(--virtual-table-cell-horizontal-padding); + + vertical-align: middle; + white-space: nowrap; + text-overflow: ellipsis; + + border-bottom: $cell-border; + + &_align { + &_left { + text-align: left; + } + &_center { + text-align: center; + } + &_right { + text-align: right; + } + } + } } From c990fdd978513e89d585ff3b2bfb9c61639a534d Mon Sep 17 00:00:00 2001 From: mufazalov Date: Tue, 6 Feb 2024 12:37:41 +0300 Subject: [PATCH 4/6] refactor(TableHead): useState instead of useRef for RO --- src/components/VirtualTable/TableHead.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/VirtualTable/TableHead.tsx b/src/components/VirtualTable/TableHead.tsx index be39ce7dc9..691387e387 100644 --- a/src/components/VirtualTable/TableHead.tsx +++ b/src/components/VirtualTable/TableHead.tsx @@ -132,14 +132,15 @@ export const TableHead = ({ }: TableHeadProps) => { const [sortParams, setSortParams] = useState({}); - const resizeObserver = useRef(); + const [resizeObserver, setResizeObserver] = useState(); const isTableResizeable = Boolean(onColumnsResize); useEffect(() => { if (!isTableResizeable) { return; } - resizeObserver.current = new ResizeObserver((entries) => { + + const newResizeObserver = new ResizeObserver((entries) => { const columnsWidth: TableColumnsWidthSetup = {}; entries.forEach((entry) => { // @ts-ignore ignore custrom property usage @@ -149,6 +150,8 @@ export const TableHead = ({ onColumnsResize?.(columnsWidth); }); + + setResizeObserver(newResizeObserver); }, [onColumnsResize, isTableResizeable]); const handleSort = (columnId: string) => { @@ -206,7 +209,7 @@ export const TableHead = ({ defaultSortOrder={defaultSortOrder} onSort={handleSort} rowHeight={rowHeight} - resizeObserver={resizeObserver.current} + resizeObserver={resizeObserver} /> ); })} From bee2d000b002a91e0e6df8c6554bc62230ae8a0b Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 7 Feb 2024 15:16:24 +0300 Subject: [PATCH 5/6] refactor(TableHead): do not pass RO to cells directly --- src/components/VirtualTable/TableHead.tsx | 30 +++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/VirtualTable/TableHead.tsx b/src/components/VirtualTable/TableHead.tsx index 691387e387..63b378662f 100644 --- a/src/components/VirtualTable/TableHead.tsx +++ b/src/components/VirtualTable/TableHead.tsx @@ -1,4 +1,4 @@ -import {useEffect, useRef, useState} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; import type { HandleTableColumnsResize, @@ -49,7 +49,8 @@ interface TableHeadCellProps { defaultSortOrder: SortOrderType; onSort?: (columnName: string) => void; rowHeight: number; - resizeObserver?: ResizeObserver; + onCellMount?: (element: Element) => void; + onCellUnMount?: (element: Element) => void; } export const TableHeadCell = ({ @@ -58,21 +59,22 @@ export const TableHeadCell = ({ defaultSortOrder, onSort, rowHeight, - resizeObserver, + onCellMount, + onCellUnMount, }: TableHeadCellProps) => { const cellWrapperRef = useRef(null); useEffect(() => { const cellWrapper = cellWrapperRef.current; if (cellWrapper) { - resizeObserver?.observe(cellWrapper); + onCellMount?.(cellWrapper); } return () => { if (cellWrapper) { - resizeObserver?.unobserve(cellWrapper); + onCellUnMount?.(cellWrapper); } }; - }, [resizeObserver]); + }, [onCellMount, onCellUnMount]); const content = column.header ?? column.name; @@ -154,6 +156,19 @@ export const TableHead = ({ setResizeObserver(newResizeObserver); }, [onColumnsResize, isTableResizeable]); + const handleCellMount = useCallback( + (element: Element) => { + resizeObserver?.observe(element); + }, + [resizeObserver], + ); + const handleCellUnMount = useCallback( + (element: Element) => { + resizeObserver?.unobserve(element); + }, + [resizeObserver], + ); + const handleSort = (columnId: string) => { let newSortParams: SortParams = {}; @@ -209,7 +224,8 @@ export const TableHead = ({ defaultSortOrder={defaultSortOrder} onSort={handleSort} rowHeight={rowHeight} - resizeObserver={resizeObserver} + onCellMount={handleCellMount} + onCellUnMount={handleCellUnMount} /> ); })} From edf687fb600432370cff5e6cbb3d3fd50c9fbd26 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 7 Feb 2024 17:30:17 +0300 Subject: [PATCH 6/6] refactor(TableHead): replace useState with useMemo for RO --- src/components/VirtualTable/TableHead.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/VirtualTable/TableHead.tsx b/src/components/VirtualTable/TableHead.tsx index 63b378662f..e2b11b8050 100644 --- a/src/components/VirtualTable/TableHead.tsx +++ b/src/components/VirtualTable/TableHead.tsx @@ -1,4 +1,4 @@ -import {useCallback, useEffect, useRef, useState} from 'react'; +import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type { HandleTableColumnsResize, @@ -134,15 +134,14 @@ export const TableHead = ({ }: TableHeadProps) => { const [sortParams, setSortParams] = useState({}); - const [resizeObserver, setResizeObserver] = useState(); const isTableResizeable = Boolean(onColumnsResize); - useEffect(() => { + const resizeObserver: ResizeObserver | undefined = useMemo(() => { if (!isTableResizeable) { - return; + return undefined; } - const newResizeObserver = new ResizeObserver((entries) => { + return new ResizeObserver((entries) => { const columnsWidth: TableColumnsWidthSetup = {}; entries.forEach((entry) => { // @ts-ignore ignore custrom property usage @@ -152,8 +151,6 @@ export const TableHead = ({ onColumnsResize?.(columnsWidth); }); - - setResizeObserver(newResizeObserver); }, [onColumnsResize, isTableResizeable]); const handleCellMount = useCallback(