diff --git a/frontend/package.json b/frontend/package.json index c691a1c44c..c6208a9a82 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/ResizeTable/ResizeTable.tsx b/frontend/src/components/ResizeTable/ResizeTable.tsx index d6898d0815..90cc588c47 100644 --- a/frontend/src/components/ResizeTable/ResizeTable.tsx +++ b/frontend/src/components/ResizeTable/ResizeTable.tsx @@ -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): JSX.Element { +function ResizeTable({ + columns, + onDragColumn, + ...restProps +}: ResizeTableProps): JSX.Element { const [columnsData, setColumns] = useState([]); const handleResize = useCallback( @@ -31,16 +39,32 @@ function ResizeTable({ columns, ...restprops }: TableProps): JSX.Element { [columnsData], ); - const mergeColumns = useMemo( + const mergedColumns = useMemo( () => columnsData.map((col, index) => ({ ...col, + ...(onDragColumn && { + title: ( + + {col?.title?.toString() || ''} + + ), + }), onHeaderCell: (column: ColumnsType[number]): unknown => ({ width: column.width, onResize: handleResize(index), }), - })), - [columnsData, handleResize], + })) as ColumnsType, + [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): JSX.Element { } }, [columns]); - return ( - } - /> + return onDragColumn ? ( + +
+ + ) : ( +
); } +ResizeTable.defaultProps = { + onDragColumn: undefined, +}; + export default ResizeTable; diff --git a/frontend/src/components/ResizeTable/styles.ts b/frontend/src/components/ResizeTable/styles.ts index acb0c28219..e69b208348 100644 --- a/frontend/src/components/ResizeTable/styles.ts +++ b/frontend/src/components/ResizeTable/styles.ts @@ -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; +`; diff --git a/frontend/src/components/ResizeTable/types.ts b/frontend/src/components/ResizeTable/types.ts new file mode 100644 index 0000000000..6390a25ba6 --- /dev/null +++ b/frontend/src/components/ResizeTable/types.ts @@ -0,0 +1,6 @@ +import { TableProps } from 'antd'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface ResizeTableProps extends TableProps { + onDragColumn?: (fromIndex: number, toIndex: number) => void; +} diff --git a/frontend/src/constants/localStorage.ts b/frontend/src/constants/localStorage.ts index 6460e094c0..63aee6b30a 100644 --- a/frontend/src/constants/localStorage.ts +++ b/frontend/src/constants/localStorage.ts @@ -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', } diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/LogsCustomTable.tsx b/frontend/src/container/LogsExplorerList/InfinityTableView/LogsCustomTable.tsx new file mode 100644 index 0000000000..d7ba10fb01 --- /dev/null +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/LogsCustomTable.tsx @@ -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 ( + + {children} + + ); + }; diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx index d6401c8485..848cd2512c 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx @@ -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 }) => ( - {children} -); - // 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 + >(LOCALSTORAGE.LOGS_LIST_COLUMNS); + + const tableColumns = useMemo( + () => getDraggedColumns>(columns, draggedColumns), + [columns, draggedColumns], + ); + + const handleDragEnd = useCallback( + (fromIndex: number, toIndex: number) => + onDragColumns(tableColumns, fromIndex, toIndex), + [tableColumns, onDragColumns], + ); const itemContent = useCallback( (index: number, log: Record): JSX.Element => ( <> - {columns.map((column) => { + {tableColumns.map((column) => { if (!column.render) return ; const element: ColumnTypeRender> = column.render( @@ -60,20 +78,29 @@ function InfinityTable({ })} ), - [columns], + [tableColumns], ); const tableHeader = useCallback( () => ( - {columns.map((column) => ( - - {column.title as string} - - ))} + {tableColumns.map((column) => { + const isDragColumn = column.key !== 'expand'; + + return ( + + {column.title as string} + + ); + })} ), - [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 diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts index a45d2d6b02..024ba88a9e 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts @@ -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` 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; } diff --git a/frontend/src/container/OptionsMenu/useOptionsMenu.ts b/frontend/src/container/OptionsMenu/useOptionsMenu.ts index 96094a7995..4e2a691666 100644 --- a/frontend/src/container/OptionsMenu/useOptionsMenu.ts +++ b/frontend/src/container/OptionsMenu/useOptionsMenu.ts @@ -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, }; }; diff --git a/frontend/src/container/TracesExplorer/ListView/index.tsx b/frontend/src/container/TracesExplorer/ListView/index.tsx index 7e70b0adf8..4ff128b629 100644 --- a/frontend/src/container/TracesExplorer/ListView/index.tsx +++ b/frontend/src/container/TracesExplorer/ListView/index.tsx @@ -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( + LOCALSTORAGE.TRACES_LIST_COLUMNS, + ); + const { queryData: paginationQueryData } = useUrlQueryData( 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 ( )} diff --git a/frontend/src/hooks/useDragColumns/configs.ts b/frontend/src/hooks/useDragColumns/configs.ts new file mode 100644 index 0000000000..fee591bc4e --- /dev/null +++ b/frontend/src/hooks/useDragColumns/configs.ts @@ -0,0 +1,7 @@ +export const COLUMNS = 'columns'; + +export const dragColumnParams = { + ignoreSelector: '.react-resizable-handle', + nodeSelector: 'th', + handleSelector: '.dragHandler', +}; diff --git a/frontend/src/hooks/useDragColumns/index.ts b/frontend/src/hooks/useDragColumns/index.ts new file mode 100644 index 0000000000..297fa55673 --- /dev/null +++ b/frontend/src/hooks/useDragColumns/index.ts @@ -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 = (storageKey: LOCALSTORAGE): UseDragColumns => { + const { + query: draggedColumnsQuery, + queryData: draggedColumns, + redirectWithQuery: redirectWithDraggedColumns, + } = useUrlQueryData>(COLUMNS, []); + + const localStorageDraggedColumns = useMemo( + () => getFromLocalstorage(storageKey), + [storageKey], + ); + + const handleRedirectWithDraggedColumns = useCallback( + (columns: ColumnsType) => { + redirectWithDraggedColumns(columns); + + setToLocalstorage(storageKey, JSON.stringify(columns)); + }, + [storageKey, redirectWithDraggedColumns], + ); + + const onDragColumns = useCallback( + (columns: ColumnsType, 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 = []; + + 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; diff --git a/frontend/src/hooks/useDragColumns/types.ts b/frontend/src/hooks/useDragColumns/types.ts new file mode 100644 index 0000000000..268edbb910 --- /dev/null +++ b/frontend/src/hooks/useDragColumns/types.ts @@ -0,0 +1,10 @@ +import { ColumnsType } from 'antd/es/table'; + +export type UseDragColumns = { + draggedColumns: ColumnsType; + onDragColumns: ( + columns: ColumnsType, + fromIndex: number, + toIndex: number, + ) => void; +}; diff --git a/frontend/src/hooks/useDragColumns/utils.ts b/frontend/src/hooks/useDragColumns/utils.ts new file mode 100644 index 0000000000..df11981066 --- /dev/null +++ b/frontend/src/hooks/useDragColumns/utils.ts @@ -0,0 +1,37 @@ +import { ColumnsType } from 'antd/es/table'; + +const filterColumns = ( + initialColumns: ColumnsType, + findColumns: ColumnsType, + isColumnExist = true, +): ColumnsType => + initialColumns.filter(({ title: columnTitle }) => { + const column = findColumns.find(({ title }) => title === columnTitle); + + return isColumnExist ? !!column : !column; + }); + +export const getDraggedColumns = ( + currentColumns: ColumnsType, + draggedColumns: ColumnsType, +): ColumnsType => { + if (draggedColumns.length) { + const actualDruggedColumns = filterColumns(draggedColumns, currentColumns); + const newColumns = filterColumns( + 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); + } + + return currentColumns; +}; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index b72d78ce59..de53c86183 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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"
Empty