From 48c44310031a679541b7d9e23031db7c3a9e14e6 Mon Sep 17 00:00:00 2001 From: Julien Pinsonneau Date: Tue, 22 Nov 2022 14:35:39 +0100 Subject: [PATCH 1/2] columns resize --- .../modals/__tests__/columns-modal.spec.tsx | 1 + web/src/components/modals/columns-modal.tsx | 22 ++++--- .../__tests__/netflow-table-header.spec.tsx | 12 +++- .../__tests__/netflow-table.spec.tsx | 4 +- .../netflow-table/netflow-table-header.css | 14 +++++ .../netflow-table/netflow-table-header.tsx | 62 +++++++++++++++++-- .../netflow-table/netflow-table.tsx | 21 ++++++- web/src/components/netflow-traffic.tsx | 7 ++- web/src/utils/columns.ts | 4 ++ web/src/utils/local-storage-hook.ts | 1 + 10 files changed, 127 insertions(+), 21 deletions(-) diff --git a/web/src/components/modals/__tests__/columns-modal.spec.tsx b/web/src/components/modals/__tests__/columns-modal.spec.tsx index 8accc3024..375573210 100644 --- a/web/src/components/modals/__tests__/columns-modal.spec.tsx +++ b/web/src/components/modals/__tests__/columns-modal.spec.tsx @@ -10,6 +10,7 @@ describe('', () => { setModalOpen: jest.fn(), columns: ShuffledDefaultColumns, setColumns: jest.fn(), + setColumnSizes: jest.fn(), id: 'columns-modal' }; it('should render component', async () => { diff --git a/web/src/components/modals/columns-modal.tsx b/web/src/components/modals/columns-modal.tsx index a6a022dfb..f4cf28210 100644 --- a/web/src/components/modals/columns-modal.tsx +++ b/web/src/components/modals/columns-modal.tsx @@ -1,35 +1,36 @@ -import * as React from 'react'; import { Button, DataList, + DataListCell, + DataListCheck, DataListControl, + DataListDragButton, DataListItem, + DataListItemCells, DataListItemRow, - DataListDragButton, - DataListCheck, - DataListCell, DragDrop, Draggable, Droppable, - DataListItemCells, Text, TextContent, TextVariants, Tooltip } from '@patternfly/react-core'; -import Modal from './modal'; -import { useTranslation } from 'react-i18next'; -import { Column, getDefaultColumns, getFullColumnName } from '../../utils/columns'; import * as _ from 'lodash'; +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Column, ColumnSizeMap, getDefaultColumns, getFullColumnName } from '../../utils/columns'; import './columns-modal.css'; +import Modal from './modal'; export const ColumnsModal: React.FC<{ isModalOpen: boolean; setModalOpen: (v: boolean) => void; columns: Column[]; setColumns: (v: Column[]) => void; + setColumnSizes: (v: ColumnSizeMap) => void; id?: string; -}> = ({ id, isModalOpen, setModalOpen, columns, setColumns }) => { +}> = ({ id, isModalOpen, setModalOpen, columns, setColumns, setColumnSizes }) => { const [updatedColumns, setUpdatedColumns] = React.useState([]); const [isSaveDisabled, setSaveDisabled] = React.useState(true); const [isAllSelected, setAllSelected] = React.useState(false); @@ -80,8 +81,9 @@ export const ColumnsModal: React.FC<{ ); const onReset = React.useCallback(() => { + setColumnSizes({}); setUpdatedColumns(getDefaultColumns(t)); - }, [setUpdatedColumns, t]); + }, [setColumnSizes, setUpdatedColumns, t]); const onSelectAll = React.useCallback(() => { const result = [...updatedColumns]; diff --git a/web/src/components/netflow-table/__tests__/netflow-table-header.spec.tsx b/web/src/components/netflow-table/__tests__/netflow-table-header.spec.tsx index 2d251e710..2a0e6f919 100644 --- a/web/src/components/netflow-table/__tests__/netflow-table-header.spec.tsx +++ b/web/src/components/netflow-table/__tests__/netflow-table-header.spec.tsx @@ -1,7 +1,7 @@ import { SortByDirection, TableComposable, Tbody, Th, Thead, Tr } from '@patternfly/react-table'; import { mount } from 'enzyme'; import * as React from 'react'; -import { Column, ColumnsId } from '../../../utils/columns'; +import { Column, ColumnsId, ColumnSizeMap } from '../../../utils/columns'; import { AllSelectedColumns, DefaultColumns, filterOrderedColumnsByIds } from '../../__tests-data__/columns'; import { NetflowTableHeader } from '../netflow-table-header'; @@ -11,7 +11,9 @@ const NetflowTableHeaderWrapper: React.FC<{ sortDirection: SortByDirection; columns: Column[]; setColumns: (v: Column[]) => void; -}> = ({ onSort, sortId, sortDirection, columns, setColumns }) => { + columnSizes: ColumnSizeMap; + setColumnSizes: (v: ColumnSizeMap) => void; +}> = ({ onSort, sortId, sortDirection, columns, setColumns, columnSizes, setColumnSizes }) => { return ( @@ -33,7 +37,9 @@ describe('', () => { sortId: ColumnsId.endtime, sortDirection: SortByDirection.asc, tableWidth: 100, - setColumns: jest.fn() + setColumns: jest.fn(), + columnSizes: {}, + setColumnSizes: jest.fn() }; it('should render component', async () => { const wrapper = mount(); diff --git a/web/src/components/netflow-table/__tests__/netflow-table.spec.tsx b/web/src/components/netflow-table/__tests__/netflow-table.spec.tsx index 1aefd3b85..9152dde9e 100644 --- a/web/src/components/netflow-table/__tests__/netflow-table.spec.tsx +++ b/web/src/components/netflow-table/__tests__/netflow-table.spec.tsx @@ -22,7 +22,9 @@ describe('', () => { size: 'm' as Size, onSelect: jest.fn(), filterActionLinks: <>, - setColumns: jest.fn() + setColumns: jest.fn(), + columnSizes: {}, + setColumnSizes: jest.fn() }; it('should render component', async () => { diff --git a/web/src/components/netflow-table/netflow-table-header.css b/web/src/components/netflow-table/netflow-table-header.css index 116846ea8..f756f6067 100644 --- a/web/src/components/netflow-table/netflow-table-header.css +++ b/web/src/components/netflow-table/netflow-table-header.css @@ -2,6 +2,16 @@ th.netobserv-header { cursor: pointer; } +th.netobserv-header.column:hover:not(.dragged)::after { + position: absolute; + top: 0; + right: 0; + content: ' '; + cursor: col-resize; + height: 100%; + width: 15px; +} + th.netobserv-header.dragged:not(.dark) { opacity: 0.25; background: #fff; @@ -12,6 +22,10 @@ th.netobserv-header.dragged.dark { background: #1b1d21; } +th.netobserv-header.resizing { + border-bottom: solid #0066CC; +} + th.netobserv-header.dropzone:not(.dragged) { box-shadow: inset 5px 0px 0px 0px #0066CC; } \ No newline at end of file diff --git a/web/src/components/netflow-table/netflow-table-header.tsx b/web/src/components/netflow-table/netflow-table-header.tsx index d0a6e1da7..3830ab9d9 100644 --- a/web/src/components/netflow-table/netflow-table-header.tsx +++ b/web/src/components/netflow-table/netflow-table-header.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; import { SortByDirection, Th, Thead, Tr } from '@patternfly/react-table'; import _ from 'lodash'; -import { Column, ColumnGroup, ColumnsId, getColumnGroups, getFullColumnName } from '../../utils/columns'; +import * as React from 'react'; +import { Column, ColumnGroup, ColumnsId, ColumnSizeMap, getColumnGroups, getFullColumnName } from '../../utils/columns'; import './netflow-table-header.css'; export type HeadersState = { @@ -10,15 +10,24 @@ export type HeadersState = { headers: Column[]; }; +export type ResizedElement = { + target: HTMLElement; + startClientX: number; + startClentWidth: number; +}; + export const NetflowTableHeader: React.FC<{ onSort: (id: ColumnsId, direction: SortByDirection) => void; sortId: ColumnsId; sortDirection: SortByDirection; columns: Column[]; setColumns: (v: Column[]) => void; + columnSizes: ColumnSizeMap; + setColumnSizes: (v: ColumnSizeMap) => void; tableWidth: number; isDark?: boolean; -}> = ({ onSort, sortId, sortDirection, columns, setColumns, tableWidth, isDark }) => { +}> = ({ onSort, sortId, sortDirection, columns, setColumns, columnSizes, setColumnSizes, tableWidth, isDark }) => { + const resizedElement = React.useRef(); const draggedElement = React.useRef(); const [headersState, setHeadersState] = React.useState({ @@ -27,6 +36,48 @@ export const NetflowTableHeader: React.FC<{ headers: [] }); + const mouseEvent = React.useCallback( + (e: MouseEvent) => { + const diffPx = e.clientX - resizedElement.current!.startClientX; + switch (e.type) { + case 'mousemove': + const minWidth = Number(resizedElement.current!.target.style.minWidth?.replace('px', '')) || 0; + if (Math.abs(minWidth - diffPx) > 10) { + const minWidth = `${resizedElement.current!.startClentWidth + diffPx}px`; + columnSizes[resizedElement.current!.target.id as ColumnsId] = minWidth; + resizedElement.current!.target.style.minWidth = minWidth; + } + break; + default: + document.getElementById('cursor-style')!.remove(); + resizedElement.current!.target.classList.remove('resizing'); + document.removeEventListener('mousemove', mouseEvent); + document.removeEventListener('mouseup', mouseEvent); + setColumnSizes(columnSizes); + break; + } + }, + [columnSizes, setColumnSizes] + ); + + const onMouseDown = React.useCallback( + (e: React.MouseEvent) => { + const target = e.currentTarget; + if (target.classList.contains('column') && e.nativeEvent.offsetX > target.clientWidth - 15) { + target.classList.add('resizing'); + const cursorStyle = document.createElement('style'); + cursorStyle.innerHTML = '*{cursor: col-resize!important;}'; + cursorStyle.id = 'cursor-style'; + document.head.appendChild(cursorStyle); + resizedElement.current = { target, startClientX: e.clientX, startClentWidth: target.clientWidth }; + document.addEventListener('mousemove', mouseEvent); + document.addEventListener('mouseup', mouseEvent); + e.preventDefault(); + } + }, + [mouseEvent] + ); + const onDragStart = React.useCallback((e: React.DragEvent) => { const target = e.currentTarget; target.classList.add('dragged'); @@ -126,6 +177,7 @@ export const NetflowTableHeader: React.FC<{ }} colSpan={1} draggable + onMouseDown={onMouseDown} onDragStart={onDragStart} onDragOver={e => { if (draggedElement.current?.classList.contains('column')) { @@ -140,7 +192,7 @@ export const NetflowTableHeader: React.FC<{ onDrop={onDrop} onDragEnd={clearDragEffects} modifier="wrap" - style={{ width: `${Math.floor((100 * c.width) / tableWidth)}%` }} + style={{ width: `${Math.floor((100 * c.width) / tableWidth)}%`, minWidth: columnSizes[c.id] }} info={c.tooltip ? { tooltip: c.tooltip } : undefined} > {headersState.useNested ? c.name : getFullColumnName(c)} @@ -148,12 +200,14 @@ export const NetflowTableHeader: React.FC<{ ); }, [ + columnSizes, columns, headersState.nestedHeaders, headersState.useNested, isDark, onDragStart, onDrop, + onMouseDown, onSort, sortDirection, sortId, diff --git a/web/src/components/netflow-table/netflow-table.tsx b/web/src/components/netflow-table/netflow-table.tsx index 3999068ec..a5b45b39d 100644 --- a/web/src/components/netflow-table/netflow-table.tsx +++ b/web/src/components/netflow-table/netflow-table.tsx @@ -16,7 +16,7 @@ import * as _ from 'lodash'; import { Record } from '../../api/ipfix'; import { NetflowTableHeader } from './netflow-table-header'; import NetflowTableRow from './netflow-table-row'; -import { Column, ColumnsId, getCommonColumns } from '../../utils/columns'; +import { Column, ColumnsId, ColumnSizeMap, getCommonColumns } from '../../utils/columns'; import { Size } from '../dropdowns/table-display-dropdown'; import { usePrevious } from '../../utils/previous-hook'; import './netflow-table.css'; @@ -32,13 +32,28 @@ const NetflowTable: React.FC<{ selectedRecord?: Record; columns: Column[]; setColumns: (v: Column[]) => void; + columnSizes: ColumnSizeMap; + setColumnSizes: (v: ColumnSizeMap) => void; size: Size; onSelect: (record?: Record) => void; loading?: boolean; error?: string; filterActionLinks: JSX.Element; isDark?: boolean; -}> = ({ flows, selectedRecord, columns, setColumns, error, loading, size, onSelect, filterActionLinks, isDark }) => { +}> = ({ + flows, + selectedRecord, + columns, + setColumns, + columnSizes, + setColumnSizes, + error, + loading, + size, + onSelect, + filterActionLinks, + isDark +}) => { const { t } = useTranslation('plugin__netobserv-plugin'); //default to 300 to allow content to be rendered in tests @@ -254,6 +269,8 @@ const NetflowTable: React.FC<{ sortId={activeSortId} columns={columns} setColumns={setColumns} + columnSizes={columnSizes} + setColumnSizes={setColumnSizes} tableWidth={width} isDark={isDark} /> diff --git a/web/src/components/netflow-traffic.tsx b/web/src/components/netflow-traffic.tsx index 83743c648..cb870eeb3 100644 --- a/web/src/components/netflow-traffic.tsx +++ b/web/src/components/netflow-traffic.tsx @@ -62,7 +62,7 @@ import { TopologyGroupTypes, TopologyOptions } from '../model/topology'; -import { Column, getDefaultColumns } from '../utils/columns'; +import { Column, ColumnSizeMap, getDefaultColumns } from '../utils/columns'; import { loadConfig } from '../utils/config'; import { ContextSingleton } from '../utils/context'; import { computeStepInterval, TimeRange } from '../utils/datetime'; @@ -70,6 +70,7 @@ import { getHTTPErrorDetails } from '../utils/errors'; import { useK8sModelsWithColors } from '../utils/k8s-models-hook'; import { LOCAL_STORAGE_COLS_KEY, + LOCAL_STORAGE_COLS_SIZES_KEY, LOCAL_STORAGE_DISABLED_FILTERS_KEY, LOCAL_STORAGE_LAST_LIMIT_KEY, LOCAL_STORAGE_LAST_TOP_KEY, @@ -215,6 +216,7 @@ export const NetflowTraffic: React.FC<{ id: 'id', criteria: 'isSelected' }); + const [columnSizes, setColumnSizes] = useLocalStorage(LOCAL_STORAGE_COLS_SIZES_KEY, {}); React.useEffect(() => { loadConfig().then(setConfig); @@ -829,6 +831,8 @@ export const NetflowTraffic: React.FC<{ onSelect={onRecordSelect} columns={columns.filter(col => col.isSelected)} setColumns={(v: Column[]) => setColumns(v.concat(columns.filter(col => !col.isSelected)))} + columnSizes={columnSizes} + setColumnSizes={setColumnSizes} filterActionLinks={filterLinks()} isDark={isDarkTheme} /> @@ -1053,6 +1057,7 @@ export const NetflowTraffic: React.FC<{ setModalOpen={setColModalOpen} columns={columns} setColumns={setColumns} + setColumnSizes={setColumnSizes} /> Date: Fri, 2 Dec 2022 10:16:45 +0100 Subject: [PATCH 2/2] reset sizes on save --- web/src/components/modals/columns-modal.tsx | 23 ++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/web/src/components/modals/columns-modal.tsx b/web/src/components/modals/columns-modal.tsx index f4cf28210..96f8f7c49 100644 --- a/web/src/components/modals/columns-modal.tsx +++ b/web/src/components/modals/columns-modal.tsx @@ -31,6 +31,7 @@ export const ColumnsModal: React.FC<{ setColumnSizes: (v: ColumnSizeMap) => void; id?: string; }> = ({ id, isModalOpen, setModalOpen, columns, setColumns, setColumnSizes }) => { + const [resetClicked, setResetClicked] = React.useState(false); const [updatedColumns, setUpdatedColumns] = React.useState([]); const [isSaveDisabled, setSaveDisabled] = React.useState(true); const [isAllSelected, setAllSelected] = React.useState(false); @@ -81,9 +82,9 @@ export const ColumnsModal: React.FC<{ ); const onReset = React.useCallback(() => { - setColumnSizes({}); + setResetClicked(true); setUpdatedColumns(getDefaultColumns(t)); - }, [setColumnSizes, setUpdatedColumns, t]); + }, [setResetClicked, setUpdatedColumns, t]); const onSelectAll = React.useCallback(() => { const result = [...updatedColumns]; @@ -93,10 +94,18 @@ export const ColumnsModal: React.FC<{ setUpdatedColumns(result); }, [updatedColumns, setUpdatedColumns, isAllSelected]); + const onClose = React.useCallback(() => { + setResetClicked(false); + setModalOpen(false); + }, [setModalOpen]); + const onSave = React.useCallback(() => { + if (resetClicked) { + setColumnSizes({}); + } setColumns(updatedColumns); - setModalOpen(false); - }, [updatedColumns, setColumns, setModalOpen]); + onClose(); + }, [resetClicked, setColumns, updatedColumns, onClose, setColumnSizes]); const draggableItems = updatedColumns.map((column, i) => ( @@ -136,7 +145,7 @@ export const ColumnsModal: React.FC<{ title={t('Manage columns')} isOpen={isModalOpen} scrollable={true} - onClose={() => setModalOpen(false)} + onClose={onClose} description={ @@ -153,10 +162,10 @@ export const ColumnsModal: React.FC<{ - - +