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; + } } } diff --git a/src/components/VirtualTable/TableHead.tsx b/src/components/VirtualTable/TableHead.tsx index a75d5b1975..e2b11b8050 100644 --- a/src/components/VirtualTable/TableHead.tsx +++ b/src/components/VirtualTable/TableHead.tsx @@ -1,14 +1,21 @@ -import {useState} from 'react'; +import {useCallback, useEffect, useMemo, 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,84 @@ const ColumnSortIcon = ({sortOrder, sortable, defaultSortOrder}: ColumnSortIconP } }; +interface TableHeadCellProps { + column: Column; + sortOrder?: SortOrderType; + defaultSortOrder: SortOrderType; + onSort?: (columnName: string) => void; + rowHeight: number; + onCellMount?: (element: Element) => void; + onCellUnMount?: (element: Element) => void; +} + +export const TableHeadCell = ({ + column, + sortOrder, + defaultSortOrder, + onSort, + rowHeight, + onCellMount, + onCellUnMount, +}: TableHeadCellProps) => { + const cellWrapperRef = useRef(null); + + useEffect(() => { + const cellWrapper = cellWrapperRef.current; + if (cellWrapper) { + onCellMount?.(cellWrapper); + } + return () => { + if (cellWrapper) { + onCellUnMount?.(cellWrapper); + } + }; + }, [onCellMount, onCellUnMount]); + + 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 +128,44 @@ interface TableHeadProps { export const TableHead = ({ columns, onSort, + onColumnsResize, defaultSortOrder = DEFAULT_SORT_ORDER, rowHeight = DEFAULT_TABLE_ROW_HEIGHT, }: TableHeadProps) => { const [sortParams, setSortParams] = useState({}); + const isTableResizeable = Boolean(onColumnsResize); + + const resizeObserver: ResizeObserver | undefined = useMemo(() => { + if (!isTableResizeable) { + return undefined; + } + + return 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 handleCellMount = useCallback( + (element: Element) => { + resizeObserver?.observe(element); + }, + [resizeObserver], + ); + const handleCellUnMount = useCallback( + (element: Element) => { + resizeObserver?.unobserve(element); + }, + [resizeObserver], + ); + const handleSort = (columnId: string) => { let newSortParams: SortParams = {}; @@ -95,34 +210,20 @@ 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} + onCellMount={handleCellMount} + onCellUnMount={handleCellUnMount} + /> ); })} diff --git a/src/components/VirtualTable/TableRow.tsx b/src/components/VirtualTable/TableRow.tsx index 35e8abaeaa..b9aa717818 100644 --- a/src/components/VirtualTable/TableRow.tsx +++ b/src/components/VirtualTable/TableRow.tsx @@ -8,14 +8,25 @@ 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 maxWidth to ensure overflow hidden for return ( - + {children} ); @@ -35,6 +46,7 @@ export const LoadingTableRow = ({index, columns, height}: LoadingTableRowPro @@ -64,6 +76,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..77441fc6c5 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,10 @@ table-layout: fixed; border-spacing: 0; border-collapse: separate; + + th { + padding: 0; + } } &__row { @@ -40,96 +42,96 @@ @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; + 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; - - display: inline-flex; + &__head-cell { + gap: 8px; - color: inherit; + font-weight: bold; + cursor: default; - transform: translate(0, -50%); + &_sortable { + cursor: pointer; - &_shadow { - opacity: 0.15; + &#{$block}__head-cell_align_right { + flex-direction: row-reverse; + } } } - &__icon { - vertical-align: top; + // Separate head cell content class for correct text ellipsis overflow + &__head-cell-content { + overflow: hidden; - &_desc { - transform: rotate(180deg); - } + width: min-content; + + white-space: nowrap; + text-overflow: ellipsis; } - &__td { - overflow: hidden; + &__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; - } - - &__td, - &__th { - height: 40px; - - vertical-align: middle; &_align { &_left { 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]; +};