Skip to content

Commit 422fcfe

Browse files
authored
Merge pull request #1382 from RedisInsight/fe/feature/RI-966_virtual-table-resize
Fe/feature/ri 966 virtual table resize
2 parents 2a31530 + ddcc98c commit 422fcfe

File tree

19 files changed

+427
-132
lines changed

19 files changed

+427
-132
lines changed

redisinsight/ui/src/components/keys-summary/KeysSummary.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface Props {
1313
items: any[]
1414
scanned?: number
1515
totalItemsCount?: number
16-
nextCursor: string
16+
nextCursor?: string
1717
scanMoreStyle?: {
1818
[key: string]: string | number;
1919
}

redisinsight/ui/src/components/virtual-grid/interfaces.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
TableCellAlignment,
55
TableCellTextAlignment,
66
} from 'uiSrc/constants'
7+
import { RedisResponseBuffer } from 'uiSrc/slices/interfaces'
78

89
export interface IColumnSearchState {
910
initialSearchValue?: string
@@ -18,7 +19,7 @@ export interface IColumnSearchState {
1819
export interface ITableColumn {
1920
id: string
2021
label: string | ReactNode
21-
minWidth: number
22+
minWidth?: number
2223
maxWidth?: number
2324
isSortable?: boolean
2425
isSearchable?: boolean

redisinsight/ui/src/components/virtual-table/VirtualTable.spec.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,26 @@ describe('VirtualTable', () => {
203203
expect(onLoadMoreItems).toBeCalledWith(argMock)
204204
})
205205
})
206+
207+
it('should show resize trigger for resizable column', () => {
208+
const updatedColumns = [
209+
{
210+
...columns[0],
211+
isResizable: true,
212+
},
213+
]
214+
215+
render(
216+
<VirtualTable
217+
{...instance(mockedProps)}
218+
items={members}
219+
columns={updatedColumns}
220+
loading={false}
221+
loadMoreItems={jest.fn()}
222+
totalItemsCount={members.length}
223+
/>
224+
)
225+
226+
expect(screen.getByTestId('resize-trigger-name')).toBeInTheDocument()
227+
})
206228
})

redisinsight/ui/src/components/virtual-table/VirtualTable.tsx

Lines changed: 164 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
1-
import React, { useCallback, useEffect, useRef, useState } from 'react'
1+
import { EuiIcon, EuiProgress, EuiResizeObserver, EuiText, } from '@elastic/eui'
22
import cx from 'classnames'
3-
import { InfiniteLoader,
4-
Table,
5-
Column,
6-
IndexRange,
3+
import { findIndex, isNumber, sumBy, xor } from 'lodash'
4+
import React, { useCallback, useEffect, useRef, useState } from 'react'
5+
import {
76
CellMeasurer,
8-
TableCellProps,
97
CellMeasurerCache,
8+
Column,
9+
IndexRange,
10+
InfiniteLoader,
1011
RowMouseEventHandlerParams,
12+
Table,
13+
TableCellProps,
1114
} from 'react-virtualized'
12-
import { findIndex, isNumber, xor } from 'lodash'
13-
import {
14-
EuiText,
15-
EuiProgress,
16-
EuiResizeObserver,
17-
EuiIcon,
18-
} from '@elastic/eui'
19-
20-
import { isEqualBuffers, Maybe, Nullable } from 'uiSrc/utils'
15+
import TableColumnSearchTrigger from 'uiSrc/components/table-column-search-trigger/TableColumnSearchTrigger'
16+
import TableColumnSearch from 'uiSrc/components/table-column-search/TableColumnSearch'
2117
import { SortOrder } from 'uiSrc/constants'
2218
import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'
23-
import TableColumnSearch from 'uiSrc/components/table-column-search/TableColumnSearch'
24-
import TableColumnSearchTrigger from 'uiSrc/components/table-column-search-trigger/TableColumnSearchTrigger'
25-
import { IColumnSearchState, IProps, IResizeEvent, ITableColumn } from './interfaces'
19+
20+
import { isEqualBuffers, Maybe, Nullable } from 'uiSrc/utils'
21+
import { ColumnWidthSizes, IColumnSearchState, IProps, IResizeEvent, ITableColumn, ResizableState } from './interfaces'
2622
import KeysSummary from '../keys-summary'
2723

2824
import styles from './styles.module.scss'
2925

26+
// this is needed to align content when scrollbar appears
27+
const TABLE_OUTSIDE_WIDTH = 24
28+
3029
const VirtualTable = (props: IProps) => {
3130
const {
3231
selectable = false,
@@ -64,17 +63,25 @@ const VirtualTable = (props: IProps) => {
6463
fixedWidth: true,
6564
minHeight: rowHeight,
6665
}),
66+
onColResizeEnd
6767
} = props
6868
let selectTimer: number = 0
6969
const selectTimerDelay = 300
7070
let preventSelect = false
7171

7272
const scrollTopRef = useRef<number>(0)
73+
const resizeColRef = useRef<ResizableState>({ column: null, active: false, x: 0 })
74+
7375
const [selectedRowIndex, setSelectedRowIndex] = useState<Nullable<number>>(null)
7476
const [search, setSearch] = useState<IColumnSearchState[]>([])
7577
const [width, setWidth] = useState<number>(100)
7678
const [height, setHeight] = useState<number>(100)
7779
const [forceScrollTop, setForceScrollTop] = useState<Maybe<number>>(scrollTopProp)
80+
const [columnWidthSizes, setColumnWidthSizes] = useState<Nullable<ColumnWidthSizes>>(null)
81+
const [isColResizing, setIsColResizing] = useState(false)
82+
const [, forceUpdate] = useState({})
83+
84+
const [minWidthAllCols, setMinWidthAllCols] = useState<number>(0)
7885

7986
useEffect(() => {
8087
const searchableFields: ITableColumn[] = columns.filter(
@@ -102,6 +109,10 @@ const VirtualTable = (props: IProps) => {
102109
}
103110
}, [])
104111

112+
useEffect(() => {
113+
setMinWidthAllCols(sumBy(columns, ((col) => col?.minWidth || 0)))
114+
}, [columns])
115+
105116
useEffect(() => {
106117
if (forceScrollTop !== undefined) {
107118
setForceScrollTop(undefined)
@@ -118,6 +129,34 @@ const VirtualTable = (props: IProps) => {
118129
cellCache?.clearAll()
119130
}, [totalItemsCount])
120131

132+
const clearSelectTimeout = (timer: number = 0) => {
133+
clearTimeout(timer || selectTimer)
134+
preventSelect = true
135+
}
136+
137+
const clearCache = () => setTimeout(() => {
138+
cellCache.clearAll()
139+
forceUpdate({})
140+
}, 0)
141+
142+
const getWidthOfColumn = (colId: string, colWidth: number, width: number, isRelative = false) => {
143+
let newColWidth = isRelative ? (colWidth / 100) * width : colWidth
144+
145+
const currentColumn = columns.find((col) => col.id === colId)
146+
const maxWidthFromTable = width - (minWidthAllCols - (currentColumn?.minWidth || 0)) - TABLE_OUTSIDE_WIDTH
147+
const maxWidth = currentColumn?.maxWidth || maxWidthFromTable
148+
const minWidth = (currentColumn?.minWidth || 60)
149+
150+
if (newColWidth > maxWidth) newColWidth = maxWidth
151+
if (newColWidth < minWidth) newColWidth = minWidth
152+
153+
const newAbsWidth = Math.floor(newColWidth)
154+
return {
155+
abs: newAbsWidth,
156+
relative: (newAbsWidth / width) * 100
157+
}
158+
}
159+
121160
const onRowSelect = (data: RowMouseEventHandlerParams) => {
122161
const isRowSelectable = checkIfRowSelectable(data.rowData)
123162

@@ -132,6 +171,9 @@ const VirtualTable = (props: IProps) => {
132171
if (!preventSelect && !textSelected) {
133172
setExpandedRows(xor(expandedRows, [data.index]))
134173
onRowToggleViewClick?.(expandedRows.indexOf(data.index) === -1, data.index)
174+
175+
clearCache()
176+
setTimeout(() => { clearCache() }, 0)
135177
}
136178
preventSelect = false
137179
}, selectTimerDelay, cellCache)
@@ -144,25 +186,96 @@ const VirtualTable = (props: IProps) => {
144186
cellCache.clearAll()
145187
}
146188
}
147-
const clearSelectTimeout = (timer: number = 0) => {
148-
clearTimeout(timer || selectTimer)
149-
preventSelect = true
150-
}
151189

152-
const onScroll = useCallback(
153-
({ scrollTop }) => {
154-
scrollTopRef.current = scrollTop
155-
},
156-
[scrollTopRef],
157-
)
190+
const onScroll = useCallback(({ scrollTop }: { scrollTop: number }) => {
191+
scrollTopRef.current = scrollTop
192+
}, [scrollTopRef])
158193

159194
const onResize = ({ height, width }: IResizeEvent): void => {
160195
setHeight(height)
161-
setWidth(width)
196+
setWidth(Math.floor(width))
162197
onChangeWidth?.(width)
198+
199+
if (!columnWidthSizes) {
200+
// init width sizes
201+
setColumnWidthSizes(
202+
columns
203+
.filter((col) => col.isResizable)
204+
.reduce((prev, next) => {
205+
const propAbsWidth = next.absoluteWidth && isNumber(next.absoluteWidth) ? next.absoluteWidth : 0
206+
return {
207+
...prev,
208+
[next.id]: next.relativeWidth
209+
? getWidthOfColumn(next.id, next.relativeWidth, width, true)
210+
: getWidthOfColumn(next.id, propAbsWidth, width)
211+
}
212+
}, {})
213+
)
214+
}
215+
216+
if (columnWidthSizes) {
217+
setColumnWidthSizes((colWidthSizesPrev) => {
218+
const newSizes: ColumnWidthSizes = {}
219+
// eslint-disable-next-line guard-for-in,no-restricted-syntax
220+
for (const col in colWidthSizesPrev) {
221+
newSizes[col] = getWidthOfColumn(col, colWidthSizesPrev[col].relative, width, true)
222+
}
223+
return newSizes
224+
})
225+
}
226+
163227
cellCache?.clearAll()
164228
}
165229

230+
const onDragColumn = (e: React.MouseEvent) => {
231+
const { column, x, active } = resizeColRef.current
232+
if (active && column) {
233+
const diffX = x - e.clientX
234+
setColumnWidthSizes((prev) => {
235+
if (!prev) return null
236+
237+
resizeColRef.current.x = e.clientX
238+
return ({
239+
...prev,
240+
[column]: getWidthOfColumn(column, prev[column].abs - diffX, width)
241+
})
242+
})
243+
244+
cellCache?.clearAll()
245+
}
246+
}
247+
248+
const onDragColumnStart = (e: React.MouseEvent, column: ITableColumn) => {
249+
resizeColRef.current = {
250+
column: column.id,
251+
active: true,
252+
x: e.clientX
253+
}
254+
setIsColResizing(true)
255+
}
256+
257+
const onDragColumnEnd = () => {
258+
if (resizeColRef.current.active) {
259+
resizeColRef.current = {
260+
active: false,
261+
column: null,
262+
x: 0
263+
}
264+
setIsColResizing(false)
265+
cellCache?.clearAll()
266+
267+
if (columnWidthSizes) {
268+
onColResizeEnd?.(
269+
Object.keys(columnWidthSizes)
270+
.reduce((prev, next) => ({
271+
...prev,
272+
[next]: columnWidthSizes[next].relative
273+
}), {})
274+
)
275+
}
276+
}
277+
}
278+
166279
const checkIfRowSelectable = (rowData: any) => !!rowData
167280

168281
const cellRenderer = ({ cellData, columnIndex, rowData, rowIndex, parent, dataKey }: TableCellProps) => {
@@ -221,7 +334,10 @@ const VirtualTable = (props: IProps) => {
221334
const isColumnSorted = sortedColumn && sortedColumn.column === column.id
222335

223336
return (
224-
<div className="flex-row fluid" style={{ justifyContent: column.alignment }}>
337+
<div
338+
className="flex-row fluid"
339+
style={{ justifyContent: column.alignment, position: 'relative' }}
340+
>
225341
{column.isSortable && !searching && (
226342
<div className={styles.headerCell} style={{ justifyContent: column.alignment }}>
227343
<button
@@ -271,6 +387,14 @@ const VirtualTable = (props: IProps) => {
271387
</button>
272388
</div>
273389
)}
390+
{column.isResizable && (
391+
<div
392+
className={styles.resizeTrigger}
393+
onMouseDown={(e) => onDragColumnStart(e, column)}
394+
data-testid={`resize-trigger-${column.id}`}
395+
role="presentation"
396+
/>
397+
)}
274398
</div>
275399
)
276400
}
@@ -363,8 +487,12 @@ const VirtualTable = (props: IProps) => {
363487
{(resizeRef) => (
364488
<div
365489
ref={resizeRef}
366-
className={styles.container}
490+
className={cx(styles.container, { [styles.isResizing]: isColResizing })}
367491
onWheel={onWheel}
492+
onMouseMove={onDragColumn}
493+
onMouseUp={onDragColumnEnd}
494+
onMouseLeave={onDragColumnEnd}
495+
role="presentation"
368496
data-testid="virtual-table-container"
369497
>
370498
{loading && !hideProgress && (
@@ -381,7 +509,7 @@ const VirtualTable = (props: IProps) => {
381509
minimumBatchSize={SCAN_COUNT_DEFAULT}
382510
threshold={threshold}
383511
loadMoreRows={loadMoreRows}
384-
rowCount={totalItemsCount}
512+
rowCount={totalItemsCount || undefined}
385513
>
386514
{({ onRowsRendered, registerChild }) => (
387515
<Table
@@ -423,11 +551,9 @@ const VirtualTable = (props: IProps) => {
423551
maxWidth={column.maxWidth}
424552
label={column.label}
425553
dataKey={column.id}
426-
width={
427-
column.absoluteWidth || column.relativeWidth
428-
? column.relativeWidth ?? 0
429-
: 20
430-
}
554+
width={columnWidthSizes?.[column.id]?.abs || (
555+
column.absoluteWidth || column.relativeWidth ? column.relativeWidth ?? 0 : 20
556+
)}
431557
flexGrow={!column.absoluteWidth && !column.relativeWidth ? 1 : 0}
432558
headerRenderer={(headerProps) =>
433559
headerRenderer({
@@ -448,7 +574,7 @@ const VirtualTable = (props: IProps) => {
448574
<div className={cx(styles.tableFooter)}>
449575
<KeysSummary
450576
scanned={scanned}
451-
totalItemsCount={totalItemsCount}
577+
totalItemsCount={totalItemsCount || undefined}
452578
loading={loading}
453579
loadMoreItems={loadMoreItems}
454580
items={items}

0 commit comments

Comments
 (0)