diff --git a/src/api/query.ts b/src/api/query.ts index c9bc2c59..c2e57db5 100644 --- a/src/api/query.ts +++ b/src/api/query.ts @@ -18,6 +18,7 @@ type CorrelationLogs = { startTime: Date; endTime: Date; limit: number; + pageOffset: number; correlationCondition?: string; selectedFields?: string[]; }; @@ -68,6 +69,12 @@ export const getCorrelationQueryLogsWithHeaders = (logsQuery: CorrelationLogs) = return Axios().post(endPoint, queryBuilder.getCorrelationQuery(), {}); }; +export const getCorrelationQueryCount = (logsQuery: CorrelationLogs) => { + const queryBuilder = new CorrelationQueryBuilder(logsQuery); + const endPoint = LOG_QUERY_URL(); + return Axios().post(endPoint, makeCustomQueryRequestData(logsQuery, queryBuilder.getCountQuery()), {}); +}; + export const getStreamDataWithHeaders = (logsQuery: CorrelationLogs) => { const queryBuilder = new CorrelationQueryBuilder(logsQuery); const endPoint = LOG_QUERY_URL({ fields: true }, queryBuilder.getResourcePath()); @@ -76,7 +83,7 @@ export const getStreamDataWithHeaders = (logsQuery: CorrelationLogs) => { // ------ Custom sql query -const makeCustomQueryRequestData = (logsQuery: LogsQuery, query: string) => { +const makeCustomQueryRequestData = (logsQuery: LogsQuery | CorrelationLogs, query: string) => { const { startTime, endTime } = logsQuery; return { query, startTime: optimizeTime(startTime), endTime: optimizeTime(endTime) }; }; diff --git a/src/components/Navbar/components/CorrelationIcon.tsx b/src/components/Navbar/components/CorrelationIcon.tsx index 1962aab1..2541daec 100644 --- a/src/components/Navbar/components/CorrelationIcon.tsx +++ b/src/components/Navbar/components/CorrelationIcon.tsx @@ -7,21 +7,28 @@ export const CorrelationIcon = forwardRef< strokeWidth?: number; } >(({ stroke, strokeWidth }, ref) => ( - - - + + + + + + + )); diff --git a/src/hooks/useCorrelationQueryLogs.tsx b/src/hooks/useCorrelationQueryLogs.tsx index f8b55d7d..c42ad6b2 100644 --- a/src/hooks/useCorrelationQueryLogs.tsx +++ b/src/hooks/useCorrelationQueryLogs.tsx @@ -1,19 +1,21 @@ -import { getCorrelationQueryLogsWithHeaders } from '@/api/query'; -import useMountedState from './useMountedState'; -import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; -import _ from 'lodash'; -import { AxiosError } from 'axios'; -import { useStreamStore } from '@/pages/Stream/providers/StreamProvider'; import { CORRELATION_LOAD_LIMIT, correlationStoreReducers, useCorrelationStore, } from '@/pages/Correlation/providers/CorrelationProvider'; + +import { AxiosError } from 'axios'; +import { LogsResponseWithHeaders } from '@/@types/parseable/api/query'; +import _ from 'lodash'; +import { getCorrelationQueryLogsWithHeaders } from '@/api/query'; import { notifyError } from '@/utils/notification'; +import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import useMountedState from './useMountedState'; import { useQuery } from 'react-query'; -import { LogsResponseWithHeaders } from '@/@types/parseable/api/query'; +import { useState } from 'react'; +import { useStreamStore } from '@/pages/Stream/providers/StreamProvider'; -const { setStreamData } = correlationStoreReducers; +const { setStreamData, setIsCorrelatedFlag } = correlationStoreReducers; export const useCorrelationQueryLogs = () => { const [error, setError] = useMountedState(null); @@ -22,6 +24,7 @@ export const useCorrelationQueryLogs = () => { const [currentStream] = useAppStore((store) => store.currentStream); const timePartitionColumn = _.get(streamInfo, 'time_partition', 'p_timestamp'); const [timeRange] = useAppStore((store) => store.timeRange); + const [loadingState, setLoading] = useState(true); const [ { tableOpts: { currentOffset }, @@ -39,13 +42,10 @@ export const useCorrelationQueryLogs = () => { correlationCondition: correlationCondition, }; - const { - isLoading: logsLoading, - isRefetching: logsRefetching, - refetch: getCorrelationData, - } = useQuery( + const { refetch: getCorrelationData } = useQuery( ['fetch-logs', defaultQueryOpts], async () => { + setLoading(true); const queryOpts = { ...defaultQueryOpts, streamNames }; const response = await getCorrelationQueryLogsWithHeaders(queryOpts); return [response]; @@ -54,6 +54,7 @@ export const useCorrelationQueryLogs = () => { enabled: false, refetchOnWindowFocus: false, onSuccess: async (responses) => { + setLoading(false); responses.map((data: { data: LogsResponseWithHeaders }) => { const logs = data.data; const isInvalidResponse = _.isEmpty(logs) || _.isNil(logs); @@ -63,6 +64,7 @@ export const useCorrelationQueryLogs = () => { if (fields.length > 0 && !correlationCondition) { return setCorrelationStore((store) => setStreamData(store, currentStream || '', records)); } else if (fields.length > 0 && correlationCondition) { + setCorrelationStore((store) => setIsCorrelatedFlag(store, true)); return setCorrelationStore((store) => setStreamData(store, 'correlatedStream', records)); } else { notifyError({ message: `${currentStream} doesn't have any fields` }); @@ -70,6 +72,7 @@ export const useCorrelationQueryLogs = () => { }); }, onError: (data: AxiosError) => { + setLoading(false); const errorMessage = data.response?.data as string; setError(_.isString(errorMessage) && !_.isEmpty(errorMessage) ? errorMessage : 'Failed to query logs'); }, @@ -78,7 +81,7 @@ export const useCorrelationQueryLogs = () => { return { error, - loading: logsLoading || logsRefetching, + loadingState, getCorrelationData, }; }; diff --git a/src/hooks/useCorrelations.tsx b/src/hooks/useCorrelations.tsx index a4ccada2..83ddd2a6 100644 --- a/src/hooks/useCorrelations.tsx +++ b/src/hooks/useCorrelations.tsx @@ -1,18 +1,11 @@ import { useMutation, useQuery } from 'react-query'; import _ from 'lodash'; -import { - deleteSavedCorrelation, - getCorrelationById, - getCorrelations, - saveCorrelation, - updateCorrelation, -} from '@/api/correlations'; +import { deleteSavedCorrelation, getCorrelations, saveCorrelation, updateCorrelation } from '@/api/correlations'; import { correlationStoreReducers, useCorrelationStore } from '@/pages/Correlation/providers/CorrelationProvider'; import { notifyError, notifySuccess } from '@/utils/notification'; import { AxiosError, isAxiosError } from 'axios'; import { appStoreReducers, useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; -import dayjs from 'dayjs'; const { setCorrelations, @@ -22,7 +15,7 @@ const { cleanCorrelationStore, toggleSavedCorrelationsModal, } = correlationStoreReducers; -const { setTimeRange, syncTimeRange } = appStoreReducers; +const { syncTimeRange } = appStoreReducers; export const useCorrelationsQuery = () => { const [{ correlationId }, setCorrelatedStore] = useCorrelationStore((store) => store); const [, setAppStore] = useAppStore((store) => store); @@ -44,30 +37,6 @@ export const useCorrelationsQuery = () => { }, }); - const { - mutate: getCorrelationByIdMutation, - isError: fetchCorrelationIdError, - isSuccess: fetchCorrelationIdSuccess, - isLoading: fetchCorrelationIdLoading, - } = useMutation((correlationId: string) => getCorrelationById(correlationId), { - onSuccess: (data: any) => { - data.data.startTime && - data.data.endTime && - setAppStore((store) => - setTimeRange(store, { - startTime: dayjs(data.data.startTime), - endTime: dayjs(data.data.endTime), - type: 'custom', - }), - ); - setCorrelatedStore((store) => setCorrelationId(store, data.data.id)); - setCorrelatedStore((store) => setActiveCorrelation(store, data.data)); - }, - onError: () => { - notifyError({ message: 'Failed to fetch correlation' }); - }, - }); - const { mutate: deleteSavedCorrelationMutation, isLoading: isDeleting } = useMutation( (data: { correlationId: string; onSuccess?: () => void }) => deleteSavedCorrelation(data.correlationId), { @@ -143,11 +112,6 @@ export const useCorrelationsQuery = () => { deleteSavedCorrelationMutation, isDeleting, - fetchCorrelationIdError, - fetchCorrelationIdSuccess, - fetchCorrelationIdLoading, - getCorrelationByIdMutation, - saveCorrelationMutation, isCorrelationSaving, diff --git a/src/layouts/MainLayout/providers/AppProvider.tsx b/src/layouts/MainLayout/providers/AppProvider.tsx index 6d33bb80..e71efab5 100644 --- a/src/layouts/MainLayout/providers/AppProvider.tsx +++ b/src/layouts/MainLayout/providers/AppProvider.tsx @@ -1,11 +1,12 @@ -import { LogStreamData } from '@/@types/parseable/api/stream'; -import initContext from '@/utils/initContext'; +import { FIXED_DURATIONS, FixedDuration } from '@/constants/timeConstants'; +import dayjs, { Dayjs } from 'dayjs'; + import { AboutData } from '@/@types/parseable/api/about'; -import _ from 'lodash'; import { AxiosResponse } from 'axios'; +import { LogStreamData } from '@/@types/parseable/api/stream'; import { SavedFilterType } from '@/@types/parseable/api/savedFilters'; -import { FIXED_DURATIONS, FixedDuration } from '@/constants/timeConstants'; -import dayjs, { Dayjs } from 'dayjs'; +import _ from 'lodash'; +import initContext from '@/utils/initContext'; import timeRangeUtils from '@/utils/timeRangeUtils'; const { makeTimeRangeLabel } = timeRangeUtils; @@ -57,6 +58,7 @@ type AppStore = { helpModalOpen: boolean; createStreamModalOpen: boolean; currentStream: null | string; + streamForCorrelation: null | string; userRoles: UserRoles | null; userSpecificStreams: null | LogStreamData; userAccessMap: { [key: string]: boolean }; @@ -76,6 +78,7 @@ type AppStoreReducers = { toggleMaximize: (store: AppStore) => ReducerOutput; toggleHelpModal: (store: AppStore, val?: boolean) => ReducerOutput; changeStream: (store: AppStore, stream: string) => ReducerOutput; + setStreamForCorrelation: (store: AppStore, stream: string) => ReducerOutput; setUserRoles: (store: AppStore, roles: UserRoles | null) => ReducerOutput; setshiftInterval: (store: AppStore, interval: number) => ReducerOutput; syncTimeRange: (store: AppStore) => ReducerOutput; @@ -93,6 +96,7 @@ const initialState: AppStore = { maximized: false, helpModalOpen: false, currentStream: null, + streamForCorrelation: null, userRoles: null, userSpecificStreams: null, userAccessMap: {}, @@ -208,6 +212,10 @@ const changeStream = (store: AppStore, stream: string) => { return { currentStream: stream, activeSavedFilters }; }; +const setStreamForCorrelation = (_store: AppStore, stream: string) => { + return { streamForCorrelation: stream }; +}; + const setUserRoles = (_store: AppStore, roles: UserRoles | null) => { return { userRoles: roles }; }; @@ -240,6 +248,7 @@ const appStoreReducers: AppStoreReducers = { toggleMaximize, toggleHelpModal, changeStream, + setStreamForCorrelation, setUserRoles, setUserSpecificStreams, setUserAccessMap, diff --git a/src/pages/Correlation/Views/CorrelationFooter.tsx b/src/pages/Correlation/Views/CorrelationFooter.tsx index 35f89e78..2dbd34e3 100644 --- a/src/pages/Correlation/Views/CorrelationFooter.tsx +++ b/src/pages/Correlation/Views/CorrelationFooter.tsx @@ -1,6 +1,6 @@ -import { FC, useCallback } from 'react'; +import { FC, useCallback, useEffect } from 'react'; import { usePagination } from '@mantine/hooks'; -import { Box, Center, Group, Menu, Pagination, Stack } from '@mantine/core'; +import { Box, Center, Group, Menu, Pagination, Stack, Tooltip } from '@mantine/core'; import _ from 'lodash'; import { Text } from '@mantine/core'; import { IconSelector } from '@tabler/icons-react'; @@ -9,9 +9,37 @@ import classes from '../styles/Footer.module.css'; import { LOGS_FOOTER_HEIGHT } from '@/constants/theme'; import { correlationStoreReducers, useCorrelationStore } from '@/pages/Correlation/providers/CorrelationProvider'; import { LOG_QUERY_LIMITS, LOAD_LIMIT } from '@/pages/Stream/providers/LogsProvider'; +import { HumanizeNumber } from '@/utils/formatBytes'; const { setCurrentOffset, setCurrentPage, setPageAndPageData } = correlationStoreReducers; +const TotalCount = (props: { totalCount: number }) => { + return ( + + {HumanizeNumber(props.totalCount)} + + ); +}; + +const TotalLogsCount = (props: { hasTableLoaded: boolean; isTableEmpty: boolean }) => { + const [{ totalCount, perPage, pageData }] = useCorrelationStore((store) => store.tableOpts); + const displayedCount = _.size(pageData); + const showingCount = displayedCount < perPage ? displayedCount : perPage; + if (typeof totalCount !== 'number' || typeof displayedCount !== 'number') return ; + + return ( + + {!props.isTableEmpty ? ( + <> + {`Showing ${showingCount} out of`} + + records + + ) : null} + + ); +}; + const LimitControl: FC = () => { const [opened, setOpened] = useMountedState(false); const [perPage, setCorrelationStore] = useCorrelationStore((store) => store.tableOpts.perPage); @@ -58,12 +86,18 @@ const LimitControl: FC = () => { const CorrelationFooter = (props: { loaded: boolean; hasNoData: boolean; isFetchingCount: boolean }) => { const [tableOpts, setCorrelationStore] = useCorrelationStore((store) => store.tableOpts); - const { totalPages, currentOffset, currentPage, perPage, totalCount } = tableOpts; + const [isCorrelatedData] = useCorrelationStore((store) => store.isCorrelatedData); + const { totalPages, currentOffset, currentPage, perPage, totalCount, targetPage } = tableOpts; const onPageChange = useCallback((page: number) => { - setCorrelationStore((store) => setPageAndPageData(store, page)); + setCorrelationStore((store) => setPageAndPageData(store, page, perPage)); }, []); + useEffect(() => { + if (!props.loaded) return; + pagination.setPage(targetPage ? targetPage : 1); + }, [props.loaded]); + const pagination = usePagination({ total: totalPages ?? 1, initialPage: 1, onChange: onPageChange }); const onChangeOffset = useCallback( (key: 'prev' | 'next') => { @@ -87,11 +121,9 @@ const CorrelationFooter = (props: { loaded: boolean; hasNoData: boolean; isFetch return ( - {/* */} + {isCorrelatedData && totalCount > 0 && ( + + )} {props.loaded ? ( diff --git a/src/pages/Correlation/Views/CorrelationJSONView.tsx b/src/pages/Correlation/Views/CorrelationJSONView.tsx new file mode 100644 index 00000000..bde18c03 --- /dev/null +++ b/src/pages/Correlation/Views/CorrelationJSONView.tsx @@ -0,0 +1,253 @@ +import { Box, Stack } from '@mantine/core'; +import { ErrorView, LoadingView } from '@/pages/Stream/Views/Explore/LoadingViews'; +import { IconCheck, IconCopy, IconDotsVertical } from '@tabler/icons-react'; +import { + PRIMARY_HEADER_HEIGHT, + STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT, + STREAM_SECONDARY_TOOLBAR_HRIGHT, +} from '@/constants/theme'; +import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; +import { formatLogTs, isJqSearch } from '@/pages/Stream/providers/LogsProvider'; + +import EmptyBox from '@/components/Empty'; +import { Log } from '@/@types/parseable/api/query'; +import _ from 'lodash'; +import classes from '../../Stream/styles/JSONView.module.css'; +import { copyTextToClipboard } from '@/utils'; +import timeRangeUtils from '@/utils/timeRangeUtils'; +import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import { useCorrelationStore } from '../providers/CorrelationProvider'; + +type ContextMenuState = { + visible: boolean; + x: number; + y: number; + row: Log | null; +}; + +const Item = (props: { header: string | null; value: string; highlight: boolean }) => { + return ( + + + {props.header}: {props.value} + + + ); +}; + +export const CopyIcon = (props: { value: Log | string }) => { + const copyIconRef = useRef(null); + const copiedIconRef = useRef(null); + + const onCopy = async (e: React.MouseEvent) => { + e.stopPropagation(); + if (copyIconRef.current && copiedIconRef.current) { + copyIconRef.current.style.display = 'none'; + copiedIconRef.current.style.display = 'flex'; + } + await copyTextToClipboard(props.value); + setTimeout(() => { + if (copyIconRef.current && copiedIconRef.current) { + copiedIconRef.current.style.display = 'none'; + copyIconRef.current.style.display = 'flex'; + } + }, 1500); + }; + + return ( + + + + + + + + + ); +}; + +const localTz = timeRangeUtils.getLocalTimezone(); + +const Row = (props: { + log: Log; + searchValue: string; + disableHighlight: boolean; + isRowHighlighted: boolean; + showEllipses: boolean; + setContextMenu: any; + shouldHighlight: (header: string | null, val: number | string | Date | null) => boolean; +}) => { + const [isSecureHTTPContext] = useAppStore((store) => store.isSecureHTTPContext); + const { log, disableHighlight, shouldHighlight, isRowHighlighted, showEllipses, setContextMenu } = props; + + return ( + + {showEllipses && ( +
{ + event.stopPropagation(); + setContextMenu({ + visible: true, + x: event.pageX, + y: event.pageY, + row: log, + }); + }}> + +
+ )} + + {_.isObject(log) ? ( + _.map(log, (value, key) => { + //skiping fields with empty strings + if (!_.toString(value)) return; + const fieldTypeMap = { + datetime: 'text', + host: 'text', + id: 'text', + method: 'text', + p_metadata: 'text', + p_tags: 'text', + p_timestamp: 'timestamp', + referrer: 'text', + status: 'number', + 'user-identifier': 'text', + }; + const isTimestamp = _.get(fieldTypeMap, key, null) === 'timestamp'; + const sanitizedValue = isTimestamp ? `${formatLogTs(_.toString(value))} (${localTz})` : _.toString(value); + return ( + + ); + }) + ) : ( + + )} + + {isSecureHTTPContext ? : null} +
+ ); +}; + +const JsonRows = (props: { isSearching: boolean; setContextMenu: any }) => { + const [{ pageData, instantSearchValue }] = useCorrelationStore((store) => store.tableOpts); + const disableHighlight = props.isSearching || _.isEmpty(instantSearchValue) || isJqSearch(instantSearchValue); + + const shouldHighlight = useCallback( + (header: string | null, val: number | string | Date | null) => { + return String(val).includes(instantSearchValue) || String(header).includes(instantSearchValue); + }, + [instantSearchValue], + ); + + return ( + + {_.map(pageData, (d, index) => ( + + ))} + + ); +}; + +const TableContainer = (props: { children: ReactNode }) => { + return {props.children}; +}; + +const CorrleationJSONView = (props: { errorMessage: string | null; hasNoData: boolean; showTable: boolean }) => { + const [maximized] = useAppStore((store) => store.maximized); + const [contextMenu, setContextMenu] = useState({ + visible: false, + x: 0, + y: 0, + row: null, + }); + + const contextMenuRef = useRef(null); + const { errorMessage, hasNoData, showTable } = props; + const [isSearching] = useState(false); + const primaryHeaderHeight = !maximized + ? PRIMARY_HEADER_HEIGHT + STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT + STREAM_SECONDARY_TOOLBAR_HRIGHT + : 0; + + // const showTableOrLoader = logsLoading || streamsLoading || showTable || !errorMessage || !hasNoData; + + 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 }); + + return ( + + {/* */} + {!errorMessage ? ( + showTable ? ( + + + + + + + + + {contextMenu.visible && ( +
+ )} +
+ ) : hasNoData ? ( + <> + + + ) : ( + + ) + ) : ( + + )} +
+ ); +}; + +export default CorrleationJSONView; diff --git a/src/pages/Correlation/components/CorrelationControlSection.tsx b/src/pages/Correlation/components/CorrelationControlSection.tsx new file mode 100644 index 00000000..44d492bd --- /dev/null +++ b/src/pages/Correlation/components/CorrelationControlSection.tsx @@ -0,0 +1,35 @@ +import classes from '../styles/Correlation.module.css'; +import SavedCorrelationsButton from './SavedCorrelationsBtn'; +import TimeRange from '@/components/Header/TimeRange'; +import RefreshInterval from '@/components/Header/RefreshInterval'; +import RefreshNow from '@/components/Header/RefreshNow'; +import ViewToggle from './CorrelationViewToggle'; +import ShareButton from './ShareButton'; +import { MaximizeButton } from '@/pages/Stream/components/PrimaryToolbar'; + +export const CorrelationControlSection = () => { + return ( +
+ {/* */} +
+
+ + + + +
+
+ + + +
+
+
+ ); +}; diff --git a/src/pages/Correlation/components/CorrelationFieldItem.tsx b/src/pages/Correlation/components/CorrelationFieldItem.tsx index 0119cebd..1a6559f1 100644 --- a/src/pages/Correlation/components/CorrelationFieldItem.tsx +++ b/src/pages/Correlation/components/CorrelationFieldItem.tsx @@ -8,9 +8,9 @@ import { } from '@tabler/icons-react'; import { Text, Tooltip } from '@mantine/core'; import classes from '../styles/Correlation.module.css'; -import { useRef, useState, useEffect } from 'react'; +import { useRef } from 'react'; -const dataTypeIcons = (iconColor: string): Record => ({ +export const dataTypeIcons = (iconColor: string): Record => ({ text: , timestamp: , number: , @@ -40,13 +40,6 @@ export const CorrelationFieldItem = ({ onDelete, }: CorrelationFieldItemProps) => { const textRef = useRef(null); - const [isOverflowing, setIsOverflowing] = useState(false); - - useEffect(() => { - if (textRef.current) { - setIsOverflowing(textRef.current.scrollWidth > textRef.current.clientWidth); - } - }, [fieldName]); return (
- {isOverflowing ? ( - - - {fieldName} - - - ) : ( + {fieldName} - )} + + {!dataType && } {dataType && dataTypeIcons(iconColor)[dataType]}
diff --git a/src/pages/Correlation/components/CorrelationFieldsSection.tsx b/src/pages/Correlation/components/CorrelationFieldsSection.tsx new file mode 100644 index 00000000..a6f3ee02 --- /dev/null +++ b/src/pages/Correlation/components/CorrelationFieldsSection.tsx @@ -0,0 +1,55 @@ +import { FC } from 'react'; +import { correlationStoreReducers, useCorrelationStore } from '../providers/CorrelationProvider'; +import { CorrelationFieldItem } from './CorrelationFieldItem'; +import classes from '../styles/Correlation.module.css'; +import { Text } from '@mantine/core'; + +const { deleteSelectedField } = correlationStoreReducers; + +interface CorrelationFieldsSectionProps { + setIsCorrelationEnabled: (enabled: boolean) => void; +} + +export const CorrelationFieldsSection: FC = ({ setIsCorrelationEnabled }) => { + const [{ fields, selectedFields, isCorrelatedData }, setCorrelationData] = useCorrelationStore((store) => store); + const streamNames = Object.keys(fields); + + return ( +
+ 0 ? 'black' : '#CBCBCB', + }}> + Fields + +
0 ? '1px solid #CBCBCB' : '1px solid #e1e5e8', + backgroundColor: streamNames.length > 0 ? 'white' : '#F7F8F9', + }} + className={classes.fieldsPillsWrapper}> + {Object.keys(selectedFields).length < 1 && ( + + Click on fields to correlate + + )} + {Object.entries(selectedFields).map(([streamName, fieldsMap]: [any, any]) => + fieldsMap.map((field: any, index: any) => ( + { + isCorrelatedData && setIsCorrelationEnabled(true); + setCorrelationData((store) => deleteSelectedField(store, field, streamName)); + }} + /> + )), + )} +
+
+ ); +}; diff --git a/src/pages/Correlation/components/CorrelationJoinSection.tsx b/src/pages/Correlation/components/CorrelationJoinSection.tsx new file mode 100644 index 00000000..3300932b --- /dev/null +++ b/src/pages/Correlation/components/CorrelationJoinSection.tsx @@ -0,0 +1,221 @@ +import { FC, useCallback } from 'react'; +import { correlationStoreReducers, useCorrelationStore } from '../providers/CorrelationProvider'; +import { dataTypeIcons } from './CorrelationFieldItem'; +import classes from '../styles/Correlation.module.css'; +import { ActionIcon, Badge, Button, Select, SelectProps, Text } from '@mantine/core'; +import { STREAM_PRIMARY_TOOLBAR_HEIGHT } from '@/constants/theme'; +import { IconX } from '@tabler/icons-react'; +import { appStoreReducers, useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import { useCorrelationFetchCount } from '../hooks/useCorrelationFetchCount'; +import { useCorrelationQueryLogs } from '@/hooks/useCorrelationQueryLogs'; +import { CorrelationSaveIcon } from './CorrelationSaveIcon'; + +const { + toggleSaveCorrelationModal, + setCorrelationCondition, + setActiveCorrelation, + setSelectedFields, + setIsCorrelatedFlag, + setCorrelationId, +} = correlationStoreReducers; + +const { syncTimeRange } = appStoreReducers; + +interface CorrelationJoinSectionProps { + select1Value: { value: string | null; dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null }; + select2Value: { value: string | null; dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null }; + isCorrelationEnabled: boolean; + setIsCorrelationEnabled: (enabled: boolean) => void; + setSelect1Value: (value: { + value: string | null; + dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null; + }) => void; + setSelect2Value: (value: { + value: string | null; + dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null; + }) => void; +} + +export const CorrelationJoinSection: FC = ({ + setIsCorrelationEnabled, + setSelect1Value, + setSelect2Value, + select1Value, + select2Value, + isCorrelationEnabled, +}) => { + const [{ fields, selectedFields, isCorrelatedData }, setCorrelationData] = useCorrelationStore((store) => store); + const [, setAppStore] = useAppStore((store) => store); + const streamNames = Object.keys(fields); + const { refetchCount } = useCorrelationFetchCount(); + const { getCorrelationData } = useCorrelationQueryLogs(); + + const clearQuery = () => { + setSelect1Value({ value: null, dataType: '' }); + setSelect2Value({ value: null, dataType: '' }); + setCorrelationData((store) => setCorrelationCondition(store, '')); + setCorrelationData((store) => setSelectedFields(store, '', '', true)); + setCorrelationData((store) => setIsCorrelatedFlag(store, false)); + setCorrelationData((store) => setCorrelationId(store, '')); + setCorrelationData((store) => setActiveCorrelation(store, null)); + setIsCorrelationEnabled(false); + setAppStore(syncTimeRange); + }; + + const renderJoinOneOptions: SelectProps['renderOption'] = ({ option }) => { + const fieldType = fields[streamNames[0]]?.fieldTypeMap[option.value]; + return ( +
+ {option.label} + {dataTypeIcons('black')[fieldType]} +
+ ); + }; + + const renderJoinTwoOptions: SelectProps['renderOption'] = ({ option }) => { + const fieldType = fields[streamNames[1]]?.fieldTypeMap[option.value]; + return ( +
+ {option.label} + {dataTypeIcons('black')[fieldType]} +
+ ); + }; + + const handleFieldChange = (fieldValue: string | null, isFirstField: boolean) => { + if (isFirstField) { + const fieldType = fieldValue && fields[streamNames[0]]?.fieldTypeMap[fieldValue]; + console.log(fieldType); + + setSelect1Value({ value: fieldValue, dataType: fieldType }); + } else { + setSelect2Value({ value: fieldValue }); + } + }; + + const openSaveCorrelationModal = useCallback(() => { + setCorrelationData((store) => toggleSaveCorrelationModal(store, true)); + }, []); + + return ( +
+ 0 ? 'black' : '#CBCBCB', + flexShrink: 0, + flexGrow: 0, + }}> + Joins + +
+
+ 1 + ? Object.keys(fields[streamNames[1]].fieldTypeMap).filter((key) => { + const dataType = fields[streamNames[1]].fieldTypeMap[key]; + return dataType !== 'list' && (!select1Value.dataType || select1Value.dataType === dataType); + }) + : [] + } + value={select2Value.value} + onChange={(value) => handleFieldChange(value, false)} + renderOption={renderJoinTwoOptions} + /> +
+ { + openSaveCorrelationModal(); + }}> + + +
+ {isCorrelatedData && ( + { + setSelect1Value({ value: null, dataType: '' }); + setSelect2Value({ value: null, dataType: '' }); + setCorrelationData((store) => setCorrelationCondition(store, '')); + setCorrelationData((store) => setIsCorrelatedFlag(store, false)); + setCorrelationData((store) => setCorrelationId(store, '')); + setCorrelationData((store) => setActiveCorrelation(store, null)); + setIsCorrelationEnabled(false); + setAppStore(syncTimeRange); + }} + /> + }> + Join Applied + + )} +
+
+ + +
+
+
+ ); +}; diff --git a/src/pages/Correlation/components/CorrelationSaveIcon.tsx b/src/pages/Correlation/components/CorrelationSaveIcon.tsx new file mode 100644 index 00000000..cd7f959e --- /dev/null +++ b/src/pages/Correlation/components/CorrelationSaveIcon.tsx @@ -0,0 +1,24 @@ +import { px } from '@mantine/core'; + +export const CorrelationSaveIcon = () => ( + + + + + + +); + +CorrelationSaveIcon.displayName = 'CorrelationSaveIcon'; diff --git a/src/pages/Correlation/components/CorrelationSideBar.tsx b/src/pages/Correlation/components/CorrelationSideBar.tsx new file mode 100644 index 00000000..10edfeb0 --- /dev/null +++ b/src/pages/Correlation/components/CorrelationSideBar.tsx @@ -0,0 +1,177 @@ +import { FC, useState } from 'react'; +import { Text, TextInput } from '@mantine/core'; +import { appStoreReducers, useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import { correlationStoreReducers, useCorrelationStore } from '../providers/CorrelationProvider'; + +import { CorrelationFieldItem } from './CorrelationFieldItem'; +import { IconTrashX } from '@tabler/icons-react'; +import { StreamSelectBox } from './StreamSelectBox'; +import classes from '../styles/Correlation.module.css'; + +const { setStreamForCorrelation } = appStoreReducers; +const { setIsCorrelatedFlag, setSelectedFields, deleteStreamData } = correlationStoreReducers; + +interface CorrelationSideBarProps { + setIsCorrelationEnabled: (enabled: boolean) => void; + setSelect1Value: (value: { + value: string | null; + dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null; + }) => void; + setSelect2Value: (value: { + value: string | null; + dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null; + }) => void; + loadingState: boolean; + isLoading: boolean; +} + +export const CorrelationSidebar: FC = ({ + setIsCorrelationEnabled, + setSelect1Value, + setSelect2Value, + loadingState, + isLoading, +}) => { + const [searchText, setSearchText] = useState(''); + + const [{ fields, selectedFields, isCorrelatedData }, setCorrelationData] = useCorrelationStore((store) => store); + const [userSpecificStreams, setAppStore] = useAppStore((store) => store.userSpecificStreams); + const streamNames = Object.keys(fields); + const streamData = + userSpecificStreams?.map((stream: any) => ({ + value: stream.name, + label: stream.name, + })) ?? []; + + const filterFields = (fieldsIter: any) => { + const typedFields = Object.keys(fieldsIter.fieldTypeMap) as string[]; + return searchText + ? typedFields.filter((field) => field.toLowerCase().includes(searchText.toLowerCase())) + : typedFields; + }; + const addStream = (value: string | null) => { + if (value) { + setAppStore((store) => setStreamForCorrelation(store, value)); + } + }; + + return ( +
+ Streams + setSearchText(e.target.value)} + /> +
+ {Object.entries(fields).map(([stream, fieldsIter]: [any, any]) => { + if (!fieldsIter) return; + const filteredFields = filterFields(fieldsIter); + const totalStreams = Object.entries(fields).length; + const heightPercentage = totalStreams === 1 ? '50%' : `${100 / totalStreams}%`; + + return ( +
+
+ + {stream} + + { + setAppStore((store) => setStreamForCorrelation(store, '')); + setCorrelationData((store) => setIsCorrelatedFlag(store, false)); + setSelect1Value({ value: null, dataType: '' }); + setSelect2Value({ value: null, dataType: '' }); + setCorrelationData((store) => deleteStreamData(store, stream)); + setIsCorrelationEnabled(false); + }} + /> +
+ {filteredFields.length > 0 ? ( +
+ {filteredFields.map((field: string) => { + const isSelected = selectedFields[stream]?.includes(field); + const dataType = fieldsIter.fieldTypeMap[field]; + return ( + { + if (isLoading) return; + if (isCorrelatedData) { + setIsCorrelationEnabled(true); + } + setCorrelationData((store) => setSelectedFields(store, field, stream)); + }} + /> + ); + })} +
+ ) : ( + No fields match your search. + )} +
+ ); + })} + {streamNames.length === 0 && ( + <> + {/* First box */} + value && addStream(value)} + data={streamData.filter((stream) => !streamNames.includes(stream.value))} + isFirst={true} + /> + + {/* Second box */} + addStream(value)} + data={streamData.filter((stream) => !streamNames.includes(stream.value))} + isFirst={false} + /> + + )} + {streamNames.length === 1 && ( + <> + {/* Render the single existing field */} + addStream(value)} + data={streamData.filter((stream) => !streamNames.includes(stream.value))} + isFirst={false} + /> + + )} +
+
+ ); +}; diff --git a/src/pages/Correlation/components/CorrelationToolbar.tsx b/src/pages/Correlation/components/CorrelationToolbar.tsx new file mode 100644 index 00000000..62f7fabd --- /dev/null +++ b/src/pages/Correlation/components/CorrelationToolbar.tsx @@ -0,0 +1,52 @@ +import { Stack } from '@mantine/core'; +import { STREAM_SECONDARY_TOOLBAR_HRIGHT } from '@/constants/theme'; +import classes from '../styles/Correlation.module.css'; +import MultiEventTimeLineGraph from './MultiEventTimeLineGraph'; +import { CorrelationFieldsSection } from './CorrelationFieldsSection'; +import { FC } from 'react'; +import { CorrelationJoinSection } from './CorrelationJoinSection'; +import { CorrelationControlSection } from './CorrelationControlSection'; + +interface CorrelationToolbarProps { + setIsCorrelationEnabled: (enabled: boolean) => void; + select1Value: { value: string | null; dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null }; + select2Value: { value: string | null; dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null }; + isCorrelationEnabled: boolean; + setSelect1Value: (value: { + value: string | null; + dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null; + }) => void; + setSelect2Value: (value: { + value: string | null; + dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null; + }) => void; +} + +export const CorrelationToolbar: FC = ({ + setIsCorrelationEnabled, + setSelect1Value, + setSelect2Value, + select1Value, + select2Value, + isCorrelationEnabled, +}) => { + return ( + + + + + + + + + + + ); +}; diff --git a/src/pages/Correlation/components/CorrelationViewToggle.tsx b/src/pages/Correlation/components/CorrelationViewToggle.tsx new file mode 100644 index 00000000..11bd101f --- /dev/null +++ b/src/pages/Correlation/components/CorrelationViewToggle.tsx @@ -0,0 +1,35 @@ +import { Button, rem } from '@mantine/core'; +import { correlationStoreReducers, useCorrelationStore } from '../providers/CorrelationProvider'; +import { useCallback } from 'react'; +import { IconTable } from '@tabler/icons-react'; +import classes from '../styles/SavedCorrelationsBtn.module.css'; + +const { onToggleView } = correlationStoreReducers; + +const ViewToggle = () => { + const [viewMode, setCorrelationStore] = useCorrelationStore((store) => store.viewMode); + const iconProps = { + style: { width: rem(20), height: rem(20), display: 'block' }, + stroke: 1.8, + }; + const onToggle = useCallback(() => { + setCorrelationStore((store) => onToggleView(store, viewMode === 'table' ? 'json' : 'table')); + }, [viewMode]); + + const isActive = viewMode === 'table'; + return ( + + ); +}; + +export default ViewToggle; diff --git a/src/pages/Correlation/components/SavedCorrelationItem.tsx b/src/pages/Correlation/components/SavedCorrelationItem.tsx index e2240a8e..e785dabd 100644 --- a/src/pages/Correlation/components/SavedCorrelationItem.tsx +++ b/src/pages/Correlation/components/SavedCorrelationItem.tsx @@ -1,14 +1,15 @@ -import { Stack, Box, Button, Text, px, Code } from '@mantine/core'; +import { Box, Button, Code, Stack, Text, px } from '@mantine/core'; +import { FC, useCallback, useState } from 'react'; import { IconClock, IconEye, IconEyeOff, IconTrash, IconX } from '@tabler/icons-react'; -import { useState, useCallback, Fragment, FC } from 'react'; -import classes from '../styles/SavedCorrelationItem.module.css'; +import { correlationStoreReducers, useCorrelationStore } from '../providers/CorrelationProvider'; + import { Correlation } from '@/@types/parseable/api/correlation'; -import dayjs from 'dayjs'; import IconButton from '@/components/Button/IconButton'; +import classes from '../styles/SavedCorrelationItem.module.css'; +import dayjs from 'dayjs'; import { useCorrelationsQuery } from '@/hooks/useCorrelations'; -import { correlationStoreReducers, useCorrelationStore } from '../providers/CorrelationProvider'; -const { toggleSavedCorrelationsModal, setCorrelationId } = correlationStoreReducers; +const { toggleSavedCorrelationsModal, setCorrelationId, cleanCorrelationStore } = correlationStoreReducers; const renderDeleteIcon = () => ; const renderCloseIcon = () => ; @@ -51,14 +52,13 @@ const SelectedFields: FC<{ tableConfigs: TableConfig[] }> = ({ tableConfigs }) = ); return ( -
- Selected Fields: - {fields.map((field, index) => ( - - {field.content} - {index < fields.length - 1 && ,} - - ))} +
+
Selected Fields:
+
+ {fields.map((field, index) => ( + {field.content} + ))} +
); }; @@ -73,8 +73,8 @@ const JoinConditions: FC<{ joinConfig: JoinConfig }> = ({ joinConfig }) => { if (!nextJoin) return null; return ( -
- Join Condition: +
+
Join Condition:
{`${join.tableName}.${join.field}`} = {`${nextJoin.tableName}.${nextJoin.field}`} @@ -106,6 +106,7 @@ const SavedCorrelationItem = (props: { item: Correlation }) => { }, []); const onCorrelationAppy = useCallback(() => { + setCorrelationData(cleanCorrelationStore); setCorrelationData((store) => setCorrelationId(store, id)); closeModal(); }, []); @@ -154,7 +155,7 @@ const SavedCorrelationItem = (props: { item: Correlation }) => { {showQuery && ( - + diff --git a/src/pages/Correlation/hooks/useCorrelationFetchCount.tsx b/src/pages/Correlation/hooks/useCorrelationFetchCount.tsx new file mode 100644 index 00000000..895dfcd6 --- /dev/null +++ b/src/pages/Correlation/hooks/useCorrelationFetchCount.tsx @@ -0,0 +1,63 @@ +import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import { + CORRELATION_LOAD_LIMIT, + correlationStoreReducers, + useCorrelationStore, +} from '../providers/CorrelationProvider'; +import { useState } from 'react'; +import { useQuery } from 'react-query'; +import { getCorrelationQueryCount } from '@/api/query'; +import _ from 'lodash'; +import { useStreamStore } from '@/pages/Stream/providers/StreamProvider'; + +const { setTotalCount } = correlationStoreReducers; + +export const useCorrelationFetchCount = () => { + const [timeRange] = useAppStore((store) => store.timeRange); + const [{ fields, tableOpts, selectedFields, correlationCondition }, setCorrelationData] = useCorrelationStore( + (store) => store, + ); + const [streamInfo] = useStreamStore((store) => store.info); + const timePartitionColumn = _.get(streamInfo, 'time_partition', 'p_timestamp'); + const [countLoading, setCountLoading] = useState(true); + + const streamNames = Object.keys(fields); + const defaultQueryOpts = { + startTime: timeRange.startTime, + endTime: timeRange.endTime, + limit: CORRELATION_LOAD_LIMIT, + pageOffset: tableOpts.currentOffset, + timePartitionColumn, + selectedFields: _.flatMap(selectedFields, (values, key) => _.map(values, (value) => `${key}.${value}`)) || [], + correlationCondition: correlationCondition, + }; + const queryOpts = { ...defaultQueryOpts, streamNames }; + + const { refetch: refetchCount } = useQuery( + ['fetchCount', defaultQueryOpts], + async () => { + setCountLoading(true); + const data = await getCorrelationQueryCount(queryOpts); + const count = _.first(data.data)?.count; + typeof count === 'number' && setCorrelationData((store) => setTotalCount(store, count)); + return data; + }, + { + // query for count should always hit the endpoint for parseable query + refetchOnWindowFocus: false, + retry: false, + enabled: false, + onSuccess: () => { + setCountLoading(false); + }, + onError: () => { + setCountLoading(false); + }, + }, + ); + + return { + countLoading, + refetchCount, + }; +}; diff --git a/src/pages/Correlation/hooks/useParamsController.ts b/src/pages/Correlation/hooks/useParamsController.ts index aeb4fb02..5383746f 100644 --- a/src/pages/Correlation/hooks/useParamsController.ts +++ b/src/pages/Correlation/hooks/useParamsController.ts @@ -7,12 +7,14 @@ import timeRangeUtils from '@/utils/timeRangeUtils'; import moment from 'moment-timezone'; import { appStoreReducers, TimeRange, useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; import { correlationStoreReducers, useCorrelationStore } from '../providers/CorrelationProvider'; +import { getOffset } from '@/utils'; +import { LOG_QUERY_LIMITS } from '@/pages/Stream/providers/LogsProvider'; const { getRelativeStartAndEndDate, formatDateWithTimezone, getLocalTimezone } = timeRangeUtils; -const { setCorrelationId } = correlationStoreReducers; +const { setCorrelationId, setTargetPage, setCurrentOffset, setPerPage } = correlationStoreReducers; const { setTimeRange } = appStoreReducers; const timeRangeFormat = 'DD-MMM-YYYY_HH-mmz'; -const keys = ['id', 'interval', 'from', 'to']; +const keys = ['id', 'interval', 'from', 'to', 'page', 'rows']; const dateToParamString = (date: Date) => { return formatDateWithTimezone(date, timeRangeFormat); @@ -48,10 +50,17 @@ const deriveTimeRangeParams = (timerange: TimeRange): { interval: string } | { f } }; -const storeToParamsObj = (opts: { correlationId: string; timeRange: TimeRange }): Record => { - const { correlationId, timeRange } = opts; +const storeToParamsObj = (opts: { + correlationId: string; + timeRange: TimeRange; + rows: string; + page: string; +}): Record => { + const { correlationId, timeRange, page, rows } = opts; const params: Record = { id: correlationId, + rows, + page, ...deriveTimeRangeParams(timeRange), }; return _.pickBy(params, (val, key) => !_.isEmpty(val) && _.includes(keys, key)); @@ -71,39 +80,87 @@ const paramsStringToParamsObj = (searchParams: URLSearchParams): Record { const [isStoreSynced, setStoreSynced] = useState(false); const [{ correlationId }, setCorrelationStore] = useCorrelationStore((store) => store); + const [tableOpts] = useCorrelationStore((store) => store.tableOpts); const [timeRange, setAppStore] = useAppStore((store) => store.timeRange); const [searchParams, setSearchParams] = useSearchParams(); + const { currentOffset, currentPage, targetPage, perPage } = tableOpts; + + const pageOffset = Math.ceil(currentOffset / perPage); + useEffect(() => { - const storeAsParams = storeToParamsObj({ correlationId, timeRange }); + const storeAsParams = storeToParamsObj({ + correlationId, + timeRange, + rows: `${perPage}`, + page: `${targetPage ? targetPage : Math.ceil(currentPage + pageOffset)}`, + }); const presentParams = paramsStringToParamsObj(searchParams); if (storeAsParams.id !== presentParams.id) { setCorrelationStore((store) => setCorrelationId(store, presentParams.id)); } + if (storeAsParams.rows !== presentParams.rows && LOG_QUERY_LIMITS.includes(_.toNumber(presentParams.rows))) { + setCorrelationStore((store) => setPerPage(store, _.toNumber(presentParams.rows))); + } + if (storeAsParams.page !== presentParams.page && !_.isEmpty(presentParams.page)) { + setCorrelationStore((store) => setTargetPage(store, _.toNumber(presentParams.page))); + const offset = getOffset(_.toNumber(presentParams.page), _.toNumber(presentParams.rows)); + + if (offset > 0) { + setCorrelationStore((store) => setCurrentOffset(store, offset)); + + setCorrelationStore((store) => + setTargetPage( + store, + Math.abs(_.toNumber(presentParams.page) - Math.ceil(offset / _.toNumber(presentParams.rows))), + ), + ); + } + } syncTimeRangeToStore(storeAsParams, presentParams); setStoreSynced(true); }, []); useEffect(() => { if (isStoreSynced) { - const storeAsParams = storeToParamsObj({ correlationId, timeRange }); + const storeAsParams = storeToParamsObj({ + correlationId, + timeRange, + rows: `${perPage}`, + page: `${targetPage ? targetPage : Math.ceil(currentPage + pageOffset)}`, + }); const presentParams = paramsStringToParamsObj(searchParams); if (_.isEqual(storeAsParams, presentParams)) return; setSearchParams(storeAsParams); } - }, [correlationId, isStoreSynced, timeRange.startTime.toISOString(), timeRange.endTime.toISOString()]); + }, [ + correlationId, + isStoreSynced, + timeRange.startTime.toISOString(), + timeRange.endTime.toISOString(), + targetPage, + tableOpts, + ]); useEffect(() => { if (!isStoreSynced) return; - const storeAsParams = storeToParamsObj({ correlationId, timeRange }); + const storeAsParams = storeToParamsObj({ + correlationId, + timeRange, + rows: `${perPage}`, + page: `${targetPage ? targetPage : Math.ceil(currentPage + pageOffset)}`, + }); const presentParams = paramsStringToParamsObj(searchParams); if (_.isEqual(storeAsParams, presentParams)) return; if (storeAsParams.id !== presentParams.id) { setCorrelationStore((store) => setCorrelationId(store, presentParams.id)); } + if (storeAsParams.rows !== presentParams.rows && LOG_QUERY_LIMITS.includes(_.toNumber(presentParams.rows))) { + setCorrelationStore((store) => setPerPage(store, _.toNumber(presentParams.rows))); + } syncTimeRangeToStore(storeAsParams, presentParams); }, [searchParams]); diff --git a/src/pages/Correlation/index.tsx b/src/pages/Correlation/index.tsx index 952f1028..20879487 100644 --- a/src/pages/Correlation/index.tsx +++ b/src/pages/Correlation/index.tsx @@ -1,53 +1,46 @@ -import { useCallback, useEffect, useState } from 'react'; -import { useDocumentTitle } from '@mantine/hooks'; -import { Stack, Box, TextInput, Text, Select, Button, Center, Skeleton, Stepper } from '@mantine/core'; -import { IconTrashX } from '@tabler/icons-react'; +import { Box, Center, Stack, Stepper } from '@mantine/core'; import { PRIMARY_HEADER_HEIGHT, STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT, - STREAM_PRIMARY_TOOLBAR_HEIGHT, STREAM_SECONDARY_TOOLBAR_HRIGHT, } from '@/constants/theme'; -import classes from './styles/Correlation.module.css'; -import { useCorrelationQueryLogs } from '@/hooks/useCorrelationQueryLogs'; -import { useGetMultipleStreamSchemas, useGetStreamSchema } from '@/hooks/useGetCorrelationStreamSchema'; -import { useFetchStreamData } from '@/hooks/useFetchStreamData'; -import { correlationStoreReducers, useCorrelationStore } from './providers/CorrelationProvider'; import { appStoreReducers, useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; -import CorrelationTable from './Views/CorrelationTable'; -import CorrelationFooter from './Views/CorrelationFooter'; -import TimeRange from '@/components/Header/TimeRange'; -import RefreshInterval from '@/components/Header/RefreshInterval'; -import RefreshNow from '@/components/Header/RefreshNow'; -import MultiEventTimeLineGraph from './components/MultiEventTimeLineGraph'; +import { correlationStoreReducers, useCorrelationStore } from './providers/CorrelationProvider'; +import { useEffect, useState } from 'react'; +import { useGetMultipleStreamSchemas, useGetStreamSchema } from '@/hooks/useGetCorrelationStreamSchema'; + import { CorrelationEmptyPlaceholder } from './components/CorrelationEmptyPlaceholder'; -import { StreamSelectBox } from './components/StreamSelectBox'; -import { CorrelationFieldItem } from './components/CorrelationFieldItem'; -import { MaximizeButton } from '../Stream/components/PrimaryToolbar'; -import ShareButton from './components/ShareButton'; -import useParamsController from './hooks/useParamsController'; +import CorrelationFooter from './Views/CorrelationFooter'; +import { CorrelationSidebar } from './components/CorrelationSideBar'; +import CorrelationTable from './Views/CorrelationTable'; +import { CorrelationToolbar } from './components/CorrelationToolbar'; +import CorrleationJSONView from './Views/CorrelationJSONView'; +import SaveCorrelationModal from './components/SaveCorrelationModal'; +import SavedCorrelationsModal from './components/SavedCorrelationsModal'; import _ from 'lodash'; +import classes from './styles/Correlation.module.css'; +import dayjs from 'dayjs'; +import { useCorrelationFetchCount } from './hooks/useCorrelationFetchCount'; +import { useCorrelationQueryLogs } from '@/hooks/useCorrelationQueryLogs'; import { useCorrelationsQuery } from '@/hooks/useCorrelations'; -import SavedCorrelationsButton from './components/SavedCorrelationsBtn'; -import SavedCorrelationsModal from './components/SavedCorrelationsModal'; -import SaveCorrelationModal from './components/SaveCorrelationModal'; +import { useDocumentTitle } from '@mantine/hooks'; +import { useFetchStreamData } from '@/hooks/useFetchStreamData'; +import useParamsController from './hooks/useParamsController'; -const { changeStream, syncTimeRange } = appStoreReducers; +const { setStreamForCorrelation, setTimeRange } = appStoreReducers; +import { getLogStreamSchema } from '@/api/logStream'; const { - deleteStreamData, setSelectedFields, - deleteSelectedField, setCorrelationCondition, - setIsCorrelatedFlag, - toggleSaveCorrelationModal, setActiveCorrelation, - setCorrelationId, + setPageAndPageData, + setTargetPage, + setStreamSchema, } = correlationStoreReducers; const Correlation = () => { useDocumentTitle('Parseable | Correlation'); // State Management Hooks - const [userSpecificStreams] = useAppStore((store) => store.userSpecificStreams); const [ { fields, @@ -58,31 +51,46 @@ const Correlation = () => { correlationCondition, correlationId, savedCorrelationId, + viewMode, + correlations, }, setCorrelationData, ] = useCorrelationStore((store) => store); const { isStoreSynced } = useParamsController(); const [timeRange] = useAppStore((store) => store.timeRange); - const [currentStream, setAppStore] = useAppStore((store) => store.currentStream); + const [streamForCorrelation, setAppStore] = useAppStore((store) => store.streamForCorrelation); const [maximized] = useAppStore((store) => store.maximized); const { isLoading: schemaLoading } = useGetStreamSchema({ - streamName: currentStream || '', + streamName: streamForCorrelation || '', }); const isSavedCorrelation = correlationId !== savedCorrelationId; const streamsToFetch = (isSavedCorrelation && activeCorrelation?.tableConfigs.map((config: { tableName: string }) => config.tableName)) || []; const { isLoading: multipleSchemasLoading } = useGetMultipleStreamSchemas(streamsToFetch); + const [schemaLoad, setSchemaLoad] = useState(false); - const { getCorrelationData, loading: logsLoading, error: errorMessage } = useCorrelationQueryLogs(); + const { getCorrelationData, loadingState, error: errorMessage } = useCorrelationQueryLogs(); const { getFetchStreamData, loading: streamsLoading } = useFetchStreamData(); - const { fetchCorrelations, getCorrelationByIdMutation } = useCorrelationsQuery(); + const { fetchCorrelations } = useCorrelationsQuery(); + const { refetchCount, countLoading } = useCorrelationFetchCount(); // Local State - const [searchText, setSearchText] = useState(''); - const [select1Value, setSelect1Value] = useState(null); - const [select2Value, setSelect2Value] = useState(null); + const [select1Value, setSelect1Value] = useState<{ + value: string | null; + dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null; + }>({ + value: null, + dataType: '', + }); + const [select2Value, setSelect2Value] = useState<{ + value: string | null; + dataType?: '' | 'number' | 'boolean' | 'text' | 'timestamp' | 'list' | null; + }>({ + value: null, + dataType: '', + }); const [isCorrelationEnabled, setIsCorrelationEnabled] = useState(false); const [isLoading, setIsLoading] = useState(true); @@ -92,11 +100,7 @@ const Correlation = () => { : PRIMARY_HEADER_HEIGHT + STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT + STREAM_SECONDARY_TOOLBAR_HRIGHT; const streamNames = Object.keys(fields); - const streamData = - userSpecificStreams?.map((stream: any) => ({ - value: stream.name, - label: stream.name, - })) ?? []; + const { currentOffset, pageData, targetPage, currentPage } = tableOpts; // Effects useEffect(() => { @@ -119,10 +123,10 @@ const Correlation = () => { ); if (sortedJoinConditions[0]) { - setSelect1Value(sortedJoinConditions[0].field); + setSelect1Value({ value: sortedJoinConditions[0].field }); } if (sortedJoinConditions[1]) { - setSelect2Value(sortedJoinConditions[1].field); + setSelect2Value({ value: sortedJoinConditions[1].field }); } activeCorrelation?.tableConfigs.flatMap((config) => @@ -134,369 +138,129 @@ const Correlation = () => { useEffect(() => { if (!isSavedCorrelation || !correlationId) return; - getCorrelationByIdMutation(correlationId); - }, [correlationId]); + const activeCorrelation = correlations?.find((correlation) => correlation.id === correlationId) || null; + activeCorrelation?.startTime && + activeCorrelation?.endTime && + setAppStore((store) => + setTimeRange(store, { + startTime: dayjs(activeCorrelation?.startTime), + endTime: dayjs(activeCorrelation?.endTime), + type: 'custom', + }), + ); + if (!activeCorrelation) return; + + const fetchSchema = async (streamName: string) => { + setSchemaLoad(true); + try { + const schema = await getLogStreamSchema(streamName); + setCorrelationData((store) => setStreamSchema(store, schema.data, streamName)); + } catch (error) { + console.log(error); + } finally { + setSchemaLoad(false); + } + }; + const streamNames = activeCorrelation.tableConfigs.map((config: { tableName: string }) => config.tableName); + streamNames.forEach(async (el) => await fetchSchema(el)); + + setSelect1Value({ value: null, dataType: '' }); + setSelect2Value({ value: null, dataType: '' }); + setCorrelationData((store) => setCorrelationCondition(store, '')); + setCorrelationData((store) => setSelectedFields(store, '', '', true)); + setCorrelationData((store) => setActiveCorrelation(store, activeCorrelation)); + }, [correlationId, correlations]); useEffect(() => { - if (currentStream && streamNames.length > 0 && Object.keys(fields).includes(currentStream)) { + if (streamForCorrelation && streamNames.length > 0 && Object.keys(fields).includes(streamForCorrelation)) { getFetchStreamData(); } - }, [currentStream, fields]); - - useEffect(() => { - getFetchStreamData(); - }, [isCorrelatedData]); + }, [streamForCorrelation, fields]); useEffect(() => { - if (isCorrelatedData) { + if (isCorrelatedData && !schemaLoad) { getCorrelationData(); } else { getFetchStreamData(); } - }, [timeRange]); + }, [currentOffset, timeRange, schemaLoad]); useEffect(() => { + if (schemaLoad) return; updateCorrelationCondition(); if (activeCorrelation && correlationCondition && isSavedCorrelation) { - setCorrelationData((store) => setIsCorrelatedFlag(store, true)); + refetchCount(); getCorrelationData(); } correlationCondition && setIsCorrelationEnabled(true); - }, [select1Value, select2Value, activeCorrelation, correlationCondition]); - - // Utility Functions - const filterFields = (fieldsIter: any) => { - const typedFields = Object.keys(fieldsIter.fieldTypeMap) as string[]; - return searchText - ? typedFields.filter((field) => field.toLowerCase().includes(searchText.toLowerCase())) - : typedFields; - }; + }, [select1Value, select2Value, activeCorrelation, correlationCondition, schemaLoad]); const updateCorrelationCondition = () => { - if (select1Value && select2Value) { - const condition = `"${streamNames[0]}".${select1Value} = "${streamNames[1]}".${select2Value}`; - setAppStore((store) => changeStream(store, 'correlatedStream')); + if (select1Value.value && select2Value.value) { + const condition = `"${streamNames[0]}".${select1Value.value} = "${streamNames[1]}".${select2Value.value}`; + setAppStore((store) => setStreamForCorrelation(store, 'correlatedStream')); setCorrelationData((store) => setCorrelationCondition(store, condition)); - setIsCorrelationEnabled(true); } }; - // Event Handlers - const addStream = (value: string | null) => { - if (value) { - setAppStore((store) => changeStream(store, value)); - } - }; + // View Flags + const hasContentLoaded = !schemaLoading && !loadingState && !streamsLoading && !multipleSchemasLoading; + const hasNoData = hasContentLoaded && !errorMessage && pageData.length === 0; + const showTable = hasContentLoaded && !hasNoData && !errorMessage; - const handleFieldChange = (fieldValue: string | null, isFirstField: boolean) => { - if (isFirstField) { - setSelect1Value(fieldValue); - } else { - setSelect2Value(fieldValue); - } - }; + const isStreamsLoading = loadingState || schemaLoading || streamsLoading || multipleSchemasLoading; - const clearQuery = () => { - setSelect1Value(null); - setSelect2Value(null); - setCorrelationData((store) => setCorrelationCondition(store, '')); - setCorrelationData((store) => setSelectedFields(store, '', '', true)); - setCorrelationData((store) => setIsCorrelatedFlag(store, false)); - setCorrelationData((store) => setCorrelationId(store, '')); - setCorrelationData((store) => setActiveCorrelation(store, null)); - setIsCorrelationEnabled(false); - setAppStore(syncTimeRange); - }; - const openSaveCorrelationModal = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - setCorrelationData((store) => toggleSaveCorrelationModal(store, true)); - }, []); + useEffect(() => { + if (!showTable) return; - // View Flags - const hasContentLoaded = !schemaLoading && !logsLoading && !streamsLoading && !multipleSchemasLoading; - const hasNoData = hasContentLoaded && !errorMessage && tableOpts.pageData.length === 0; - const showTable = hasContentLoaded && !hasNoData && !errorMessage; + if (targetPage) { + setCorrelationData((store) => setPageAndPageData(store, targetPage)); + if (currentPage === targetPage) { + setCorrelationData((store) => setTargetPage(store, undefined)); + } + } + }, [loadingState, currentPage]); - if (isLoading) return; + if (isLoading || schemaLoad || !Object.keys(fields) || !Object.keys(selectedFields)) return; return ( -
- Streams - setSearchText(e.target.value)} - /> -
- {Object.entries(fields).map(([stream, fieldsIter]: [any, any]) => { - const filteredFields = filterFields(fieldsIter); - const totalStreams = Object.entries(fields).length; - const heightPercentage = totalStreams === 1 ? '50%' : `${100 / totalStreams}%`; - - return ( -
-
- - {stream} - - { - setAppStore((store) => changeStream(store, '')); - setCorrelationData((store) => setIsCorrelatedFlag(store, false)); - setSelect1Value(null); - setSelect2Value(null); - setCorrelationData((store) => deleteStreamData(store, stream)); - }} - /> -
- {logsLoading || schemaLoading || streamsLoading || multipleSchemasLoading ? ( - - {Array.from({ length: 8 }).map((_, index) => ( - - ))} - - ) : filteredFields.length > 0 ? ( -
- {filteredFields.map((field: string) => { - const isSelected = selectedFields[stream]?.includes(field); - const dataType = fieldsIter.fieldTypeMap[field]; - return ( - { - if (isCorrelatedData) { - setIsCorrelationEnabled(true); - setCorrelationData((store) => setIsCorrelatedFlag(store, false)); - } - setCorrelationData((store) => setSelectedFields(store, field, stream)); - }} - /> - ); - })} -
- ) : ( - No fields match your search. - )} -
- ); - })} - {streamNames.length === 0 && ( - <> - {/* First box */} - value && addStream(value)} - data={streamData.filter((stream) => !streamNames.includes(stream.value))} - isFirst={true} - /> - - {/* Second box */} - addStream(value)} - data={streamData.filter((stream) => !streamNames.includes(stream.value))} - isFirst={false} - /> - - )} - {streamNames.length === 1 && ( - <> - {/* Render the single existing field */} - addStream(value)} - data={streamData.filter((stream) => !streamNames.includes(stream.value))} - isFirst={false} - /> - - )} -
-
+ - - -
- 0 ? 'black' : '#CBCBCB', - }}> - Fields - -
0 ? '1px solid #CBCBCB' : '1px solid #e1e5e8', - backgroundColor: streamNames.length > 0 ? 'white' : '#F7F8F9', - }} - className={classes.fieldsPillsWrapper}> - {Object.keys(selectedFields).length < 1 && ( - - Click on fields to correlate - - )} - {Object.entries(selectedFields).map(([streamName, fieldsMap]: [any, any]) => - fieldsMap.map((field: any, index: any) => ( - { - isCorrelatedData && setIsCorrelationEnabled(true); - setCorrelationData((store) => deleteSelectedField(store, field, streamName)); - }} - /> - )), - )} -
-
-
- 0 ? 'black' : '#CBCBCB', - flexShrink: 0, - flexGrow: 0, - }}> - Joins - -
-
- 1 - ? Object.keys(fields[streamNames[1]].fieldTypeMap).filter( - (key) => fields[streamNames[1]].fieldTypeMap[key] !== 'list', - ) - : [] - } - value={select2Value} - onChange={(value) => handleFieldChange(value, false)} - /> -
-
-
-
-
- {/* */} -
- - - - - - - -
-
- - - -
-
-
- - - + {Object.keys(selectedFields).length > 0 && ( <> - - + {viewMode === 'table' ? ( + <> + + + ) : ( + + )} + )} {Object.keys(selectedFields).length === 0 && ( diff --git a/src/pages/Correlation/providers/CorrelationProvider.tsx b/src/pages/Correlation/providers/CorrelationProvider.tsx index ec99c772..a2498ba8 100644 --- a/src/pages/Correlation/providers/CorrelationProvider.tsx +++ b/src/pages/Correlation/providers/CorrelationProvider.tsx @@ -7,6 +7,7 @@ import { QueryType } from '@/pages/Stream/providers/FilterProvider'; import { FilterQueryBuilder } from '@/utils/queryBuilder'; import { formatQuery } from 'react-querybuilder'; import { Correlation } from '@/@types/parseable/api/correlation'; +import { isJqSearch, ViewMode } from '@/pages/Stream/providers/LogsProvider'; export const CORRELATION_LOAD_LIMIT = 1000; @@ -25,6 +26,7 @@ type ReducerOutput = { tableOpts?: any; selectedFields?: any; isCorrelatedData?: boolean; + rawData?: any; }; type CorrelationStore = { @@ -49,6 +51,9 @@ type CorrelationStore = { activeCorrelation: Correlation | null; isSavedCorrelationsModalOpen: boolean; isSaveCorrelationModalOpen: boolean; + viewMode: ViewMode; + filteredData: Log[]; + rawData: Log[]; tableOpts: { disabledColumns: string[]; wrapDisabledColumns: string[]; @@ -59,6 +64,7 @@ type CorrelationStore = { displayedCount: number; currentPage: number; perPage: number; + targetPage: number | undefined; currentOffset: number; headers: string[]; orderedHeaders: string[]; @@ -72,7 +78,7 @@ type CorrelationStore = { }; type CorrelationStoreReducers = { - setStreamData: (store: CorrelationStore, currentStream: string, data: Log[]) => ReducerOutput; + setStreamData: (store: CorrelationStore, currentStream: string, data: Log[], jqFilteredData?: Log[]) => ReducerOutput; deleteStreamData: (store: CorrelationStore, currentStream: string) => ReducerOutput; setSelectedFields: (store: CorrelationStore, field: string, streamName: string, clearAll?: boolean) => ReducerOutput; deleteSelectedField: (store: CorrelationStore, field: string, streamName: string) => ReducerOutput; @@ -90,6 +96,13 @@ type CorrelationStoreReducers = { toggleSaveCorrelationModal: (_store: CorrelationStore, val: boolean) => ReducerOutput; cleanCorrelationStore: (store: CorrelationStore) => ReducerOutput; setSavedCorrelationId: (store: CorrelationStore, id: string) => ReducerOutput; + setTotalCount: (store: CorrelationStore, count: number) => ReducerOutput; + setTargetPage: (store: CorrelationStore, target: number | undefined) => ReducerOutput; + setPerPage: (store: CorrelationStore, perPage: number) => ReducerOutput; + onToggleView: (store: CorrelationStore, viewMode: 'json' | 'table') => ReducerOutput; + applyJqSearch: (store: CorrelationStore, jqFilteredData: any[]) => ReducerOutput; + applyInstantSearch: (store: CorrelationStore) => ReducerOutput; + setInstantSearchValue: (store: CorrelationStore, value: string) => ReducerOutput; }; const initialState: CorrelationStore = { @@ -105,12 +118,16 @@ const initialState: CorrelationStore = { activeCorrelation: null, isSavedCorrelationsModalOpen: false, isSaveCorrelationModalOpen: false, + filteredData: [], + rawData: [], + viewMode: 'json', tableOpts: { disabledColumns: [], wrapDisabledColumns: [], pinnedColumns: [], pageData: [], perPage: 50, + targetPage: undefined, totalCount: 0, displayedCount: 0, totalPages: 0, @@ -247,7 +264,77 @@ const generatePaginatedPageData = ( .filter(Boolean); }; +const setInstantSearchValue = (store: CorrelationStore, value: string) => { + return { tableOpts: { ...store.tableOpts, instantSearchValue: value } }; +}; + +const searchAndSortData = (opts: { searchValue: string }, data: Log[]) => { + const { searchValue } = opts; + const filteredData = _.isEmpty(searchValue) + ? data + : (_.reduce( + data, + (acc: Log[], d: Log) => { + const allValues = _.chain(d) + .entries() + .map(([key, value]) => [key, _.toString(value)]) + .value(); + + const doesMatch = _.some( + allValues, + ([key, value]) => key.includes(searchValue) || value.includes(searchValue), + ); + + return doesMatch ? [...acc, d] : acc; + }, + [], + ) as Log[]); + const sortedData = _.orderBy(filteredData, [defaultSortKey], [defaultSortOrder]); + return sortedData; +}; + // Reducer Functions +const applyJqSearch = (store: CorrelationStore, jqFilteredData: any[]) => { + const { tableOpts } = store; + const currentPage = 1; + const newPageSlice = getPageSlice(currentPage, tableOpts.perPage, jqFilteredData); + + return { + tableOpts: { + ...tableOpts, + filters: {}, + pageData: newPageSlice, + currentPage, + totalPages: getTotalPages(jqFilteredData, tableOpts.perPage), + }, + }; +}; + +const applyInstantSearch = (store: CorrelationStore) => { + const { tableOpts, rawData, selectedFields, streamData } = store; + const { instantSearchValue: searchValue, perPage } = tableOpts; + const filteredData = searchAndSortData({ searchValue }, rawData); + const currentPage = 1; + const newPageSlice = searchValue + ? getPageSlice(currentPage, tableOpts.perPage, filteredData) + : generatePaginatedPageData(store, selectedFields, currentPage, perPage); + + const totalPages = Math.max( + ...Object.values(streamData).map((stream) => + _.isEmpty(stream?.logData) ? 0 : Math.ceil(_.size(stream?.logData) / perPage), + ), + ); + return { + tableOpts: { + ...tableOpts, + filters: {}, + pageData: newPageSlice, + currentPage, + totalPages: searchValue ? getTotalPages(filteredData, tableOpts.perPage) : totalPages, + }, + filteredData: newPageSlice, + }; +}; const cleanCorrelationStore = (store: CorrelationStore) => { return { @@ -260,6 +347,15 @@ const cleanCorrelationStore = (store: CorrelationStore) => { }; }; +const setTotalCount = (store: CorrelationStore, totalCount: number) => { + return { + tableOpts: { + ...store.tableOpts, + totalCount, + }, + }; +}; + const toggleSaveCorrelationModal = (store: CorrelationStore, val: boolean) => { return { ...store, @@ -281,6 +377,15 @@ const setCorrelations = (store: CorrelationStore, correlations: Correlation[]) = }; }; +const setPerPage = (store: CorrelationStore, perPage: number) => { + return { + tableOpts: { + ...store.tableOpts, + perPage, + }, + }; +}; + const setActiveCorrelation = (store: CorrelationStore, correlation: Correlation | null) => { return { ...store, @@ -323,14 +428,17 @@ const setSelectedFields = ( return { ...store, selectedFields: updatedSelectedFields, + rawData: updatedPageData, tableOpts: { ...store.tableOpts, pageData: updatedPageData || [], currentPage, - totalPages: getTotalPages( - filterAndSortData(store.tableOpts, store.streamData[streamName]?.logData || []), - store.tableOpts.perPage, - ), + totalPages: store.isCorrelatedData + ? store.tableOpts.totalPages + : getTotalPages( + filterAndSortData(store.tableOpts, store.streamData[streamName]?.logData || []), + store.tableOpts.perPage, + ), }, }; }; @@ -416,15 +524,27 @@ const deleteSelectedField = (store: CorrelationStore, field: string, streamName: }; }; -const setStreamData = (store: CorrelationStore, currentStream: string, data: Log[]): ReducerOutput => { +const setStreamData = ( + store: CorrelationStore, + currentStream: string, + data: Log[], + jqFilteredData?: Log[], +): ReducerOutput => { if (!currentStream) return { fields: store.fields }; + const { tableOpts, viewMode } = store; + const isJsonView = viewMode === 'json'; // Update streamData const updatedStreamData = { ...store.streamData, [currentStream]: { logData: data }, }; // Recompute filtered and sliced data for the table - const filteredData = filterAndSortData(store.tableOpts, updatedStreamData[currentStream]?.logData || []); + const filteredData = + isJsonView && !_.isEmpty(tableOpts.instantSearchValue) + ? isJqSearch(tableOpts.instantSearchValue) + ? jqFilteredData || [] + : searchAndSortData({ searchValue: tableOpts.instantSearchValue }, data) + : filterAndSortData(tableOpts, data); const currentPage = 1; if (store.isCorrelatedData) { @@ -455,6 +575,29 @@ const setStreamData = (store: CorrelationStore, currentStream: string, data: Log }; }; +const onToggleView = (store: CorrelationStore, viewMode: 'json' | 'table') => { + const { tableOpts } = store; + // const filteredData = filterAndSortData( + // { sortOrder: defaultSortOrder, sortKey: defaultSortKey, filters: {} }, + // streamData, + // ); + // const currentPage = tableOpts.currentPage; + // const newPageSlice = getPageSlice(currentPage, tableOpts.perPage, filteredData); + + return { + ...store, + tableOpts: { + ...tableOpts, + filters: {}, + // pageData: newPageSlice, + instantSearchValue: '', + // currentPage, + // totalPages: getTotalPages(filteredData, tableOpts.perPage), + }, + viewMode, + }; +}; + const setCorrelationCondition = (store: CorrelationStore, correlationCondition: string) => { return { ...store, @@ -475,6 +618,15 @@ const setCurrentOffset = (store: CorrelationStore, currentOffset: number) => { }; }; +const setTargetPage = (store: CorrelationStore, target: number | undefined) => { + return { + tableOpts: { + ...store.tableOpts, + targetPage: target ? target : undefined, + }, + }; +}; + const setCurrentPage = (store: CorrelationStore, currentPage: number) => { return { tableOpts: { @@ -599,6 +751,13 @@ const correlationStoreReducers: CorrelationStoreReducers = { setActiveCorrelation, cleanCorrelationStore, setSavedCorrelationId, + setTotalCount, + setTargetPage, + setPerPage, + onToggleView, + applyJqSearch, + applyInstantSearch, + setInstantSearchValue, }; export { CorrelationProvider, useCorrelationStore, correlationStoreReducers }; diff --git a/src/pages/Correlation/styles/Correlation.module.css b/src/pages/Correlation/styles/Correlation.module.css index b09b9b0a..83af18cc 100644 --- a/src/pages/Correlation/styles/Correlation.module.css +++ b/src/pages/Correlation/styles/Correlation.module.css @@ -157,12 +157,13 @@ .fieldsPillsWrapper { width: 100%; - height: 100%; + height: auto; border-radius: rem(8px); display: flex; gap: 10px; - padding: 5px 8px; + padding: 5px; align-items: center; + flex-wrap: wrap; } .joinsWrapper { @@ -181,6 +182,8 @@ padding: 5px; height: 100%; margin-left: 44px; + width: 100%; + justify-content: space-between; } .clearBtn { @@ -237,3 +240,33 @@ font-size: 0.65rem; cursor: pointer; } + +.savedFiltersBtn { + background-color: white; + color: var(--mantine-color-gray-7); + border: 1px #e9ecef solid; + border-radius: rem(8px); + font-size: 0.65rem; + &:hover { + color: black; + } +} + +.savedFiltersBtn:hover { + background-color: #e0e0e0; +} + +.childCombinatorPill { + color: white; + background: var(--mantine-color-indigo-4); + text-transform: uppercase; + font-weight: 700; +} + +.saveCorrelationIcon { + background-color: white; + border: 1px solid #e0e0e0; + &:hover { + background-color: #e0e0e0; + } +} diff --git a/src/pages/Stream/components/PrimaryToolbar.tsx b/src/pages/Stream/components/PrimaryToolbar.tsx index 5070d2d0..b19c5013 100644 --- a/src/pages/Stream/components/PrimaryToolbar.tsx +++ b/src/pages/Stream/components/PrimaryToolbar.tsx @@ -5,9 +5,8 @@ import { appStoreReducers, useAppStore } from '@/layouts/MainLayout/providers/Ap import { filterStoreReducers, useFilterStore } from '../providers/FilterProvider'; import { logsStoreReducers, useLogsStore } from '../providers/LogsProvider'; import { useCallback, useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; -import { CorrelationIcon } from '@/components/Navbar/components/CorrelationIcon'; import IconButton from '@/components/Button/IconButton'; import Querier from './Querier'; import RefreshInterval from '@/components/Header/RefreshInterval'; @@ -45,20 +44,6 @@ const SavedFiltersButton = () => { ); }; -const AddCorrelationButton = () => { - const navigate = useNavigate(); - - return ( - - ); -}; - const DeleteStreamButton = () => { const [, setLogsStore] = useLogsStore(() => null); const onClick = useCallback(() => setLogsStore(toggleDeleteModal), []); @@ -117,7 +102,6 @@ const PrimaryToolbar = () => { {view === 'explore' ? ( - diff --git a/src/pages/Stream/styles/JSONView.module.css b/src/pages/Stream/styles/JSONView.module.css index 78a74a51..50a8590c 100644 --- a/src/pages/Stream/styles/JSONView.module.css +++ b/src/pages/Stream/styles/JSONView.module.css @@ -12,6 +12,7 @@ display: flex; flex-direction: column; overflow: hidden; + width: 100%; } .actionIconContainer { diff --git a/src/utils/queryBuilder.ts b/src/utils/queryBuilder.ts index fbfef159..1c1685ae 100644 --- a/src/utils/queryBuilder.ts +++ b/src/utils/queryBuilder.ts @@ -25,6 +25,7 @@ type CorrelationQueryBuilderType = { selectedFields?: string[]; startTime: Date; endTime: Date; + pageOffset: number; }; //! RESOURCE PATH CONSTANTS @@ -106,6 +107,7 @@ export class CorrelationQueryBuilder { selectedFields?: string[]; startTime: Date; endTime: Date; + pageOffset: number; constructor({ streamNames, @@ -114,6 +116,7 @@ export class CorrelationQueryBuilder { selectedFields, startTime, endTime, + pageOffset, }: CorrelationQueryBuilderType) { this.streamNames = streamNames; this.startTime = startTime; @@ -121,6 +124,7 @@ export class CorrelationQueryBuilder { this.limit = limit; this.correlationCondition = correlationCondition; this.selectedFields = selectedFields; + this.pageOffset = pageOffset; } getCorrelationQuery() { @@ -134,7 +138,7 @@ export class CorrelationQueryBuilder { }) .join(', ')} from \"${this.streamNames[0]}\" join \"${this.streamNames[1]}\" on ${ this.correlationCondition - } offset 0 LIMIT ${this.limit}`; + } OFFSET ${this.pageOffset} LIMIT ${this.limit}`; return { startTime: this.startTime, endTime: this.endTime, @@ -142,9 +146,18 @@ export class CorrelationQueryBuilder { }; } + getCountQuery() { + const baseQuery = this.getCorrelationQuery()?.query; + if (!baseQuery) { + throw new Error('Base query is undefined. Unable to generate count query.'); + } + + const queryWithoutLimit = baseQuery.replace(/LIMIT \d+/i, '').trim(); + return `WITH user_query_count as ( ${queryWithoutLimit} ) SELECT count(*) as count FROM user_query_count`; + } + getParseableQuery() { - /* eslint-disable no-useless-escape */ - const query = `SELECT * FROM \"${this.streamNames[0]}\" LIMIT ${this.limit}`; + const query = `SELECT * FROM \"${this.streamNames[0]}\" OFFSET ${this.pageOffset} LIMIT ${this.limit}`; return { startTime: this.startTime, endTime: this.endTime,