{
+ event.stopPropagation();
+ setContextMenu({
+ visible: true,
+ x: event.pageX,
+ y: event.pageY,
+ row: cell.row.original,
+ });
+ }}
+ style={{
+ display: isFirstSelectedRow && isFirstColumn ? 'flex' : '',
+ }}>
+ {isSecureHTTPContext
+ ? sanitizedValue &&
+ : null}
+
{isSecureHTTPContext ? sanitizedValue &&
: null}
@@ -91,19 +137,31 @@ const makeHeaderOpts = (headers: string[], isSecureHTTPContext: boolean, fieldTy
[],
);
};
-
const makeColumnVisiblityOpts = (columns: string[]) => {
return _.reduce(columns, (acc, column) => ({ ...acc, [column]: false }), {});
};
const Table = (props: { primaryHeaderHeight: number }) => {
- const [{ orderedHeaders, disabledColumns, pinnedColumns, pageData, wrapDisabledColumns }, setLogsStore] =
- useLogsStore((store) => store.tableOpts);
+ const [contextMenu, setContextMenu] = useState
({
+ visible: false,
+ x: 0,
+ y: 0,
+ row: null,
+ });
+
+ const contextMenuRef = useRef(null);
+ 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]);
+ const columns = useMemo(
+ () => makeHeaderOpts(orderedHeaders, isSecureHTTPContext, fieldTypeMap, rowNumber, setContextMenu),
+ [orderedHeaders, rowNumber],
+ );
const columnVisibility = useMemo(() => makeColumnVisiblityOpts(disabledColumns), [disabledColumns, orderedHeaders]);
- const selectLog = useCallback((log: Log) => {
+ const selectLog = useCallback((log: Log | null) => {
+ if (!log) return;
const selectedText = window.getSelection()?.toString();
if (selectedText !== undefined && selectedText?.length > 0) return;
@@ -126,67 +184,179 @@ const Table = (props: { primaryHeaderHeight: number }) => {
},
[wrapDisabledColumns],
);
+ 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 copyUrl = useCallback(() => {
+ copyTextToClipboard(window.location.href);
+ notifySuccess({ message: 'Link Copied!' });
+ }, [window.location.href]);
+
+ const copyJSON = useCallback(() => {
+ const [start, end] = rowNumber.split(':').map(Number);
+
+ const rowsToCopy = pageData.slice(start, end + 1);
+
+ copyTextToClipboard(rowsToCopy);
+ notifySuccess({ message: 'JSON Copied!' });
+ }, [rowNumber]);
+
+ const handleRowClick = (index: number, event: React.MouseEvent) => {
+ let newRange = `${index}:${index}`;
+
+ if ((event.ctrlKey || event.metaKey) && rowNumber) {
+ const [start, end] = rowNumber.split(':').map(Number);
+ const lastIndex = Math.max(start, end);
+
+ const startIndex = Math.min(lastIndex, index);
+ const endIndex = Math.max(lastIndex, index);
+ newRange = `${startIndex}:${endIndex}`;
+ setLogsStore((store) => setRowNumber(store, newRange));
+ } else {
+ if (rowNumber) {
+ const [start, end] = rowNumber.split(':').map(Number);
+ if (index >= start && index <= end) {
+ setLogsStore((store) => setRowNumber(store, ''));
+ return;
+ }
+ }
+
+ setLogsStore((store) => setRowNumber(store, newRange));
+ }
+ };
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);
+ },
+ style: {
+ border: 'none',
+ background: row.index % 2 === 0 ? '#f8f9fa' : 'white',
+ backgroundColor:
+ rowNumber &&
+ (() => {
+ const [start, end] = rowNumber.split(':').map(Number);
+ return row.index >= start && row.index <= end;
+ })()
+ ? '#E8EDFE'
+ : '',
+ },
+ };
+ }}
+ mantineTableProps={{ highlightOnHover: false }}
+ 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
+ initialState={{
+ columnPinning: {
+ left: ['rowNumber'],
+ },
+ }}
+ enableStickyHeader
+ 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 && (
+
+
+
+ )}
+ >
);
};
diff --git a/src/pages/Stream/hooks/useParamsController.ts b/src/pages/Stream/hooks/useParamsController.ts
index 0af65474..c048edbd 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: string) => {
+ if (rowNumber.length > 0) {
+ return {
+ rowNumber,
+ };
+ }
+};
+
const storeToParamsObj = (opts: {
timeRange: TimeRange;
view: string;
@@ -60,10 +68,12 @@ const storeToParamsObj = (opts: {
rows: string;
query: string;
filterType: string;
+ rowNumber: string;
}): 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,10 +104,18 @@ const useParamsController = () => {
const [, setLogsStore] = useLogsStore(() => null);
const [, setFilterStore] = useFilterStore((store) => store);
- const { currentOffset, currentPage, perPage } = tableOpts;
+ const { currentOffset, currentPage, perPage, rowNumber } = tableOpts;
const [searchParams, setSearchParams] = useSearchParams();
+ const syncRowNumber = useCallback((storeAsParams: Record, presentParams: Record) => {
+ if (_.has(presentParams, 'rowNumber')) {
+ if (storeAsParams.rowNumber !== presentParams.rowNumber) {
+ setLogsStore((store) => setRowNumber(store, presentParams.rowNumber));
+ }
+ }
+ }, []);
+
useEffect(() => {
const storeAsParams = storeToParamsObj({
timeRange,
@@ -107,6 +125,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 +144,7 @@ const useParamsController = () => {
);
}
syncTimeRangeToStore(storeAsParams, presentParams);
+ syncRowNumber(storeAsParams, presentParams);
setStoreSynced(true);
}, []);
@@ -138,6 +158,7 @@ const useParamsController = () => {
rows: `${perPage}`,
query: custQuerySearchState.custSearchQuery,
filterType: custQuerySearchState.viewMode,
+ rowNumber,
});
const presentParams = paramsStringToParamsObj(searchParams);
if (_.isEqual(storeAsParams, presentParams)) return;
@@ -156,6 +177,7 @@ const useParamsController = () => {
rows: `${perPage}`,
query: custQuerySearchState.custSearchQuery,
filterType: custQuerySearchState.viewMode,
+ rowNumber,
});
const presentParams = paramsStringToParamsObj(searchParams);
@@ -178,6 +200,7 @@ const useParamsController = () => {
setLogsStore((store) => setCustQuerySearchState(store, presentParams.query, presentParams.filterType));
}
syncTimeRangeToStore(storeAsParams, presentParams);
+ syncRowNumber(storeAsParams, presentParams);
}, [searchParams]);
const syncTimeRangeToStore = useCallback(
diff --git a/src/pages/Stream/providers/LogsProvider.tsx b/src/pages/Stream/providers/LogsProvider.tsx
index 443c36b2..98c1da9b 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: string;
};
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: string) => 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: string) => {
+ const { tableOpts } = store;
+ return {
+ tableOpts: {
+ ...tableOpts,
+ rowNumber,
+ },
+ };
+};
+
const filterAndSortData = (
opts: { sortOrder: 'asc' | 'desc'; sortKey: string; filters: Record },
data: Log[],
@@ -861,6 +874,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..384b27c4 100644
--- a/src/pages/Stream/styles/Logs.module.css
+++ b/src/pages/Stream/styles/Logs.module.css
@@ -272,5 +272,25 @@
right: 0;
display: none;
}
+ .actionIconContainer {
+ position: absolute;
+ top: 25%;
+ left: 3px;
+ cursor: pointer;
+ padding: 1px 0px;
+ border-radius: 4px;
+ background-color: white;
+ border: 0.8px solid #545beb;
+ display: none;
+ }
}
}
+
+.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);
+}