diff --git a/src/@types/parseable/api/savedFilters.ts b/src/@types/parseable/api/savedFilters.ts new file mode 100644 index 00000000..d45dd3c8 --- /dev/null +++ b/src/@types/parseable/api/savedFilters.ts @@ -0,0 +1,32 @@ +import { QueryType } from "@/pages/Stream/providers/FilterProvider"; + +export type SavedFilterType = { + version: string; + stream_name: string; + filter_name: string, + filter_id: string, + user_id: string; + query: { + filter_type: 'sql' | 'builder'; + filter_query?: string; + filter_builder?: QueryType + } + time_filter: null | { + from: string, + to: string + } +} + +export type CreateSavedFilterType = { + stream_name: string; + filter_name: string; + query: { + filter_type: 'sql' | 'builder'; + filter_query?: string; + filter_builder?: QueryType + } + time_filter: null | { + from: string, + to: string + } +} diff --git a/src/api/constants.ts b/src/api/constants.ts index 216934d4..00327e63 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -5,6 +5,10 @@ export const LOG_STREAM_LIST_URL = `${API_V1}/logstream`; export const LOG_STREAMS_SCHEMA_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/schema`; export const LOG_QUERY_URL = `${API_V1}/query`; export const LOG_STREAMS_ALERTS_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/alert`; +export const LIST_SAVED_FILTERS_URL = (userId: string) => `${API_V1}/filters/${userId}`; +export const UPDATE_SAVED_FILTERS_URL = (filterId: string) => `${API_V1}/filters/filter/${filterId}`; +export const CREATE_SAVED_FILTERS_URL = `${API_V1}/filters`; +export const DELETE_SAVED_FILTERS_URL = (filterId: string) => `${API_V1}/filters/filter/${filterId}`; export const LOG_STREAMS_RETRNTION_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/retention`; export const LOG_STREAMS_STATS_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/stats`; export const LOG_STREAMS_INFO_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/info`; diff --git a/src/api/logStream.ts b/src/api/logStream.ts index e6fa6616..9c1983ec 100644 --- a/src/api/logStream.ts +++ b/src/api/logStream.ts @@ -1,3 +1,4 @@ +import { CreateSavedFilterType, SavedFilterType } from '@/@types/parseable/api/savedFilters'; import { Axios } from './axios'; import { DELETE_STREAMS_URL, @@ -7,7 +8,11 @@ import { LOG_STREAMS_RETRNTION_URL, LOG_STREAMS_STATS_URL, CREATE_STREAM_URL, - LOG_STREAMS_INFO_URL + LOG_STREAMS_INFO_URL, + LIST_SAVED_FILTERS_URL, + UPDATE_SAVED_FILTERS_URL, + DELETE_SAVED_FILTERS_URL, + CREATE_SAVED_FILTERS_URL } from './constants'; import { LogStreamData, LogStreamSchemaData } from '@/@types/parseable/api/stream'; @@ -27,6 +32,22 @@ export const putLogStreamAlerts = (streamName: string, data: any) => { return Axios().put(LOG_STREAMS_ALERTS_URL(streamName), data); }; +export const getSavedFilters = (userId: string) => { + return Axios().get(LIST_SAVED_FILTERS_URL(userId)); +}; + +export const putSavedFilters = (filterId: string, filter: SavedFilterType) => { + return Axios().put(UPDATE_SAVED_FILTERS_URL(filterId), filter); +}; + +export const postSavedFilters = (filter: CreateSavedFilterType) => { + return Axios().post(CREATE_SAVED_FILTERS_URL, filter); +}; + +export const deleteSavedFilter = (filterId: string) => { + return Axios().delete(DELETE_SAVED_FILTERS_URL(filterId)); +}; + export const getLogStreamRetention = (streamName: string) => { return Axios().get(LOG_STREAMS_RETRNTION_URL(streamName)); }; diff --git a/src/components/Header/styles/LogQuery.module.css b/src/components/Header/styles/LogQuery.module.css index d3d564f9..8d48063d 100644 --- a/src/components/Header/styles/LogQuery.module.css +++ b/src/components/Header/styles/LogQuery.module.css @@ -270,20 +270,7 @@ white-space: nowrap; } -.filterContainer { - background-color: white; - /* padding: 4px 12px; */ - color: #211F1F; - border: 1px var(--mantine-color-gray-4) solid; - border-radius: rem(8px); - cursor: pointer; - /* height: 2.2rem; */ - overflow: auto; - flex: 1; - margin-right: 0.625rem; - flex-direction: row; - /* height: fit-content; */ -} + .modeContainer { width: 5rem; diff --git a/src/hooks/useSavedFilters.tsx b/src/hooks/useSavedFilters.tsx new file mode 100644 index 00000000..1c58d7e4 --- /dev/null +++ b/src/hooks/useSavedFilters.tsx @@ -0,0 +1,103 @@ +import { useMutation, useQuery } from 'react-query'; +import { getSavedFilters, deleteSavedFilter, putSavedFilters, postSavedFilters } from '@/api/logStream'; +import { notifyError, notifySuccess } from '@/utils/notification'; +import { AxiosError, isAxiosError } from 'axios'; +import { useAppStore, appStoreReducers } from '@/layouts/MainLayout/providers/AppProvider'; +import Cookies from 'js-cookie'; +import _ from 'lodash'; +import { CreateSavedFilterType, SavedFilterType } from '@/@types/parseable/api/savedFilters'; +import { useLogsStore, logsStoreReducers } from '@/pages/Stream/providers/LogsProvider'; +const { setSavedFilters } = appStoreReducers; +const { updateSavedFilterId } = logsStoreReducers; + +const useSavedFiltersQuery = () => { + const [, setAppStore] = useAppStore((_store) => null); + const [, setLogsStore] = useLogsStore((_store) => null); + const username = Cookies.get('username'); + const { isError, isSuccess, isLoading, refetch } = useQuery( + ['saved-filters'], + () => getSavedFilters(username || ''), + { + retry: false, + enabled: false, // not on mount + refetchOnWindowFocus: false, + onSuccess: (data) => { + setAppStore((store) => setSavedFilters(store, data)); + }, + }, + ); + + const { mutate: updateSavedFilters } = useMutation( + (data: { filter: SavedFilterType; onSuccess?: () => void }) => + putSavedFilters(data.filter.filter_id, data.filter), + { + onSuccess: (_data, variables) => { + variables.onSuccess && variables.onSuccess(); + refetch(); + notifySuccess({ message: '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 }); + } + }, + }, + ); + + const { mutate: createSavedFilters } = useMutation( + (data: { filter: CreateSavedFilterType; onSuccess?: () => void }) => + postSavedFilters(data.filter), + { + onSuccess: (data, variables) => { + variables.onSuccess && variables.onSuccess(); + setLogsStore((store) => updateSavedFilterId(store, data.data.filter_id)); + refetch(); + notifySuccess({ message: '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 }); + } + }, + }, + ); + + const { mutate: deleteSavedFilterMutation } = useMutation( + (data: { filter_id: string; onSuccess?: () => void }) => + deleteSavedFilter(data.filter_id), + { + onSuccess: (_data, variables) => { + variables.onSuccess && variables.onSuccess(); + setLogsStore((store) => updateSavedFilterId(store, null)); + refetch(); + notifySuccess({ message: '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 { + isError, + isSuccess, + isLoading, + refetch, + updateSavedFilters, + deleteSavedFilterMutation, + createSavedFilters + }; +}; + +export default useSavedFiltersQuery; diff --git a/src/layouts/MainLayout/providers/AppProvider.tsx b/src/layouts/MainLayout/providers/AppProvider.tsx index 1fb9727e..5dcc8019 100644 --- a/src/layouts/MainLayout/providers/AppProvider.tsx +++ b/src/layouts/MainLayout/providers/AppProvider.tsx @@ -3,6 +3,8 @@ import { getStreamsSepcificAccess } from '@/components/Navbar/rolesHandler'; import initContext from '@/utils/initContext'; import { AboutData } from '@/@types/parseable/api/about'; import _ from 'lodash'; +import { AxiosResponse } from 'axios'; +import { SavedFilterType } from '@/@types/parseable/api/savedFilters'; export type UserRoles = { [roleName: string]: { @@ -26,7 +28,9 @@ type AppStore = { userAccessMap: { [key: string]: boolean }; streamSpecificUserAccess: string[] | null; instanceConfig: AboutData | null; - isStandAloneMode: boolean | null; + isStandAloneMode: boolean | null; + savedFilters: SavedFilterType[] | null; // null to verify whether filters have been fetched or not + activeSavedFilters: SavedFilterType[]; // stream specific }; type AppStoreReducers = { @@ -39,6 +43,7 @@ type AppStoreReducers = { setStreamSpecificUserAccess: (store: AppStore) => ReducerOutput; setInstanceConfig: (store: AppStore, instanceConfig: AboutData) => ReducerOutput; toggleCreateStreamModal: (store: AppStore, val?: boolean) => ReducerOutput; + setSavedFilters: (store: AppStore, savedFilters: AxiosResponse) => ReducerOutput; }; const initialState: AppStore = { @@ -51,7 +56,9 @@ const initialState: AppStore = { streamSpecificUserAccess: null, instanceConfig: null, createStreamModalOpen: false, - isStandAloneMode: null + isStandAloneMode: null, + savedFilters: null, + activeSavedFilters: [], }; const { Provider: AppProvider, useStore: useAppStore } = initContext(initialState); @@ -89,8 +96,9 @@ const toggleCreateStreamModal = (store: AppStore, val?: boolean) => { return { createStreamModalOpen: _.isBoolean(val) ? val : !store.createStreamModalOpen }; }; -const changeStream = (_store: AppStore, stream: string) => { - return { currentStream: stream }; +const changeStream = (store: AppStore, stream: string) => { + const activeSavedFilters = _.filter(store.savedFilters, (filter) => filter.stream_name === stream); + return { currentStream: stream, activeSavedFilters }; }; const setUserRoles = (_store: AppStore, roles: UserRoles | null) => { @@ -118,6 +126,13 @@ const setInstanceConfig = (_store: AppStore, instanceConfig: AboutData | null) = return { instanceConfig, isStandAloneMode: mode === 'Standalone' }; }; +const setSavedFilters = (store: AppStore, savedFiltersResponse: AxiosResponse) => { + const { currentStream } = store; + const savedFilters = savedFiltersResponse.data; + const activeSavedFilters = _.filter(savedFilters, (filter) => filter.stream_name === currentStream); + return { savedFilters, activeSavedFilters }; +}; + const appStoreReducers: AppStoreReducers = { toggleMaximize, toggleHelpModal, @@ -128,6 +143,7 @@ const appStoreReducers: AppStoreReducers = { setStreamSpecificUserAccess, setInstanceConfig, toggleCreateStreamModal, + setSavedFilters, }; export { AppProvider, useAppStore, appStoreReducers }; diff --git a/src/pages/Stream/components/EventTimeLineGraph.tsx b/src/pages/Stream/components/EventTimeLineGraph.tsx index 0a81f6a0..c4635616 100644 --- a/src/pages/Stream/components/EventTimeLineGraph.tsx +++ b/src/pages/Stream/components/EventTimeLineGraph.tsx @@ -14,6 +14,15 @@ const { parseQuery } = filterStoreReducers; type CompactInterval = 'minute' | 'day' | 'hour' | 'quarter-hour' | 'half-hour' | 'month'; +function extractWhereClause(sql: string) { + const whereClauseRegex = /WHERE\s+(.*?)(?=\s*(ORDER\s+BY|GROUP\s+BY|LIMIT|$))/i; + const match = sql.match(whereClauseRegex); + if (match) { + return match[1].trim(); + } + return '(1 = 1)'; +} + const getCompactType = (interval: number): CompactInterval => { const totalMinutes = interval / (1000 * 60); if (totalMinutes <= 60) { @@ -31,7 +40,7 @@ const getCompactType = (interval: number): CompactInterval => { } else if (totalMinutes <= 259200) { return 'day'; } else { - return 'month' + return 'month'; } }; @@ -123,7 +132,7 @@ const compactTypeIntervalMap = { day: '24 hour', 'quarter-hour': '15 minute', 'half-hour': '30 minute', - month: '1 month' + month: '1 month', }; const generateCountQuery = ( @@ -233,6 +242,7 @@ const EventTimeLineGraph = () => { const { fetchQueryMutation } = useQueryResult(); const [currentStream] = useAppStore((store) => store.currentStream); const [appliedQuery] = useFilterStore((store) => store.appliedQuery); + const [{ activeMode, custSearchQuery }] = useLogsStore((store) => store.custQuerySearchState); const [{ interval, startTime, endTime }] = useLogsStore((store) => store.timeRange); useEffect(() => { @@ -246,13 +256,14 @@ const EventTimeLineGraph = () => { access: [], }; - const { where: whereClause } = parseQuery(appliedQuery, currentStream); + const whereClause = + activeMode === 'sql' ? extractWhereClause(custSearchQuery) : parseQuery(appliedQuery, currentStream).where; const query = generateCountQuery(currentStream, modifiedStartTime, modifiedEndTime, compactType, whereClause); fetchQueryMutation.mutate({ logsQuery, query, }); - }, [currentStream, startTime.toISOString(), endTime.toISOString(), appliedQuery]); + }, [currentStream, startTime.toISOString(), endTime.toISOString(), custSearchQuery]); const isLoading = fetchQueryMutation.isLoading; const avgEventCount = useMemo(() => calcAverage(fetchQueryMutation?.data), [fetchQueryMutation?.data]); diff --git a/src/pages/Stream/components/PrimaryToolbar.tsx b/src/pages/Stream/components/PrimaryToolbar.tsx index 5dff8b5e..fffe73dc 100644 --- a/src/pages/Stream/components/PrimaryToolbar.tsx +++ b/src/pages/Stream/components/PrimaryToolbar.tsx @@ -1,6 +1,6 @@ import { Stack, px } from '@mantine/core'; import IconButton from '@/components/Button/IconButton'; -import { IconMaximize, IconTrash } from '@tabler/icons-react'; +import { IconListSearch, IconMaximize, IconTrash } from '@tabler/icons-react'; import { STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT, STREAM_PRIMARY_TOOLBAR_HEIGHT } from '@/constants/theme'; import TimeRange from '@/components/Header/TimeRange'; import RefreshInterval from '@/components/Header/RefreshInterval'; @@ -14,10 +14,13 @@ import { useParams } from 'react-router-dom'; import _ from 'lodash'; import StreamingButton from '@/components/Header/StreamingButton'; import { useLogsStore, logsStoreReducers } from '../providers/LogsProvider'; +import { filterStoreReducers, useFilterStore } from '../providers/FilterProvider'; const { toggleDeleteModal } = logsStoreReducers; +const { toggleSavedFiltersModal } = filterStoreReducers; const renderMaximizeIcon = () => ; const renderDeleteIcon = () => ; +const renderSavedFiltersIcon = () => ; const MaximizeButton = () => { const [_appStore, setAppStore] = useAppStore((_store) => null); @@ -25,8 +28,14 @@ const MaximizeButton = () => { return ; }; +const SavedFiltersButton = () => { + const [_store, setLogsStore] = useFilterStore((_store) => null); + const onClick = useCallback(() => setLogsStore((store) => toggleSavedFiltersModal(store, true)), []); + return ; +}; + const DeleteStreamButton = () => { - const [_appStore, setLogsStore] = useLogsStore((_store) => null); + const [_store, setLogsStore] = useLogsStore((_store) => null); const onClick = useCallback(() => setLogsStore(toggleDeleteModal), []); return ; }; @@ -57,6 +66,7 @@ const PrimaryToolbar = () => { + diff --git a/src/pages/Stream/components/Querier/FilterQueryBuilder.tsx b/src/pages/Stream/components/Querier/FilterQueryBuilder.tsx index 4bf28fe9..82160a78 100644 --- a/src/pages/Stream/components/Querier/FilterQueryBuilder.tsx +++ b/src/pages/Stream/components/Querier/FilterQueryBuilder.tsx @@ -228,7 +228,7 @@ export const QueryPills = () => { const { combinator, rules: ruleSets } = appliedQuery; return ( - + {ruleSets.map((ruleSet, index) => { const shouldShowCombinatorPill = ruleSets.length !== 1 && index + 1 !== ruleSets.length; diff --git a/src/pages/Stream/components/Querier/QueryCodeEditor.tsx b/src/pages/Stream/components/Querier/QueryCodeEditor.tsx index d356dba4..15fab664 100644 --- a/src/pages/Stream/components/Querier/QueryCodeEditor.tsx +++ b/src/pages/Stream/components/Querier/QueryCodeEditor.tsx @@ -41,7 +41,7 @@ const defaultCustSQLQuery = (streamName: string | null) => { const QueryCodeEditor: FC<{ queryCodeEditorRef: MutableRefObject, onSqlSearchApply: (query: string) => void; onClear: () => void; }> = (props) => { const [llmActive] = useAppStore((store) => store.instanceConfig?.llmActive); const [] = useFilterStore((store) => store); - const [{ isQuerySearchActive, activeMode }] = useLogsStore((store) => store.custQuerySearchState); + const [{ isQuerySearchActive, activeMode, savedFilterId, custSearchQuery }] = useLogsStore((store) => store.custQuerySearchState); const [schema] = useStreamStore((store) => store.schema); const fields = schema?.fields || []; const editorRef = React.useRef(); @@ -66,6 +66,12 @@ const QueryCodeEditor: FC<{ queryCodeEditorRef: MutableRefObject, onSqlSear setQuery(query); }, []); + useEffect(() => { + if (savedFilterId && isSqlSearchActive) { + updateQuery(custSearchQuery) + } + }, [savedFilterId]) + const handleAIGenerate = useCallback(() => { if (!aiQuery?.length) { notify({ message: 'Please enter a valid query' }); @@ -103,19 +109,12 @@ const QueryCodeEditor: FC<{ queryCodeEditorRef: MutableRefObject, onSqlSear updateQuery(props.queryCodeEditorRef.current); }, []); - function handleEditorDidMount(editor: any, monaco: any) { - editorRef.current = editor; - monacoRef.current = monaco; - editor.addCommand(monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter, async () => { - runQuery(props.queryCodeEditorRef.current); - }); - } - - const runQuery = (inputQuery: string) => { - const query = sanitiseSqlString(inputQuery); - const parsedQuery = query.replace(/(\r\n|\n|\r)/gm, ''); + const runQuery = useCallback(() => { + const sanitizedQuery = sanitiseSqlString(query); + const parsedQuery = sanitizedQuery.replace(/(\r\n|\n|\r)/gm, ''); + updateQuery(sanitizedQuery); props.onSqlSearchApply(parsedQuery) - }; + }, [query]); return ( @@ -160,7 +159,7 @@ const QueryCodeEditor: FC<{ queryCodeEditorRef: MutableRefObject, onSqlSear mouseWheelZoom: true, padding: { top: 8 }, }} - onMount={handleEditorDidMount} + // onMount={handleEditorDidMount} /> @@ -168,7 +167,7 @@ const QueryCodeEditor: FC<{ queryCodeEditorRef: MutableRefObject, onSqlSear - + ); diff --git a/src/pages/Stream/components/Querier/SaveFilterModal.tsx b/src/pages/Stream/components/Querier/SaveFilterModal.tsx new file mode 100644 index 00000000..f8278bea --- /dev/null +++ b/src/pages/Stream/components/Querier/SaveFilterModal.tsx @@ -0,0 +1,263 @@ +import { Box, Button, Modal, Select, Stack, Text, TextInput } from '@mantine/core'; +import { filterStoreReducers, useFilterStore } from '../../providers/FilterProvider'; +import { ChangeEvent, useCallback, useEffect, useState } from 'react'; +import { makeTimeRangeLabel, useLogsStore } from '../../providers/LogsProvider'; +import { CodeHighlight } from '@mantine/code-highlight'; +import _ from 'lodash'; +import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import { CreateSavedFilterType, SavedFilterType } from '@/@types/parseable/api/savedFilters'; +import useSavedFiltersQuery from '@/hooks/useSavedFilters'; +import Cookies from 'js-cookie'; + +const { toggleSaveFiltersModal } = filterStoreReducers; + +interface FormObjectType extends Omit { + isNew: boolean; + isError: boolean; + filter_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 sanitizeFilterItem = (formObject: FormObjectType): SavedFilterType => { + const { + stream_name, + filter_name, + filter_id = '', + query, + selectedTimeRangeOption, + user_id, + version = '', + } = formObject; + return { + filter_id, + version, + stream_name, + filter_name, + time_filter: selectedTimeRangeOption.time_filter, + query, + user_id, + }; +}; + +const defaultTimeRangeOption = { + value: 'none', + label: 'Time range not included', + time_filter: null, +}; + +const makeTimeRangeOptions = ({ + selected, + current, +}: { + selected: { from: string; to: string } | null; + current: { startTime: Date; endTime: Date }; +}) => { + return [ + defaultTimeRangeOption, + { + value: 'current', + label: `Current - ${makeTimeRangeLabel(current.startTime, current.endTime)}`, + time_filter: { + from: current.startTime.toISOString(), + to: current.endTime.toISOString(), + }, + }, + ...(selected + ? [ + { + value: 'selected', + label: `Stored - ${makeTimeRangeLabel(selected.from, selected.to)}`, + time_filter: { + from: selected.from, + to: selected.to, + }, + }, + ] + : []), + ]; +}; + +const getDefaultTimeRangeOption = ( + opts: { value: string; label: string; time_filter: null | { from: string; to: string } }[], +) => { + const selectedTimeRange = _.find(opts, (option) => option.value === 'selected'); + return selectedTimeRange ? selectedTimeRange : defaultTimeRangeOption; +}; + +const SaveFilterModal = () => { + const username = Cookies.get('username'); + const [isSaveFiltersModalOpen, setFilterStore] = useFilterStore((store) => store.isSaveFiltersModalOpen); + const [appliedQuery] = useFilterStore((store) => store.appliedQuery); + const [activeSavedFilters] = useAppStore((store) => store.activeSavedFilters); + const [formObject, setFormObject] = useState(null); + const [currentStream] = useAppStore((store) => store.currentStream); + const [timeRange] = useLogsStore((store) => store.timeRange); + const [{ custSearchQuery, savedFilterId, activeMode }] = useLogsStore((store) => store.custQuerySearchState); + const [isDirty, setDirty] = useState(false); + + const { updateSavedFilters, createSavedFilters } = useSavedFiltersQuery(); + + useEffect(() => { + const selectedFilter = _.find(activeSavedFilters, (filter) => filter.filter_id === savedFilterId); + if (!currentStream || !activeMode) return; + + if (selectedFilter) { + const { time_filter } = selectedFilter; + const timeRangeOptions = makeTimeRangeOptions({ selected: time_filter, current: timeRange }); + const selectedTimeRangeOption = getDefaultTimeRangeOption(timeRangeOptions); + setFormObject({ + ...selectedFilter, + isNew: false, + isError: false, + timeRangeOptions, + selectedTimeRangeOption, + }); + } else { + const isSqlMode = activeMode === 'sql'; + const timeRangeOptions = makeTimeRangeOptions({ selected: null, current: timeRange }); + const selectedTimeRangeOption = getDefaultTimeRangeOption(timeRangeOptions); + setFormObject({ + stream_name: currentStream, + filter_name: '', + query: { + filter_type: isSqlMode ? 'sql' : 'builder', + ...(isSqlMode ? { filter_query: custSearchQuery } : { filter_builder: appliedQuery }), + }, + time_filter: { + from: timeRange.startTime.toISOString(), + to: timeRange.endTime.toISOString(), + }, + isNew: true, + isError: false, + user_id: username || '', + timeRangeOptions, + selectedTimeRangeOption, + }); + } + }, [custSearchQuery, savedFilterId, activeMode, timeRange, activeSavedFilters]); + + const closeModal = useCallback(() => { + setFilterStore((store) => toggleSaveFiltersModal(store, false)); + }, []); + + 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?.filter_name)) { + return setFormObject((prev) => { + if (!prev) return null; + + return { + ...prev, + isError: true, + }; + }); + } + + if (!_.isEmpty(formObject.filter_id) && !_.isEmpty(formObject.user_id) && !_.isEmpty(formObject.version)) { + updateSavedFilters({ filter: sanitizeFilterItem(formObject), onSuccess: closeModal }); + } else { + const keysToRemove = ['filter_id', 'version']; + const sanitizedFilterItem = sanitizeFilterItem(formObject); + const filteredEntries = Object.entries(sanitizedFilterItem).filter(([key]) => !keysToRemove.includes(key)); + const newObj: CreateSavedFilterType = Object.fromEntries(filteredEntries) as CreateSavedFilterType; + createSavedFilters({ filter: newObj, onSuccess: closeModal }); + } + }, [formObject]); + + const onNameChange = useCallback((e: ChangeEvent) => { + setDirty(true); + setFormObject((prev) => { + if (!prev) return null; + + return { + ...prev, + filter_name: e.target.value, + isError: _.isEmpty(e.target.value), + }; + }); + }, []); + + return ( + + {formObject?.isNew ? 'Save Filters' : 'Update Filters'} + + }> + + + + + + + Fixed Time Range +