From f17d74a93329874db48094352cb9fed418a26a89 Mon Sep 17 00:00:00 2001 From: Praveen K B Date: Mon, 16 Dec 2024 11:59:32 +0530 Subject: [PATCH] feat: Line share feature intial commit --- .../Stream/Views/Explore/StaticLogTable.tsx | 211 +++++++++++++----- src/pages/Stream/hooks/useParamsController.ts | 31 ++- src/pages/Stream/providers/LogsProvider.tsx | 14 ++ src/pages/Stream/styles/Logs.module.css | 9 + 4 files changed, 201 insertions(+), 64 deletions(-) diff --git a/src/pages/Stream/Views/Explore/StaticLogTable.tsx b/src/pages/Stream/Views/Explore/StaticLogTable.tsx index e9b638da..8ed9eb0c 100644 --- a/src/pages/Stream/Views/Explore/StaticLogTable.tsx +++ b/src/pages/Stream/Views/Explore/StaticLogTable.tsx @@ -1,5 +1,5 @@ -import { Box } from '@mantine/core'; -import { useCallback, useMemo } from 'react'; +import { Box, Menu } from '@mantine/core'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { ReactNode } from 'react'; import EmptyBox from '@/components/Empty'; import FilterPills from '../../components/FilterPills'; @@ -21,8 +21,9 @@ import { Log } from '@/@types/parseable/api/query'; import { CopyIcon } from './JSONView'; import { FieldTypeMap, useStreamStore } from '../../providers/StreamProvider'; import timeRangeUtils from '@/utils/timeRangeUtils'; +import { IconBraces } from '@tabler/icons-react'; -const { setSelectedLog } = logsStoreReducers; +const { setSelectedLog, setRowNumber } = logsStoreReducers; const TableContainer = (props: { children: ReactNode }) => { return {props.children}; }; @@ -97,8 +98,9 @@ const makeColumnVisiblityOpts = (columns: string[]) => { }; const Table = (props: { primaryHeaderHeight: number }) => { - const [{ orderedHeaders, disabledColumns, pinnedColumns, pageData, wrapDisabledColumns }, setLogsStore] = - useLogsStore((store) => store.tableOpts); + const [{ orderedHeaders, disabledColumns, pageData, wrapDisabledColumns, rowNumber }, setLogsStore] = useLogsStore( + (store) => store.tableOpts, + ); const [isSecureHTTPContext] = useAppStore((store) => store.isSecureHTTPContext); const [fieldTypeMap] = useStreamStore((store) => store.fieldTypeMap); const columns = useMemo(() => makeHeaderOpts(orderedHeaders, isSecureHTTPContext, fieldTypeMap), [orderedHeaders]); @@ -126,67 +128,156 @@ const Table = (props: { primaryHeaderHeight: number }) => { }, [wrapDisabledColumns], ); + const [contextMenu, setContextMenu] = useState<{ + visible: boolean; + x: number; + y: number; + row: any | null; + }>({ + visible: false, + x: 0, + y: 0, + row: null, + }); + + const contextMenuRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (contextMenuRef.current && !contextMenuRef.current.contains(event.target as Node)) { + closeContextMenu(); + } + }; + + if (contextMenu.visible) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [contextMenu.visible]); + + const closeContextMenu = () => setContextMenu({ visible: false, x: 0, y: 0, row: null }); + + const handleRowClick = (index: number, event: React.MouseEvent) => { + if ((event.ctrlKey || event.metaKey) && rowNumber.length > 0) { + const lastIndex = rowNumber[rowNumber.length - 1]; + const start = Math.min(lastIndex, index); + const end = Math.max(lastIndex, index); + + const newSelectedRows = Array.from({ length: end - start + 1 }, (_, i) => start + i); + setLogsStore((store) => setRowNumber(store, newSelectedRows)); + } else { + setLogsStore((store) => { + if (rowNumber.includes(index)) { + return setRowNumber( + store, + rowNumber.filter((rowIndex) => rowIndex !== index), + ); + } else { + return setRowNumber(store, [index]); + } + }); + } + }; return ( - makeCellCustomStyles(id)} - mantineTableHeadRowProps={{ style: { border: 'none' } }} - mantineTableHeadCellProps={{ - style: { - fontWeight: 600, - fontSize: '0.65rem', - border: 'none', - padding: '0.5rem 1rem', - }, - }} - mantineTableBodyRowProps={({ row }) => { - return { - onClick: () => { - selectLog(row.original); + <> + makeCellCustomStyles(id)} + mantineTableHeadRowProps={{ style: { border: 'none' } }} + mantineTableHeadCellProps={{ + style: { + fontWeight: 600, + fontSize: '0.65rem', + border: 'none', + padding: '0.5rem 1rem', }, + }} + mantineTableBodyRowProps={({ row }) => { + return { + onClick: (event) => { + event.preventDefault(); + handleRowClick(row.index, event); + }, + onContextMenu: (event) => { + event.preventDefault(); + setContextMenu({ + visible: true, + x: event.pageX, + y: event.pageY, + row: row.original, + }); + }, + style: { + border: rowNumber.includes(row.index) ? '2px solid #007BFF' : 'none', + background: row.index % 2 === 0 ? '#f8f9fa' : 'white', + transition: 'border 0.2s', + }, + }; + }} + mantineTableHeadProps={{ style: { border: 'none', - background: row.index % 2 === 0 ? '#f8f9fa' : 'white', }, - }; - }} - mantineTableHeadProps={{ - style: { - border: 'none', - }, - }} - columns={columns} - data={pageData} - mantinePaperProps={{ style: { border: 'none' } }} - enablePagination={false} - enableColumnPinning={true} - initialState={{ - columnPinning: { - left: pinnedColumns, - }, - }} - enableStickyHeader={true} - defaultColumn={{ minSize: 100 }} - layoutMode="grid" - state={{ - columnPinning: { - left: pinnedColumns, - }, - columnVisibility, - columnOrder: orderedHeaders, - }} - mantineTableContainerProps={{ - style: { - height: `calc(100vh - ${props.primaryHeaderHeight + LOGS_FOOTER_HEIGHT}px )`, - }, - }} - renderColumnActionsMenuItems={({ column }) => { - return ; - }} - /> + }} + columns={columns} + data={pageData} + mantinePaperProps={{ style: { border: 'none' } }} + enablePagination={false} + enableColumnPinning={true} + initialState={{ + columnPinning: { + left: ['rowNumber'], + }, + }} + enableStickyHeader={true} + defaultColumn={{ minSize: 100 }} + layoutMode="grid" + state={{ + columnPinning: { + left: ['rowNumber'], + }, + columnVisibility, + columnOrder: orderedHeaders, + }} + mantineTableContainerProps={{ + style: { + height: `calc(100vh - ${props.primaryHeaderHeight + LOGS_FOOTER_HEIGHT}px )`, + }, + }} + renderColumnActionsMenuItems={({ column }) => { + return ; + }} + /> + {contextMenu.visible && ( +
+ + {rowNumber.length === 1 && ( + } + onClick={() => { + selectLog(contextMenu.row); + closeContextMenu(); + }}> + View JSON + + )} + +
+ )} + ); }; diff --git a/src/pages/Stream/hooks/useParamsController.ts b/src/pages/Stream/hooks/useParamsController.ts index b7894003..2eccba5f 100644 --- a/src/pages/Stream/hooks/useParamsController.ts +++ b/src/pages/Stream/hooks/useParamsController.ts @@ -12,11 +12,11 @@ import { generateQueryBuilderASTFromSQL } from '../utils'; import { appStoreReducers, TimeRange, useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; const { getRelativeStartAndEndDate, formatDateWithTimezone, getLocalTimezone } = timeRangeUtils; -const { onToggleView, setPerPage, setCustQuerySearchState } = logsStoreReducers; +const { onToggleView, setPerPage, setCustQuerySearchState, setRowNumber } = logsStoreReducers; const { setTimeRange, syncTimeRange } = appStoreReducers; const { applySavedFilters } = filterStoreReducers; const timeRangeFormat = 'DD-MMM-YYYY_HH-mmz'; -const keys = ['view', 'rows', 'interval', 'from', 'to', 'query', 'filterType']; +const keys = ['view', 'rows', 'interval', 'from', 'to', 'query', 'filterType', 'rowNumber']; const dateToParamString = (date: Date) => { return formatDateWithTimezone(date, timeRangeFormat); @@ -52,6 +52,14 @@ const deriveTimeRangeParams = (timerange: TimeRange): { interval: string } | { f } }; +const deriveRowNumber = (rowNumber: number[]) => { + if (rowNumber.length > 0) { + return { + rowNumber: JSON.stringify(rowNumber), + }; + } +}; + const storeToParamsObj = (opts: { timeRange: TimeRange; view: string; @@ -60,10 +68,12 @@ const storeToParamsObj = (opts: { rows: string; query: string; filterType: string; + rowNumber: number[]; }): Record => { - const { timeRange, offset, page, view, rows, query, filterType } = opts; + const { timeRange, offset, page, view, rows, query, filterType, rowNumber } = opts; const params: Record = { ...deriveTimeRangeParams(timeRange), + ...deriveRowNumber(rowNumber), view, offset, rows, @@ -94,7 +104,7 @@ const useParamsController = () => { const [, setLogsStore] = useLogsStore((_store) => null); const [, setFilterStore] = useFilterStore((store) => store); - const { currentOffset, currentPage, perPage } = tableOpts; + const { currentOffset, currentPage, perPage, rowNumber } = tableOpts; const [searchParams, setSearchParams] = useSearchParams(); @@ -107,6 +117,7 @@ const useParamsController = () => { rows: `${perPage}`, query: custQuerySearchState.custSearchQuery, filterType: custQuerySearchState.viewMode, + rowNumber, }); const presentParams = paramsStringToParamsObj(searchParams); if (['table', 'json'].includes(presentParams.view) && presentParams.view !== storeAsParams.view) { @@ -125,6 +136,7 @@ const useParamsController = () => { ); } syncTimeRangeToStore(storeAsParams, presentParams); + syncRowNumber(storeAsParams, presentParams); setStoreSynced(true); }, []); @@ -138,6 +150,7 @@ const useParamsController = () => { rows: `${perPage}`, query: custQuerySearchState.custSearchQuery, filterType: custQuerySearchState.viewMode, + rowNumber: rowNumber, }); const presentParams = paramsStringToParamsObj(searchParams); if (_.isEqual(storeAsParams, presentParams)) return; @@ -156,6 +169,7 @@ const useParamsController = () => { rows: `${perPage}`, query: custQuerySearchState.custSearchQuery, filterType: custQuerySearchState.viewMode, + rowNumber: rowNumber, }); const presentParams = paramsStringToParamsObj(searchParams); @@ -178,8 +192,17 @@ const useParamsController = () => { setLogsStore((store) => setCustQuerySearchState(store, presentParams.query, presentParams.filterType)); } syncTimeRangeToStore(storeAsParams, presentParams); + syncRowNumber(storeAsParams, presentParams); }, [searchParams]); + const syncRowNumber = useCallback((storeAsParams: Record, presentParams: Record) => { + if (_.has(presentParams, 'rowNumber')) { + if (storeAsParams.rowNumber !== presentParams.rowNumber) { + setLogsStore((store) => setRowNumber(store, JSON.parse(presentParams.rowNumber))); + } + } + }, []); + const syncTimeRangeToStore = useCallback( (storeAsParams: Record, presentParams: Record) => { if (_.has(presentParams, 'interval')) { diff --git a/src/pages/Stream/providers/LogsProvider.tsx b/src/pages/Stream/providers/LogsProvider.tsx index 8af9ed18..7c0e54cc 100644 --- a/src/pages/Stream/providers/LogsProvider.tsx +++ b/src/pages/Stream/providers/LogsProvider.tsx @@ -187,6 +187,7 @@ type LogsStore = { instantSearchValue: string; configViewType: 'schema' | 'columns'; enableWordWrap: boolean; + rowNumber: number[]; }; data: LogQueryData; @@ -251,6 +252,7 @@ type LogsStoreReducers = { setDisabledColumns: (store: LogsStore, columns: string[]) => ReducerOutput; setOrderedHeaders: (store: LogsStore, columns: string[]) => ReducerOutput; toggleWordWrap: (store: LogsStore) => ReducerOutput; + setRowNumber: (store: LogsStore, rowNumber: number[]) => ReducerOutput; }; const defaultSortKey = 'p_timestamp'; @@ -290,6 +292,7 @@ const initialState: LogsStore = { instantSearchValue: '', configViewType: 'columns', enableWordWrap: true, + rowNumber: [], }, // data @@ -461,6 +464,16 @@ const togglePinnedColumns = (store: LogsStore, columnName: string) => { }; }; +const setRowNumber = (store: LogsStore, rowNumber: number[]) => { + const { tableOpts } = store; + return { + tableOpts: { + ...tableOpts, + rowNumber, + }, + }; +}; + const filterAndSortData = ( opts: { sortOrder: 'asc' | 'desc'; sortKey: string; filters: Record }, data: Log[], @@ -867,6 +880,7 @@ const logsStoreReducers: LogsStoreReducers = { toggleDeleteModal, toggleDisabledColumns, togglePinnedColumns, + setRowNumber, setLogData, setStreamSchema, setPerPage, diff --git a/src/pages/Stream/styles/Logs.module.css b/src/pages/Stream/styles/Logs.module.css index cd2308d8..bb08517f 100644 --- a/src/pages/Stream/styles/Logs.module.css +++ b/src/pages/Stream/styles/Logs.module.css @@ -274,3 +274,12 @@ } } } + +.contextMenuContainer { + position: fixed; + z-index: 1000; + background-color: white; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +}