feat: add the ability to drag columns (#3100)

* feat: add the ability to drag columns

* feat: add the ability to drag columns in the logs explorer

* feat: update drag logic

* fix: resolve comments

* feat: resolve comment regarding error handling

---------

Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
dnazarenkoo 2023-07-18 14:48:34 +03:00 committed by GitHub
parent 07833b9859
commit 8f1451e154
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 307 additions and 37 deletions

View File

@ -69,6 +69,7 @@
"papaparse": "5.4.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-drag-listview": "2.0.0",
"react-force-graph": "^1.41.0",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.16.1",

View File

@ -1,6 +1,8 @@
/* eslint-disable react/jsx-props-no-spreading */
import { Table } from 'antd';
import type { TableProps } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import { dragColumnParams } from 'hooks/useDragColumns/configs';
import {
SyntheticEvent,
useCallback,
@ -8,12 +10,18 @@ import {
useMemo,
useState,
} from 'react';
import ReactDragListView from 'react-drag-listview';
import { ResizeCallbackData } from 'react-resizable';
import ResizableHeader from './ResizableHeader';
import { DragSpanStyle } from './styles';
import { ResizeTableProps } from './types';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
function ResizeTable({
columns,
onDragColumn,
...restProps
}: ResizeTableProps): JSX.Element {
const [columnsData, setColumns] = useState<ColumnsType>([]);
const handleResize = useCallback(
@ -31,16 +39,32 @@ function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
[columnsData],
);
const mergeColumns = useMemo(
const mergedColumns = useMemo(
() =>
columnsData.map((col, index) => ({
...col,
...(onDragColumn && {
title: (
<DragSpanStyle className="dragHandler">
{col?.title?.toString() || ''}
</DragSpanStyle>
),
}),
onHeaderCell: (column: ColumnsType<unknown>[number]): unknown => ({
width: column.width,
onResize: handleResize(index),
}),
})),
[columnsData, handleResize],
})) as ColumnsType<any>,
[columnsData, onDragColumn, handleResize],
);
const tableParams = useMemo(
() => ({
...restProps,
components: { header: { cell: ResizableHeader } },
columns: mergedColumns,
}),
[mergedColumns, restProps],
);
useEffect(() => {
@ -49,15 +73,17 @@ function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
}
}, [columns]);
return (
<Table
// eslint-disable-next-line react/jsx-props-no-spreading
{...restprops}
components={{ header: { cell: ResizableHeader } }}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
columns={mergeColumns as ColumnsType<any>}
/>
return onDragColumn ? (
<ReactDragListView.DragColumn {...dragColumnParams} onDragEnd={onDragColumn}>
<Table {...tableParams} />
</ReactDragListView.DragColumn>
) : (
<Table {...tableParams} />
);
}
ResizeTable.defaultProps = {
onDragColumn: undefined,
};
export default ResizeTable;

View File

@ -2,10 +2,16 @@ import styled from 'styled-components';
export const SpanStyle = styled.span`
position: absolute;
right: -5px;
right: -0.313rem;
bottom: 0;
z-index: 1;
width: 10px;
width: 0.625rem;
height: 100%;
cursor: col-resize;
`;
export const DragSpanStyle = styled.span`
display: flex;
margin: -1rem;
padding: 1rem;
`;

View File

@ -0,0 +1,6 @@
import { TableProps } from 'antd';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface ResizeTableProps extends TableProps<any> {
onDragColumn?: (fromIndex: number, toIndex: number) => void;
}

View File

@ -8,4 +8,6 @@ export enum LOCALSTORAGE {
LOGS_LINES_PER_ROW = 'LOGS_LINES_PER_ROW',
LOGS_LIST_OPTIONS = 'LOGS_LIST_OPTIONS',
TRACES_LIST_OPTIONS = 'TRACES_LIST_OPTIONS',
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
}

View File

@ -0,0 +1,24 @@
import { dragColumnParams } from 'hooks/useDragColumns/configs';
import ReactDragListView from 'react-drag-listview';
import { TableComponents } from 'react-virtuoso';
import { TableStyled } from './styles';
interface LogsCustomTableProps {
handleDragEnd: (fromIndex: number, toIndex: number) => void;
}
export const LogsCustomTable = ({
handleDragEnd,
}: LogsCustomTableProps): TableComponents['Table'] =>
function CustomTable({ style, children }): JSX.Element {
return (
<ReactDragListView.DragColumn
// eslint-disable-next-line react/jsx-props-no-spreading
{...dragColumnParams}
onDragEnd={handleDragEnd}
>
<TableStyled style={style}>{children}</TableStyled>
</ReactDragListView.DragColumn>
);
};

View File

@ -1,22 +1,26 @@
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { useTableView } from 'components/Logs/TableView/useTableView';
import { cloneElement, ReactElement, ReactNode, useCallback } from 'react';
import { LOCALSTORAGE } from 'constants/localStorage';
import useDragColumns from 'hooks/useDragColumns';
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
import {
cloneElement,
ReactElement,
ReactNode,
useCallback,
useMemo,
} from 'react';
import { TableComponents, TableVirtuoso } from 'react-virtuoso';
import { infinityDefaultStyles } from './config';
import { LogsCustomTable } from './LogsCustomTable';
import {
TableCellStyled,
TableHeaderCellStyled,
TableRowStyled,
TableStyled,
} from './styles';
import { InfinityTableProps } from './types';
// eslint-disable-next-line react/function-component-definition
const CustomTable: TableComponents['Table'] = ({ style, children }) => (
<TableStyled style={style}>{children}</TableStyled>
);
// eslint-disable-next-line react/function-component-definition
const CustomTableRow: TableComponents['TableRow'] = ({
children,
@ -31,11 +35,25 @@ function InfinityTable({
}: InfinityTableProps): JSX.Element | null {
const { onEndReached } = infitiyTableProps;
const { dataSource, columns } = useTableView(tableViewProps);
const { draggedColumns, onDragColumns } = useDragColumns<
Record<string, unknown>
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
const tableColumns = useMemo(
() => getDraggedColumns<Record<string, unknown>>(columns, draggedColumns),
[columns, draggedColumns],
);
const handleDragEnd = useCallback(
(fromIndex: number, toIndex: number) =>
onDragColumns(tableColumns, fromIndex, toIndex),
[tableColumns, onDragColumns],
);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => (
<>
{columns.map((column) => {
{tableColumns.map((column) => {
if (!column.render) return <td>Empty</td>;
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
@ -60,20 +78,29 @@ function InfinityTable({
})}
</>
),
[columns],
[tableColumns],
);
const tableHeader = useCallback(
() => (
<tr>
{columns.map((column) => (
<TableHeaderCellStyled key={column.key}>
{column.title as string}
</TableHeaderCellStyled>
))}
{tableColumns.map((column) => {
const isDragColumn = column.key !== 'expand';
return (
<TableHeaderCellStyled
isDragColumn={isDragColumn}
key={column.key}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isDragColumn && { className: 'dragHandler' })}
>
{column.title as string}
</TableHeaderCellStyled>
);
})}
</tr>
),
[columns],
[tableColumns],
);
return (
@ -81,7 +108,8 @@ function InfinityTable({
style={infinityDefaultStyles}
data={dataSource}
components={{
Table: CustomTable,
// eslint-disable-next-line react/jsx-props-no-spreading
Table: LogsCustomTable({ handleDragEnd }),
// TODO: fix it in the future
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore

View File

@ -1,6 +1,10 @@
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
interface TableHeaderCellStyledProps {
isDragColumn: boolean;
}
export const TableStyled = styled.table`
width: 100%;
border-top: 1px solid rgba(253, 253, 253, 0.12);
@ -26,10 +30,12 @@ export const TableRowStyled = styled.tr`
}
`;
export const TableHeaderCellStyled = styled.th`
export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
padding: 0.5rem;
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
background-color: #1d1d1d;
${({ isDragColumn }): string => (isDragColumn ? 'cursor: col-resize;' : '')}
&:first-child {
border-start-start-radius: 2px;
}

View File

@ -30,6 +30,7 @@ interface UseOptionsMenuProps {
interface UseOptionsMenu {
options: OptionsQuery;
config: OptionsMenuConfig;
handleOptionsChange: (newQueryData: OptionsQuery) => void;
}
const useOptionsMenu = ({
@ -306,6 +307,7 @@ const useOptionsMenu = ({
return {
options: optionsQueryData,
config: optionsMenuConfig,
handleOptionsChange: handleRedirectWithOptionsData,
};
};

View File

@ -6,6 +6,8 @@ import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination, URL_PAGINATION } from 'hooks/queryPagination';
import useDragColumns from 'hooks/useDragColumns';
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
import useUrlQueryData from 'hooks/useUrlQueryData';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
@ -37,6 +39,10 @@ function ListView(): JSX.Element {
},
});
const { draggedColumns, onDragColumns } = useDragColumns<RowData>(
LOCALSTORAGE.TRACES_LIST_COLUMNS,
);
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
URL_PAGINATION,
);
@ -82,9 +88,10 @@ function ListView(): JSX.Element {
queryTableDataResult,
]);
const columns = useMemo(() => getListColumns(options?.selectColumns || []), [
options?.selectColumns,
]);
const columns = useMemo(() => {
const updatedColumns = getListColumns(options?.selectColumns || []);
return getDraggedColumns(updatedColumns, draggedColumns);
}, [options?.selectColumns, draggedColumns]);
const transformedQueryTableData = useMemo(
() => transformDataWithDate(queryTableData) || [],
@ -106,6 +113,12 @@ function ListView(): JSX.Element {
[],
);
const handleDragColumn = useCallback(
(fromIndex: number, toIndex: number) =>
onDragColumns(columns, fromIndex, toIndex),
[columns, onDragColumns],
);
return (
<Container>
<TraceExplorerControls
@ -127,6 +140,7 @@ function ListView(): JSX.Element {
dataSource={transformedQueryTableData}
columns={columns}
onRow={handleRow}
onDragColumn={handleDragColumn}
/>
)}
</Container>

View File

@ -0,0 +1,7 @@
export const COLUMNS = 'columns';
export const dragColumnParams = {
ignoreSelector: '.react-resizable-handle',
nodeSelector: 'th',
handleSelector: '.dragHandler',
};

View File

@ -0,0 +1,75 @@
import { ColumnsType } from 'antd/es/table';
import getFromLocalstorage from 'api/browser/localstorage/get';
import setToLocalstorage from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { useCallback, useEffect, useMemo } from 'react';
import { COLUMNS } from './configs';
import { UseDragColumns } from './types';
const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
const {
query: draggedColumnsQuery,
queryData: draggedColumns,
redirectWithQuery: redirectWithDraggedColumns,
} = useUrlQueryData<ColumnsType<T>>(COLUMNS, []);
const localStorageDraggedColumns = useMemo(
() => getFromLocalstorage(storageKey),
[storageKey],
);
const handleRedirectWithDraggedColumns = useCallback(
(columns: ColumnsType<T>) => {
redirectWithDraggedColumns(columns);
setToLocalstorage(storageKey, JSON.stringify(columns));
},
[storageKey, redirectWithDraggedColumns],
);
const onDragColumns = useCallback(
(columns: ColumnsType<T>, fromIndex: number, toIndex: number): void => {
const columnsData = [...columns];
const item = columnsData.splice(fromIndex, 1)[0];
columnsData.splice(toIndex, 0, item);
handleRedirectWithDraggedColumns(columnsData);
},
[handleRedirectWithDraggedColumns],
);
const redirectWithNewDraggedColumns = useCallback(
async (localStorageColumns: string) => {
let nextDraggedColumns: ColumnsType<T> = [];
try {
const parsedDraggedColumns = await JSON.parse(localStorageColumns);
nextDraggedColumns = parsedDraggedColumns;
} catch (e) {
console.log('error while parsing json');
} finally {
redirectWithDraggedColumns(nextDraggedColumns);
}
},
[redirectWithDraggedColumns],
);
useEffect(() => {
if (draggedColumnsQuery || !localStorageDraggedColumns) return;
redirectWithNewDraggedColumns(localStorageDraggedColumns);
}, [
draggedColumnsQuery,
localStorageDraggedColumns,
redirectWithNewDraggedColumns,
]);
return {
draggedColumns,
onDragColumns,
};
};
export default useDragColumns;

View File

@ -0,0 +1,10 @@
import { ColumnsType } from 'antd/es/table';
export type UseDragColumns<T> = {
draggedColumns: ColumnsType<T>;
onDragColumns: (
columns: ColumnsType<T>,
fromIndex: number,
toIndex: number,
) => void;
};

View File

@ -0,0 +1,37 @@
import { ColumnsType } from 'antd/es/table';
const filterColumns = <T>(
initialColumns: ColumnsType<T>,
findColumns: ColumnsType<T>,
isColumnExist = true,
): ColumnsType<T> =>
initialColumns.filter(({ title: columnTitle }) => {
const column = findColumns.find(({ title }) => title === columnTitle);
return isColumnExist ? !!column : !column;
});
export const getDraggedColumns = <T>(
currentColumns: ColumnsType<T>,
draggedColumns: ColumnsType<T>,
): ColumnsType<T> => {
if (draggedColumns.length) {
const actualDruggedColumns = filterColumns<T>(draggedColumns, currentColumns);
const newColumns = filterColumns<T>(
currentColumns,
actualDruggedColumns,
false,
);
return [...actualDruggedColumns, ...newColumns].reduce((acc, { title }) => {
const column = currentColumns.find(
({ title: columnTitle }) => title === columnTitle,
);
if (column) return [...acc, column];
return acc;
}, [] as ColumnsType<T>);
}
return currentColumns;
};

View File

@ -3719,6 +3719,14 @@ babel-preset-react-app@^10.0.0:
babel-plugin-macros "^3.1.0"
babel-plugin-transform-react-remove-prop-types "^0.4.24"
babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
@ -4474,6 +4482,11 @@ core-js-compat@^3.25.1:
dependencies:
browserslist "^4.21.5"
core-js@^2.4.0:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
@ -9999,7 +10012,7 @@ prompts@^2.0.1, prompts@^2.4.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@15, prop-types@15.x, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@15, prop-types@15.x, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -10513,6 +10526,14 @@ react-dom@18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-drag-listview@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-drag-listview/-/react-drag-listview-2.0.0.tgz#b8e7ec5f980ecbbf3abb85f50db0b03cd764edbf"
integrity sha512-7Apx/1Xt4qu+JHHP0rH6aLgZgS7c2MX8ocHVGCi03KfeIWEu0t14MhT3boQKM33l5eJrE/IWfExFTvoYq22fsg==
dependencies:
babel-runtime "^6.26.0"
prop-types "^15.5.8"
react-draggable@^4.0.0, react-draggable@^4.0.3:
version "4.4.5"
resolved "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz"
@ -10808,6 +10829,11 @@ regenerator-runtime@0.13.9:
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.11:
version "0.13.11"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz"