Skip to content

Commit 8886986

Browse files
authored
Merge pull request #238 from jpinsonneau/609
NETOBSERV-609 UI: Resize a column in flow table
2 parents 0256276 + 993ac32 commit 8886986

File tree

10 files changed

+141
-26
lines changed

10 files changed

+141
-26
lines changed

web/src/components/modals/__tests__/columns-modal.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('<ColumnsModal />', () => {
1010
setModalOpen: jest.fn(),
1111
columns: ShuffledDefaultColumns,
1212
setColumns: jest.fn(),
13+
setColumnSizes: jest.fn(),
1314
id: 'columns-modal'
1415
};
1516
it('should render component', async () => {

web/src/components/modals/columns-modal.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,37 @@
1-
import * as React from 'react';
21
import {
32
Button,
43
DataList,
4+
DataListCell,
5+
DataListCheck,
56
DataListControl,
7+
DataListDragButton,
68
DataListItem,
9+
DataListItemCells,
710
DataListItemRow,
8-
DataListDragButton,
9-
DataListCheck,
10-
DataListCell,
1111
DragDrop,
1212
Draggable,
1313
Droppable,
14-
DataListItemCells,
1514
Text,
1615
TextContent,
1716
TextVariants,
1817
Tooltip
1918
} from '@patternfly/react-core';
20-
import Modal from './modal';
21-
import { useTranslation } from 'react-i18next';
22-
import { Column, getDefaultColumns, getFullColumnName } from '../../utils/columns';
2319
import * as _ from 'lodash';
20+
import * as React from 'react';
21+
import { useTranslation } from 'react-i18next';
22+
import { Column, ColumnSizeMap, getDefaultColumns, getFullColumnName } from '../../utils/columns';
2423
import './columns-modal.css';
24+
import Modal from './modal';
2525

2626
export const ColumnsModal: React.FC<{
2727
isModalOpen: boolean;
2828
setModalOpen: (v: boolean) => void;
2929
columns: Column[];
3030
setColumns: (v: Column[]) => void;
31+
setColumnSizes: (v: ColumnSizeMap) => void;
3132
id?: string;
32-
}> = ({ id, isModalOpen, setModalOpen, columns, setColumns }) => {
33+
}> = ({ id, isModalOpen, setModalOpen, columns, setColumns, setColumnSizes }) => {
34+
const [resetClicked, setResetClicked] = React.useState<boolean>(false);
3335
const [updatedColumns, setUpdatedColumns] = React.useState<Column[]>([]);
3436
const [isSaveDisabled, setSaveDisabled] = React.useState<boolean>(true);
3537
const [isAllSelected, setAllSelected] = React.useState<boolean>(false);
@@ -80,8 +82,9 @@ export const ColumnsModal: React.FC<{
8082
);
8183

8284
const onReset = React.useCallback(() => {
85+
setResetClicked(true);
8386
setUpdatedColumns(getDefaultColumns(t));
84-
}, [setUpdatedColumns, t]);
87+
}, [setResetClicked, setUpdatedColumns, t]);
8588

8689
const onSelectAll = React.useCallback(() => {
8790
const result = [...updatedColumns];
@@ -91,10 +94,18 @@ export const ColumnsModal: React.FC<{
9194
setUpdatedColumns(result);
9295
}, [updatedColumns, setUpdatedColumns, isAllSelected]);
9396

97+
const onClose = React.useCallback(() => {
98+
setResetClicked(false);
99+
setModalOpen(false);
100+
}, [setModalOpen]);
101+
94102
const onSave = React.useCallback(() => {
103+
if (resetClicked) {
104+
setColumnSizes({});
105+
}
95106
setColumns(updatedColumns);
96-
setModalOpen(false);
97-
}, [updatedColumns, setColumns, setModalOpen]);
107+
onClose();
108+
}, [resetClicked, setColumns, updatedColumns, onClose, setColumnSizes]);
98109

99110
const draggableItems = updatedColumns.map((column, i) => (
100111
<Draggable key={i} hasNoWrapper>
@@ -134,7 +145,7 @@ export const ColumnsModal: React.FC<{
134145
title={t('Manage columns')}
135146
isOpen={isModalOpen}
136147
scrollable={true}
137-
onClose={() => setModalOpen(false)}
148+
onClose={onClose}
138149
description={
139150
<TextContent>
140151
<Text component={TextVariants.p}>
@@ -151,10 +162,10 @@ export const ColumnsModal: React.FC<{
151162
<Button data-test="columns-reset-button" key="reset" variant="link" onClick={() => onReset()}>
152163
{t('Restore default columns')}
153164
</Button>
154-
<Button data-test="columns-cancel-button" key="cancel" variant="link" onClick={() => setModalOpen(false)}>
165+
<Button data-test="columns-cancel-button" key="cancel" variant="link" onClick={() => onClose()}>
155166
{t('Cancel')}
156167
</Button>
157-
<Tooltip content={t('At least one column must be selected')} isVisible={isSaveDisabled}>
168+
<Tooltip content={t('At least one column must be selected')} trigger="" isVisible={isSaveDisabled}>
158169
<Button
159170
data-test="columns-save-button"
160171
isDisabled={isSaveDisabled}

web/src/components/netflow-table/__tests__/netflow-table-header.spec.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { SortByDirection, TableComposable, Tbody, Th, Thead, Tr } from '@patternfly/react-table';
22
import { mount } from 'enzyme';
33
import * as React from 'react';
4-
import { Column, ColumnsId } from '../../../utils/columns';
4+
import { Column, ColumnsId, ColumnSizeMap } from '../../../utils/columns';
55
import { AllSelectedColumns, DefaultColumns, filterOrderedColumnsByIds } from '../../__tests-data__/columns';
66
import { NetflowTableHeader } from '../netflow-table-header';
77

@@ -11,7 +11,9 @@ const NetflowTableHeaderWrapper: React.FC<{
1111
sortDirection: SortByDirection;
1212
columns: Column[];
1313
setColumns: (v: Column[]) => void;
14-
}> = ({ onSort, sortId, sortDirection, columns, setColumns }) => {
14+
columnSizes: ColumnSizeMap;
15+
setColumnSizes: (v: ColumnSizeMap) => void;
16+
}> = ({ onSort, sortId, sortDirection, columns, setColumns, columnSizes, setColumnSizes }) => {
1517
return (
1618
<TableComposable aria-label="Misc table" variant="compact">
1719
<NetflowTableHeader
@@ -20,6 +22,8 @@ const NetflowTableHeaderWrapper: React.FC<{
2022
sortId={sortId}
2123
columns={columns}
2224
setColumns={setColumns}
25+
columnSizes={columnSizes}
26+
setColumnSizes={setColumnSizes}
2327
tableWidth={100}
2428
/>
2529
<Tbody></Tbody>
@@ -33,7 +37,9 @@ describe('<NetflowTableHeader />', () => {
3337
sortId: ColumnsId.endtime,
3438
sortDirection: SortByDirection.asc,
3539
tableWidth: 100,
36-
setColumns: jest.fn()
40+
setColumns: jest.fn(),
41+
columnSizes: {},
42+
setColumnSizes: jest.fn()
3743
};
3844
it('should render component', async () => {
3945
const wrapper = mount(<NetflowTableHeaderWrapper {...mocks} columns={AllSelectedColumns} />);

web/src/components/netflow-table/__tests__/netflow-table.spec.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ describe('<NetflowTable />', () => {
2222
size: 'm' as Size,
2323
onSelect: jest.fn(),
2424
filterActionLinks: <></>,
25-
setColumns: jest.fn()
25+
setColumns: jest.fn(),
26+
columnSizes: {},
27+
setColumnSizes: jest.fn()
2628
};
2729

2830
it('should render component', async () => {

web/src/components/netflow-table/netflow-table-header.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ th.netobserv-header {
22
cursor: pointer;
33
}
44

5+
th.netobserv-header.column:hover:not(.dragged)::after {
6+
position: absolute;
7+
top: 0;
8+
right: 0;
9+
content: ' ';
10+
cursor: col-resize;
11+
height: 100%;
12+
width: 15px;
13+
}
14+
515
th.netobserv-header.dragged:not(.dark) {
616
opacity: 0.25;
717
background: #fff;
@@ -12,6 +22,10 @@ th.netobserv-header.dragged.dark {
1222
background: #1b1d21;
1323
}
1424

25+
th.netobserv-header.resizing {
26+
border-bottom: solid #0066CC;
27+
}
28+
1529
th.netobserv-header.dropzone:not(.dragged) {
1630
box-shadow: inset 5px 0px 0px 0px #0066CC;
1731
}

web/src/components/netflow-table/netflow-table-header.tsx

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import * as React from 'react';
21
import { SortByDirection, Th, Thead, Tr } from '@patternfly/react-table';
32
import _ from 'lodash';
4-
import { Column, ColumnGroup, ColumnsId, getColumnGroups, getFullColumnName } from '../../utils/columns';
3+
import * as React from 'react';
4+
import { Column, ColumnGroup, ColumnsId, ColumnSizeMap, getColumnGroups, getFullColumnName } from '../../utils/columns';
55
import './netflow-table-header.css';
66

77
export type HeadersState = {
@@ -10,15 +10,24 @@ export type HeadersState = {
1010
headers: Column[];
1111
};
1212

13+
export type ResizedElement = {
14+
target: HTMLElement;
15+
startClientX: number;
16+
startClentWidth: number;
17+
};
18+
1319
export const NetflowTableHeader: React.FC<{
1420
onSort: (id: ColumnsId, direction: SortByDirection) => void;
1521
sortId: ColumnsId;
1622
sortDirection: SortByDirection;
1723
columns: Column[];
1824
setColumns: (v: Column[]) => void;
25+
columnSizes: ColumnSizeMap;
26+
setColumnSizes: (v: ColumnSizeMap) => void;
1927
tableWidth: number;
2028
isDark?: boolean;
21-
}> = ({ onSort, sortId, sortDirection, columns, setColumns, tableWidth, isDark }) => {
29+
}> = ({ onSort, sortId, sortDirection, columns, setColumns, columnSizes, setColumnSizes, tableWidth, isDark }) => {
30+
const resizedElement = React.useRef<ResizedElement>();
2231
const draggedElement = React.useRef<HTMLElement>();
2332

2433
const [headersState, setHeadersState] = React.useState<HeadersState>({
@@ -27,6 +36,48 @@ export const NetflowTableHeader: React.FC<{
2736
headers: []
2837
});
2938

39+
const mouseEvent = React.useCallback(
40+
(e: MouseEvent) => {
41+
const diffPx = e.clientX - resizedElement.current!.startClientX;
42+
switch (e.type) {
43+
case 'mousemove':
44+
const minWidth = Number(resizedElement.current!.target.style.minWidth?.replace('px', '')) || 0;
45+
if (Math.abs(minWidth - diffPx) > 10) {
46+
const minWidth = `${resizedElement.current!.startClentWidth + diffPx}px`;
47+
columnSizes[resizedElement.current!.target.id as ColumnsId] = minWidth;
48+
resizedElement.current!.target.style.minWidth = minWidth;
49+
}
50+
break;
51+
default:
52+
document.getElementById('cursor-style')!.remove();
53+
resizedElement.current!.target.classList.remove('resizing');
54+
document.removeEventListener('mousemove', mouseEvent);
55+
document.removeEventListener('mouseup', mouseEvent);
56+
setColumnSizes(columnSizes);
57+
break;
58+
}
59+
},
60+
[columnSizes, setColumnSizes]
61+
);
62+
63+
const onMouseDown = React.useCallback(
64+
(e: React.MouseEvent<HTMLElement>) => {
65+
const target = e.currentTarget;
66+
if (target.classList.contains('column') && e.nativeEvent.offsetX > target.clientWidth - 15) {
67+
target.classList.add('resizing');
68+
const cursorStyle = document.createElement('style');
69+
cursorStyle.innerHTML = '*{cursor: col-resize!important;}';
70+
cursorStyle.id = 'cursor-style';
71+
document.head.appendChild(cursorStyle);
72+
resizedElement.current = { target, startClientX: e.clientX, startClentWidth: target.clientWidth };
73+
document.addEventListener('mousemove', mouseEvent);
74+
document.addEventListener('mouseup', mouseEvent);
75+
e.preventDefault();
76+
}
77+
},
78+
[mouseEvent]
79+
);
80+
3081
const onDragStart = React.useCallback((e: React.DragEvent<HTMLElement>) => {
3182
const target = e.currentTarget;
3283
target.classList.add('dragged');
@@ -126,6 +177,7 @@ export const NetflowTableHeader: React.FC<{
126177
}}
127178
colSpan={1}
128179
draggable
180+
onMouseDown={onMouseDown}
129181
onDragStart={onDragStart}
130182
onDragOver={e => {
131183
if (draggedElement.current?.classList.contains('column')) {
@@ -140,20 +192,22 @@ export const NetflowTableHeader: React.FC<{
140192
onDrop={onDrop}
141193
onDragEnd={clearDragEffects}
142194
modifier="wrap"
143-
style={{ width: `${Math.floor((100 * c.width) / tableWidth)}%` }}
195+
style={{ width: `${Math.floor((100 * c.width) / tableWidth)}%`, minWidth: columnSizes[c.id] }}
144196
info={c.tooltip ? { tooltip: c.tooltip } : undefined}
145197
>
146198
{headersState.useNested ? c.name : getFullColumnName(c)}
147199
</Th>
148200
);
149201
},
150202
[
203+
columnSizes,
151204
columns,
152205
headersState.nestedHeaders,
153206
headersState.useNested,
154207
isDark,
155208
onDragStart,
156209
onDrop,
210+
onMouseDown,
157211
onSort,
158212
sortDirection,
159213
sortId,

web/src/components/netflow-table/netflow-table.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as _ from 'lodash';
1616
import { Record } from '../../api/ipfix';
1717
import { NetflowTableHeader } from './netflow-table-header';
1818
import NetflowTableRow from './netflow-table-row';
19-
import { Column, ColumnsId, getCommonColumns } from '../../utils/columns';
19+
import { Column, ColumnsId, ColumnSizeMap, getCommonColumns } from '../../utils/columns';
2020
import { Size } from '../dropdowns/table-display-dropdown';
2121
import { usePrevious } from '../../utils/previous-hook';
2222
import './netflow-table.css';
@@ -32,13 +32,28 @@ const NetflowTable: React.FC<{
3232
selectedRecord?: Record;
3333
columns: Column[];
3434
setColumns: (v: Column[]) => void;
35+
columnSizes: ColumnSizeMap;
36+
setColumnSizes: (v: ColumnSizeMap) => void;
3537
size: Size;
3638
onSelect: (record?: Record) => void;
3739
loading?: boolean;
3840
error?: string;
3941
filterActionLinks: JSX.Element;
4042
isDark?: boolean;
41-
}> = ({ flows, selectedRecord, columns, setColumns, error, loading, size, onSelect, filterActionLinks, isDark }) => {
43+
}> = ({
44+
flows,
45+
selectedRecord,
46+
columns,
47+
setColumns,
48+
columnSizes,
49+
setColumnSizes,
50+
error,
51+
loading,
52+
size,
53+
onSelect,
54+
filterActionLinks,
55+
isDark
56+
}) => {
4257
const { t } = useTranslation('plugin__netobserv-plugin');
4358

4459
//default to 300 to allow content to be rendered in tests
@@ -254,6 +269,8 @@ const NetflowTable: React.FC<{
254269
sortId={activeSortId}
255270
columns={columns}
256271
setColumns={setColumns}
272+
columnSizes={columnSizes}
273+
setColumnSizes={setColumnSizes}
257274
tableWidth={width}
258275
isDark={isDark}
259276
/>

web/src/components/netflow-traffic.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,15 @@ import {
6262
TopologyGroupTypes,
6363
TopologyOptions
6464
} from '../model/topology';
65-
import { Column, getDefaultColumns } from '../utils/columns';
65+
import { Column, ColumnSizeMap, getDefaultColumns } from '../utils/columns';
6666
import { loadConfig } from '../utils/config';
6767
import { ContextSingleton } from '../utils/context';
6868
import { computeStepInterval, TimeRange } from '../utils/datetime';
6969
import { getHTTPErrorDetails } from '../utils/errors';
7070
import { useK8sModelsWithColors } from '../utils/k8s-models-hook';
7171
import {
7272
LOCAL_STORAGE_COLS_KEY,
73+
LOCAL_STORAGE_COLS_SIZES_KEY,
7374
LOCAL_STORAGE_DISABLED_FILTERS_KEY,
7475
LOCAL_STORAGE_LAST_LIMIT_KEY,
7576
LOCAL_STORAGE_LAST_TOP_KEY,
@@ -215,6 +216,7 @@ export const NetflowTraffic: React.FC<{
215216
id: 'id',
216217
criteria: 'isSelected'
217218
});
219+
const [columnSizes, setColumnSizes] = useLocalStorage<ColumnSizeMap>(LOCAL_STORAGE_COLS_SIZES_KEY, {});
218220

219221
React.useEffect(() => {
220222
loadConfig().then(setConfig);
@@ -848,6 +850,8 @@ export const NetflowTraffic: React.FC<{
848850
onSelect={onRecordSelect}
849851
columns={columns.filter(col => col.isSelected)}
850852
setColumns={(v: Column[]) => setColumns(v.concat(columns.filter(col => !col.isSelected)))}
853+
columnSizes={columnSizes}
854+
setColumnSizes={setColumnSizes}
851855
filterActionLinks={filterLinks()}
852856
isDark={isDarkTheme}
853857
/>
@@ -1074,6 +1078,7 @@ export const NetflowTraffic: React.FC<{
10741078
setModalOpen={setColModalOpen}
10751079
columns={columns}
10761080
setColumns={setColumns}
1081+
setColumnSizes={setColumnSizes}
10771082
/>
10781083
<ExportModal
10791084
id="export-modal"

0 commit comments

Comments
 (0)