diff --git a/src/@types/parseable/api/correlation.ts b/src/@types/parseable/api/correlation.ts new file mode 100644 index 00000000..d55877c0 --- /dev/null +++ b/src/@types/parseable/api/correlation.ts @@ -0,0 +1,18 @@ +export type Correlation = { + version: string; + id: string; + title: string; + tableConfigs: Array<{ + selectedFields: string[]; + tableName: string; + }>; + joinConfig: { + joinConditions: Array<{ + tableName: string; + field: string; + }>; + }; + filter: null; + startTime: string; + endTime: string; +}; diff --git a/src/api/constants.ts b/src/api/constants.ts index 3b2a5d65..ecad2320 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -26,6 +26,10 @@ export const LOG_QUERY_URL = (params?: Params, resourcePath = 'query') => export const LOG_STREAMS_ALERTS_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/alert`; export const LIST_SAVED_FILTERS_URL = `${API_V1}/filters`; export const LIST_DASHBOARDS = `${API_V1}/dashboards`; +export const LIST_CORRELATIONS = `${API_V1}/correlation`; +export const UPDATE_CORRELATION_URL = (correlationId: string) => `${API_V1}/correlation/${correlationId}`; +export const DELETE_SAVED_CORRELATION_URL = (correlationId: string) => `${API_V1}/correlation/${correlationId}`; +export const GET_SAVED_CORRELATION_URL = (correlationId: string) => `${API_V1}/correlation/${correlationId}`; export const UPDATE_SAVED_FILTERS_URL = (filterId: string) => `${API_V1}/filters/${filterId}`; export const UPDATE_DASHBOARDS_URL = (dashboardId: string) => `${API_V1}/dashboards/${dashboardId}`; export const DELETE_DASHBOARDS_URL = (dashboardId: string) => `${API_V1}/dashboards/${dashboardId}`; diff --git a/src/api/correlations.ts b/src/api/correlations.ts new file mode 100644 index 00000000..bcc58725 --- /dev/null +++ b/src/api/correlations.ts @@ -0,0 +1,28 @@ +import { Correlation } from '@/@types/parseable/api/correlation'; +import { Axios } from './axios'; +import { + DELETE_SAVED_CORRELATION_URL, + GET_SAVED_CORRELATION_URL, + LIST_CORRELATIONS, + UPDATE_CORRELATION_URL, +} from './constants'; + +export const getCorrelations = () => { + return Axios().get(LIST_CORRELATIONS); +}; + +export const getCorrelationById = (correlationId: string) => { + return Axios().get(GET_SAVED_CORRELATION_URL(correlationId)); +}; + +export const deleteSavedCorrelation = (correlationId: string) => { + return Axios().delete(DELETE_SAVED_CORRELATION_URL(correlationId)); +}; + +export const saveCorrelation = (correlationData: Correlation) => { + return Axios().post(LIST_CORRELATIONS, correlationData); +}; + +export const updateCorrelation = (correlationData: Correlation) => { + return Axios().put(UPDATE_CORRELATION_URL(correlationData.id), correlationData); +}; diff --git a/src/hooks/useCorrelationQueryLogs.tsx b/src/hooks/useCorrelationQueryLogs.tsx index 8615e10a..f8b55d7d 100644 --- a/src/hooks/useCorrelationQueryLogs.tsx +++ b/src/hooks/useCorrelationQueryLogs.tsx @@ -1,5 +1,4 @@ import { getCorrelationQueryLogsWithHeaders } from '@/api/query'; -import { StatusCodes } from 'http-status-codes'; import useMountedState from './useMountedState'; import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; import _ from 'lodash'; @@ -55,9 +54,9 @@ export const useCorrelationQueryLogs = () => { enabled: false, refetchOnWindowFocus: false, onSuccess: async (responses) => { - responses.map((data: { data: LogsResponseWithHeaders; status: StatusCodes }) => { + responses.map((data: { data: LogsResponseWithHeaders }) => { const logs = data.data; - const isInvalidResponse = _.isEmpty(logs) || _.isNil(logs) || data.status !== StatusCodes.OK; + const isInvalidResponse = _.isEmpty(logs) || _.isNil(logs); if (isInvalidResponse) return setError('Failed to query logs'); const { records, fields } = logs; diff --git a/src/hooks/useCorrelations.tsx b/src/hooks/useCorrelations.tsx new file mode 100644 index 00000000..a4ccada2 --- /dev/null +++ b/src/hooks/useCorrelations.tsx @@ -0,0 +1,157 @@ +import { useMutation, useQuery } from 'react-query'; +import _ from 'lodash'; + +import { + deleteSavedCorrelation, + getCorrelationById, + 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, + setActiveCorrelation, + setCorrelationId, + setSavedCorrelationId, + cleanCorrelationStore, + toggleSavedCorrelationsModal, +} = correlationStoreReducers; +const { setTimeRange, syncTimeRange } = appStoreReducers; +export const useCorrelationsQuery = () => { + const [{ correlationId }, setCorrelatedStore] = useCorrelationStore((store) => store); + const [, setAppStore] = useAppStore((store) => store); + const { + isError: fetchCorrelationsError, + isSuccess: fetchCorrelationsSuccess, + isLoading: fetchCorrelationsLoading, + refetch: fetchCorrelations, + } = useQuery(['correlations'], () => getCorrelations(), { + retry: false, + enabled: false, + refetchOnWindowFocus: false, + onSuccess: (data) => { + setCorrelatedStore((store) => setCorrelations(store, data.data || [])); + }, + onError: () => { + setCorrelatedStore((store) => setCorrelations(store, [])); + notifyError({ message: 'Failed to fetch correlations' }); + }, + }); + + 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), + { + onSuccess: (_data, variables) => { + variables.onSuccess && variables.onSuccess(); + if (variables.correlationId === correlationId) { + setCorrelatedStore(cleanCorrelationStore); + setAppStore(syncTimeRange); + } + fetchCorrelations(); + setCorrelatedStore((store) => toggleSavedCorrelationsModal(store, false)); + notifySuccess({ message: 'Deleted Successfully' }); + }, + onError: (data: AxiosError) => { + if (isAxiosError(data) && data.response) { + const error = data.response?.data as string; + typeof error === 'string' && notifyError({ message: error }); + } else if (data.message && typeof data.message === 'string') { + notifyError({ message: data.message }); + } + }, + }, + ); + + const { mutate: saveCorrelationMutation, isLoading: isCorrelationSaving } = useMutation( + (data: { correlationData: any; onSuccess?: () => void }) => saveCorrelation(data.correlationData), + { + onSuccess: (data, variables) => { + variables.onSuccess && variables.onSuccess(); + setCorrelatedStore((store) => setCorrelationId(store, data.data.id)); + setCorrelatedStore((store) => setSavedCorrelationId(store, data.data.id)); + fetchCorrelations(); + notifySuccess({ message: 'Correlation saved successfully' }); + }, + onError: (data: AxiosError) => { + if (isAxiosError(data) && data.response) { + const error = data.response?.data as string; + typeof error === 'string' && notifyError({ message: error }); + } else if (data.message && typeof data.message === 'string') { + notifyError({ message: data.message }); + } + }, + }, + ); + + const { mutate: updateCorrelationMutation, isLoading: isCorrelationUpdating } = useMutation( + (data: { correlationData: any; onSuccess?: () => void }) => updateCorrelation(data.correlationData), + { + onSuccess: (data, variables) => { + variables.onSuccess && variables.onSuccess(); + setCorrelatedStore((store) => setCorrelationId(store, data.data.id)); + setCorrelatedStore((store) => setActiveCorrelation(store, data.data)); + fetchCorrelations(); + notifySuccess({ message: 'Correlation updated successfully' }); + }, + onError: (data: AxiosError) => { + if (isAxiosError(data) && data.response) { + const error = data.response?.data as string; + typeof error === 'string' && notifyError({ message: error }); + } else if (data.message && typeof data.message === 'string') { + notifyError({ message: data.message }); + } + }, + }, + ); + + return { + fetchCorrelationsError, + fetchCorrelationsSuccess, + fetchCorrelationsLoading, + fetchCorrelations, + + deleteSavedCorrelationMutation, + isDeleting, + + fetchCorrelationIdError, + fetchCorrelationIdSuccess, + fetchCorrelationIdLoading, + getCorrelationByIdMutation, + + saveCorrelationMutation, + isCorrelationSaving, + + updateCorrelationMutation, + isCorrelationUpdating, + }; +}; diff --git a/src/hooks/useFetchStreamData.tsx b/src/hooks/useFetchStreamData.tsx index 0d0f3470..e4c3f465 100644 --- a/src/hooks/useFetchStreamData.tsx +++ b/src/hooks/useFetchStreamData.tsx @@ -12,7 +12,6 @@ import { } from '@/pages/Correlation/providers/CorrelationProvider'; import { notifyError } from '@/utils/notification'; import { useQuery } from 'react-query'; -import { LogsResponseWithHeaders } from '@/@types/parseable/api/query'; import { useRef, useEffect } from 'react'; const { setStreamData } = correlationStoreReducers; @@ -23,7 +22,6 @@ export const useFetchStreamData = () => { (store) => store, ); const [streamInfo] = useStreamStore((store) => store.info); - const [currentStream] = useAppStore((store) => store.currentStream); const timePartitionColumn = _.get(streamInfo, 'time_partition', 'p_timestamp'); const [timeRange] = useAppStore((store) => store.timeRange); const [ @@ -66,7 +64,7 @@ export const useFetchStreamData = () => { const fetchPromises = streamsToFetch.map((streamName) => { const queryOpts = { ...defaultQueryOpts, streamNames: [streamName] }; - return getStreamDataWithHeaders(queryOpts); + return getStreamDataWithHeaders(queryOpts).then((data) => ({ streamName, data })); }); return Promise.all(fetchPromises); }, @@ -74,18 +72,21 @@ export const useFetchStreamData = () => { enabled: false, refetchOnWindowFocus: false, onSuccess: async (responses) => { - responses.map((data: { data: LogsResponseWithHeaders; status: StatusCodes }) => { + responses.forEach(({ streamName, data }) => { const logs = data.data; const isInvalidResponse = _.isEmpty(logs) || _.isNil(logs) || data.status !== StatusCodes.OK; - if (isInvalidResponse) return setError('Failed to query logs'); + if (isInvalidResponse) { + setError('Failed to query logs'); + return; + } const { records, fields } = logs; if (fields.length > 0 && !correlationCondition) { - return setCorrelationStore((store) => setStreamData(store, currentStream || '', records)); + setCorrelationStore((store) => setStreamData(store, streamName, records)); } else if (fields.length > 0 && correlationCondition) { - return setCorrelationStore((store) => setStreamData(store, 'correlatedStream', records)); + setCorrelationStore((store) => setStreamData(store, 'correlatedStream', records)); } else { - notifyError({ message: `${currentStream} doesn't have any fields` }); + notifyError({ message: `${streamName} doesn't have any fields` }); } }); }, diff --git a/src/hooks/useGetCorrelationStreamSchema.ts b/src/hooks/useGetCorrelationStreamSchema.ts index f426c948..ef3b6655 100644 --- a/src/hooks/useGetCorrelationStreamSchema.ts +++ b/src/hooks/useGetCorrelationStreamSchema.ts @@ -1,7 +1,7 @@ import { getLogStreamSchema } from '@/api/logStream'; import { AxiosError, isAxiosError } from 'axios'; import _ from 'lodash'; -import { useQuery } from 'react-query'; +import { useQueries, useQuery } from 'react-query'; import { useState } from 'react'; import { correlationStoreReducers, useCorrelationStore } from '@/pages/Correlation/providers/CorrelationProvider'; @@ -43,3 +43,46 @@ export const useGetStreamSchema = (opts: { streamName: string }) => { isRefetching, }; }; + +// Multiple stream schemas hook +export const useGetMultipleStreamSchemas = (streams: string[]) => { + const [, setCorrelationStore] = useCorrelationStore((_store) => null); + const [errors, setErrors] = useState>({}); + + const queries = useQueries( + streams.map((streamName) => ({ + queryKey: ['stream-schema', streamName], + queryFn: () => getLogStreamSchema(streamName), + retry: false, + enabled: streamName !== '' && streamName !== 'correlatedStream', + refetchOnWindowFocus: false, + onSuccess: (data: any) => { + setErrors((prev) => _.omit(prev, streamName)); + setCorrelationStore((store) => setStreamSchema(store, data.data, streamName)); + }, + onError: (error: AxiosError) => { + let errorMessage = 'An unknown error occurred'; + if (isAxiosError(error) && error.response?.data) { + errorMessage = typeof error.response.data === 'string' ? error.response.data : error.message; + } + setErrors((prev) => ({ + ...prev, + [streamName]: errorMessage, + })); + }, + })), + ); + + const isLoading = queries.some((query) => query.isLoading); + const isError = queries.some((query) => query.isError); + const isSuccess = queries.every((query) => query.isSuccess); + const isRefetching = queries.some((query) => query.isRefetching); + + return { + isLoading, + isError, + isSuccess, + isRefetching, + errors, + }; +}; diff --git a/src/pages/Correlation/components/MultiEventTimeLineGraph.tsx b/src/pages/Correlation/components/MultiEventTimeLineGraph.tsx index 4813a9d7..c3b6532c 100644 --- a/src/pages/Correlation/components/MultiEventTimeLineGraph.tsx +++ b/src/pages/Correlation/components/MultiEventTimeLineGraph.tsx @@ -10,6 +10,7 @@ import { LogsResponseWithHeaders } from '@/@types/parseable/api/query'; import _ from 'lodash'; import timeRangeUtils from '@/utils/timeRangeUtils'; import { useCorrelationStore } from '../providers/CorrelationProvider'; +import { createStreamQueries, fetchStreamData } from '../utils'; const { makeTimeRangeLabel } = timeRangeUtils; const { setTimeRange } = appStoreReducers; @@ -53,10 +54,10 @@ const calcAverage = (data: LogsResponseWithHeaders | undefined) => { if (!data || !Array.isArray(data?.records)) return 0; const { fields, records } = data; - if (_.isEmpty(records) || !_.includes(fields, 'log_count')) return 0; + if (_.isEmpty(records) || !_.includes(fields, 'count')) return 0; const total = records.reduce((acc, d) => { - return acc + _.toNumber(d.log_count) || 0; + return acc + _.toNumber(d.count) || 0; }, 0); return parseInt(Math.abs(total / records.length).toFixed(0)); }; @@ -138,73 +139,67 @@ function ChartTooltip({ payload, series }: ChartTooltipProps) { type LogRecord = { counts_timestamp: string; - log_count: number; + count: number; }; // date_bin removes tz info // filling data with empty values where there is no rec -const parseGraphData = ( - dataSets: (LogsResponseWithHeaders | undefined)[], - startTime: Date, - endTime: Date, - interval: number, -) => { +const parseGraphData = (dataSets: (LogsResponseWithHeaders | undefined)[], interval: number) => { if (!dataSets || !Array.isArray(dataSets)) return []; const firstResponse = dataSets[0]?.records || []; const secondResponse = dataSets[1]?.records || []; + const hasSecondDataset = dataSets[1] !== undefined; const compactType = getCompactType(interval); - const ticksCount = interval < 10 * 60 * 1000 ? interval / (60 * 1000) : interval < 60 * 60 * 1000 ? 10 : 60; - const intervalDuration = (endTime.getTime() - startTime.getTime()) / ticksCount; - - const allTimestamps = Array.from( - { length: ticksCount }, - (_, index) => new Date(startTime.getTime() + index * intervalDuration), - ); - - const hasSecondDataset = dataSets[1] !== undefined; const isValidRecord = (record: any): record is LogRecord => { - return typeof record.counts_timestamp === 'string' && typeof record.log_count === 'number'; + return ( + typeof record.start_time === 'string' && + typeof record.end_time === 'string' && + typeof record.count === 'number' && + record.start_time !== null && + record.end_time !== null + ); }; - const secondResponseMap = - secondResponse.length > 0 - ? new Map( - secondResponse - .filter((entry) => isValidRecord(entry)) - .map((entry) => { - const timestamp = entry.counts_timestamp; - if (timestamp != null) { - return [new Date(timestamp).getTime(), entry.log_count]; - } - return null; - }) - .filter((entry): entry is [number, number] => entry !== null), - ) - : new Map(); - - const combinedData = allTimestamps.map((ts) => { - const firstRecord = firstResponse.find((record) => { - if (!isValidRecord(record)) return false; - const recordTimestamp = new Date(record.counts_timestamp).getTime(); - const tsISO = ts.getTime(); - return recordTimestamp === tsISO; - }); - - const secondCount = secondResponseMap?.get(ts.toISOString()) ?? 0; + // Process first dataset + const validFirstRecords = firstResponse + .filter(isValidRecord) + .map((record) => ({ + ...record, + startDate: record.start_time ? new Date(record.start_time) : new Date(), + endDate: record.end_time ? new Date(record.end_time) : new Date(), + })) + .sort((a, b) => a.startDate.getTime() - b.startDate.getTime()); + + // Process second dataset + const validSecondRecords = secondResponse + .filter(isValidRecord) + .map((record) => ({ + ...record, + startDate: record.start_time ? new Date(record.start_time) : new Date(), + endDate: record.end_time ? new Date(record.end_time) : new Date(), + })) + .sort((a, b) => a.startDate.getTime() - b.startDate.getTime()); + + // Create a Map for quick lookup of second dataset + const secondResponseMap = new Map( + validSecondRecords.map((record: any) => [record.startDate.getTime(), record.count]), + ); + // Combine the datasets using the first dataset's timestamps as reference points + const combinedData = validFirstRecords.map((record: any) => { const defaultOpts: Record = { - stream: firstRecord?.log_count || 0, - minute: ts, + stream: record.count, + minute: record.startDate, compactType, - startTime: dayjs(ts), - endTime: dayjs(new Date(ts.getTime() + intervalDuration)), + startTime: dayjs(record.startDate), + endTime: dayjs(record.endDate), }; if (hasSecondDataset) { - defaultOpts.stream1 = secondCount; + defaultOpts.stream1 = secondResponseMap.get(record.startDate.getTime()) ?? 0; } return defaultOpts; @@ -227,7 +222,7 @@ const MultiEventTimeLineGraph = () => { useEffect(() => { setMultipleStreamData((prevData) => { const newData = { ...prevData }; - const streamDataKeys = Object.keys(streamData); + const streamDataKeys = Object.keys(fields); Object.keys(newData).forEach((key) => { if (!streamDataKeys.includes(key)) { delete newData[key]; @@ -235,8 +230,21 @@ const MultiEventTimeLineGraph = () => { }); return newData; }); - }, [streamData]); + }, [fields]); + + // Effect for timeRange changes + useEffect(() => { + if (!fields || Object.keys(fields).length === 0) { + setMultipleStreamData({}); + return; + } + + const streamNames = Object.keys(fields); + const queries = createStreamQueries(streamNames, startTime, endTime, interval); + fetchStreamData(queries, fetchGraphDataMutation, setMultipleStreamData); + }, [timeRange]); + // Effect for fields changes useEffect(() => { if (!fields || Object.keys(fields).length === 0) { setMultipleStreamData({}); @@ -245,38 +253,19 @@ const MultiEventTimeLineGraph = () => { const streamNames = Object.keys(fields); const streamsToFetch = streamNames.filter((streamName) => !Object.keys(streamData).includes(streamName)); - const totalMinutes = interval / (1000 * 60); - const numBins = Math.trunc(totalMinutes < 10 ? totalMinutes : totalMinutes < 60 ? 10 : 60); - const eventTimeLineGraphOpts = streamsToFetch.map((streamKey) => { - const logsQuery = { - stream: streamKey, - startTime: dayjs(startTime).toISOString(), - endTime: dayjs(endTime).add(1, 'minute').toISOString(), - numBins, - }; - return logsQuery; - }); - Promise.all(eventTimeLineGraphOpts.map((queryData: any) => fetchGraphDataMutation.mutateAsync(queryData))) - .then((results) => { - setMultipleStreamData((prevData: any) => { - const newData = { ...prevData }; - results.forEach((result, index) => { - newData[eventTimeLineGraphOpts[index].stream] = result; - }); - return newData; - }); - }) - .catch((error) => { - console.error('Error fetching queries:', error); - }); - }, [fields, timeRange]); + + if (streamsToFetch.length === 0) return; + + const queries = createStreamQueries(streamsToFetch, startTime, endTime, interval); + fetchStreamData(queries, fetchGraphDataMutation, setMultipleStreamData); + }, [fields]); const isLoading = fetchGraphDataMutation.isLoading; const avgEventCount = useMemo(() => calcAverage(fetchGraphDataMutation?.data), [fetchGraphDataMutation?.data]); const graphData = useMemo(() => { if (!streamGraphData || streamGraphData.length === 0 || streamGraphData.length !== Object.keys(fields).length) return []; - return parseGraphData(streamGraphData, startTime, endTime, interval); + return parseGraphData(streamGraphData, interval); }, [streamGraphData]); const hasData = Array.isArray(graphData) && graphData.length !== 0; diff --git a/src/pages/Correlation/components/SaveCorrelationModal.tsx b/src/pages/Correlation/components/SaveCorrelationModal.tsx new file mode 100644 index 00000000..109b9f46 --- /dev/null +++ b/src/pages/Correlation/components/SaveCorrelationModal.tsx @@ -0,0 +1,264 @@ +import { Box, Button, Modal, Select, Stack, Text, TextInput } from '@mantine/core'; +import { ChangeEvent, useCallback, useEffect, useState } from 'react'; +import _ from 'lodash'; +import classes from '../styles/SaveCorrelationModal.module.css'; +import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import timeRangeUtils from '@/utils/timeRangeUtils'; +import { correlationStoreReducers, useCorrelationStore } from '../providers/CorrelationProvider'; +import { Correlation } from '@/@types/parseable/api/correlation'; +import { useCorrelationsQuery } from '@/hooks/useCorrelations'; + +const { defaultTimeRangeOption, makeTimeRangeOptions, getDefaultTimeRangeOption } = timeRangeUtils; + +const { toggleSaveCorrelationModal } = correlationStoreReducers; + +interface FormObjectType extends Omit { + isNew: boolean; + isError: boolean; + correlation_id?: string; + version?: string; + timeRangeOptions: { value: string; label: string; time_filter: null | { from: string; to: string } }[]; + selectedTimeRangeOption: { value: string; label: string; time_filter: null | { from: string; to: string } }; +} + +const SaveCorrelationModal = () => { + const [ + { + isSaveCorrelationModalOpen, + selectedFields, + fields, + correlationCondition, + activeCorrelation, + savedCorrelationId, + correlationId, + }, + setCorrelationData, + ] = useCorrelationStore((store) => store); + + const [formObject, setFormObject] = useState(null); + const [timeRange] = useAppStore((store) => store.timeRange); + const [isDirty, setDirty] = useState(false); + + const { saveCorrelationMutation, updateCorrelationMutation } = useCorrelationsQuery(); + + const streamNames = Object.keys(fields); + + const joins = correlationCondition.split('='); + + useEffect(() => { + const timeRangeOptions = makeTimeRangeOptions({ selected: null, current: timeRange }); + const selectedTimeRangeOption = getDefaultTimeRangeOption(timeRangeOptions); + if (activeCorrelation !== null) { + setFormObject({ + title: activeCorrelation.title, + isNew: false, + isError: false, + timeRangeOptions, + selectedTimeRangeOption, + tableConfigs: [], + joinConfig: { + joinConditions: [], + }, + id: activeCorrelation.id, + filter: null, + startTime: '', + endTime: '', + }); + } else { + setFormObject({ + title: '', + isNew: true, + isError: false, + timeRangeOptions, + selectedTimeRangeOption, + tableConfigs: [], + joinConfig: { + joinConditions: [], + }, + id: '', + filter: null, + startTime: '', + endTime: '', + }); + } + }, [timeRange, activeCorrelation]); + + const closeModal = useCallback(() => { + setCorrelationData((store) => toggleSaveCorrelationModal(store, false)); + }, []); + + const updateCorrelation = useCallback(() => { + updateCorrelationMutation({ + correlationData: { + id: savedCorrelationId, + tableConfigs: [ + { + selectedFields: selectedFields[streamNames[0]] || [], + tableName: streamNames[0], + }, + { + selectedFields: selectedFields[streamNames[1]] || [], + tableName: streamNames[1], + }, + ], + joinConfig: { + joinConditions: [ + { + tableName: streamNames[0], + field: joins[0].split('.')[1].trim(), + }, + { + tableName: streamNames[1], + field: joins[1].split('.')[1].trim(), + }, + ], + }, + filter: null, + startTime: formObject?.selectedTimeRangeOption.time_filter?.from, + endTime: formObject?.selectedTimeRangeOption.time_filter?.to, + title: formObject?.title, + }, + onSuccess: () => { + closeModal(); + }, + }); + }, [formObject]); + + const saveCorrelation = useCallback(() => { + saveCorrelationMutation({ + correlationData: { + tableConfigs: [ + { + selectedFields: selectedFields[streamNames[0]] || [], + tableName: streamNames[0], + }, + { + selectedFields: selectedFields[streamNames[1]] || [], + tableName: streamNames[1], + }, + ], + joinConfig: { + joinConditions: [ + { + tableName: streamNames[0], + field: joins[0].split('.')[1].trim(), + }, + { + tableName: streamNames[1], + field: joins[1].split('.')[1].trim(), + }, + ], + }, + filter: null, + startTime: formObject?.selectedTimeRangeOption.time_filter?.from, + endTime: formObject?.selectedTimeRangeOption.time_filter?.to, + title: formObject?.title, + }, + onSuccess: () => { + closeModal(); + }, + }); + }, [formObject]); + + const onToggleIncludeTimeRange = useCallback( + (value: string | null) => { + setDirty(true); + setFormObject((prev) => { + if (!prev) return null; + + return { + ...prev, + selectedTimeRangeOption: + _.find(prev.timeRangeOptions, (option) => option.value === value) || defaultTimeRangeOption, + }; + }); + }, + [timeRange], + ); + + const onSubmit = useCallback(() => { + if (!formObject) return; + + if (_.isEmpty(formObject?.title)) { + return setFormObject((prev) => { + if (!prev) return null; + + return { + ...prev, + isError: true, + }; + }); + } + + if (!_.isEmpty(formObject.title) && !savedCorrelationId) { + saveCorrelation(); + } else { + updateCorrelation(); + } + }, [formObject]); + + const onNameChange = useCallback((e: ChangeEvent) => { + setDirty(true); + setFormObject((prev) => { + if (!prev) return null; + + return { + ...prev, + title: e.target.value, + isError: _.isEmpty(e.target.value), + }; + }); + }, []); + + return ( + + {formObject?.isNew ? 'Save Correlation' : 'Update Correlation'} + + }> + + + + + + + Time Range +