From bdf2bbce3c3d0469de209b5d3f616f5665f3d489 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 18 Jun 2024 23:35:04 +0530 Subject: [PATCH 01/12] readme - updated the demo app url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b48d8ef7..1aedc2b5 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ For complete Parseable API documentation, refer to [Parseable API workspace on P - + From cfdae77cc54486b4de2d1b3fd3bf192bc51c2eb7 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Mon, 24 Jun 2024 12:39:53 +0530 Subject: [PATCH 02/12] replace limit in custom query if it exceeds load limit --- src/utils/sanitiseSqlString.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/utils/sanitiseSqlString.ts b/src/utils/sanitiseSqlString.ts index 847a0082..266689f4 100644 --- a/src/utils/sanitiseSqlString.ts +++ b/src/utils/sanitiseSqlString.ts @@ -2,13 +2,23 @@ import { notify } from './notification'; import { LOAD_LIMIT } from '@/pages/Stream/providers/LogsProvider'; export const sanitiseSqlString = (sqlString: string): string => { - const withoutComments = sqlString.replace(/--.*$/gm, ''); - const withoutNewLines = withoutComments.replace(/\n/g, ' '); - const withoutTrailingSemicolon = withoutNewLines.replace(/;/, ''); - const limitRegex = /limit\s+(\d+)/i; - if (!limitRegex.test(withoutTrailingSemicolon)) { - notify({ message: `Default limit used i.e - ${LOAD_LIMIT}` }); - return `${withoutTrailingSemicolon.trim()} LIMIT ${LOAD_LIMIT}`; - } - return withoutTrailingSemicolon; -}; + const withoutComments = sqlString.replace(/--.*$/gm, ''); + const withoutNewLines = withoutComments.replace(/\n/g, ' '); + const withoutTrailingSemicolon = withoutNewLines.replace(/;/g, '').trim(); + const limitRegex = /limit\s+(\d+)/i; + + if (limitRegex.test(withoutTrailingSemicolon)) { + return withoutTrailingSemicolon.replace(limitRegex, (match, p1) => { + const currentLimit = parseInt(p1, 10); + console.log(currentLimit, LOAD_LIMIT) + if (currentLimit > LOAD_LIMIT) { + notify({ message: `Limit exceeds the default load limit. Replacing with default limit - ${LOAD_LIMIT}` }); + return `LIMIT ${LOAD_LIMIT}`; + } + return match; + }); + } + + notify({ message: `Default limit used i.e - ${LOAD_LIMIT}` }); + return `${withoutTrailingSemicolon} LIMIT ${LOAD_LIMIT}`; +}; \ No newline at end of file From 6ec2f4a88eba39eeeb186808f6def84dad069862 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Mon, 24 Jun 2024 12:41:54 +0530 Subject: [PATCH 03/12] fixed context not setting properly when sql search is active --- src/pages/Stream/components/Querier/index.tsx | 9 ++++----- src/utils/sanitiseSqlString.ts | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pages/Stream/components/Querier/index.tsx b/src/pages/Stream/components/Querier/index.tsx index b6dc22df..0d70c826 100644 --- a/src/pages/Stream/components/Querier/index.tsx +++ b/src/pages/Stream/components/Querier/index.tsx @@ -76,9 +76,8 @@ const QuerierModal = (props: { }; const Querier = () => { - const [{ isQuerySearchActive, viewMode, showQueryBuilder, activeMode }, setLogsStore] = useLogsStore( - (store) => store.custQuerySearchState, - ); + const [custQuerySearchState, setLogsStore] = useLogsStore((store) => store.custQuerySearchState); + const { isQuerySearchActive, viewMode, showQueryBuilder, activeMode } = custQuerySearchState; const [currentStream] = useAppStore((store) => store.currentStream); const openBuilderModal = useCallback(() => { setLogsStore((store) => toggleQueryBuilder(store)); @@ -135,7 +134,7 @@ const Querier = () => { } // trigger query fetch if the rules were updated by the remove btn on pills - if (!showQueryBuilder) { + if (!showQueryBuilder && activeMode !== 'sql') { if (!shouldSumbitDisabled) { onFiltersApply(); } @@ -146,7 +145,7 @@ const Querier = () => { } // trigger reset when no active rules are available - if (isQuerySearchActive && allValues.length === 0) { + if (isQuerySearchActive && allValues.length === 0 && activeMode !== 'sql') { onClear(); } }, [query.rules]); diff --git a/src/utils/sanitiseSqlString.ts b/src/utils/sanitiseSqlString.ts index 266689f4..e070b652 100644 --- a/src/utils/sanitiseSqlString.ts +++ b/src/utils/sanitiseSqlString.ts @@ -10,7 +10,6 @@ export const sanitiseSqlString = (sqlString: string): string => { if (limitRegex.test(withoutTrailingSemicolon)) { return withoutTrailingSemicolon.replace(limitRegex, (match, p1) => { const currentLimit = parseInt(p1, 10); - console.log(currentLimit, LOAD_LIMIT) if (currentLimit > LOAD_LIMIT) { notify({ message: `Limit exceeds the default load limit. Replacing with default limit - ${LOAD_LIMIT}` }); return `LIMIT ${LOAD_LIMIT}`; From acb0f0b3bfc37c67b37c022d3cde02ea4fdfd079 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Thu, 27 Jun 2024 14:08:18 +0530 Subject: [PATCH 04/12] feature: Saved Filters --- src/@types/parseable/api/savedFilters.ts | 17 ++ src/api/constants.ts | 2 + src/api/logStream.ts | 17 +- .../Header/styles/LogQuery.module.css | 15 +- src/hooks/useSavedFilters.tsx | 81 +++++++ .../MainLayout/providers/AppProvider.tsx | 19 +- .../Stream/components/PrimaryToolbar.tsx | 14 +- .../components/Querier/FilterQueryBuilder.tsx | 2 +- .../components/Querier/QueryCodeEditor.tsx | 29 ++- .../components/Querier/SaveFilterModal.tsx | 200 +++++++++++++++++ .../components/Querier/SavedFiltersModal.tsx | 210 ++++++++++++++++++ src/pages/Stream/components/Querier/index.tsx | 67 ++++-- .../styles/SavedFiltersModalStyles.module.css | 7 + src/pages/Stream/index.tsx | 1 - src/pages/Stream/providers/FilterProvider.tsx | 34 ++- src/pages/Stream/providers/LogsProvider.tsx | 26 ++- 16 files changed, 685 insertions(+), 56 deletions(-) create mode 100644 src/@types/parseable/api/savedFilters.ts create mode 100644 src/hooks/useSavedFilters.tsx create mode 100644 src/pages/Stream/components/Querier/SaveFilterModal.tsx create mode 100644 src/pages/Stream/components/Querier/SavedFiltersModal.tsx create mode 100644 src/pages/Stream/components/Querier/styles/SavedFiltersModalStyles.module.css diff --git a/src/@types/parseable/api/savedFilters.ts b/src/@types/parseable/api/savedFilters.ts new file mode 100644 index 00000000..57172db0 --- /dev/null +++ b/src/@types/parseable/api/savedFilters.ts @@ -0,0 +1,17 @@ +import { QueryType } from "@/pages/Stream/providers/FilterProvider"; + +export type SavedFilterType = { + version: string; + stream_name: string; + filter_name: string, + filter_id: string, + query: { + type: 'sql' | 'builder'; + filter_query?: string; + filter_builder?: QueryType + } + time_filter: { + from?: string, + to?: string + } +} diff --git a/src/api/constants.ts b/src/api/constants.ts index 216934d4..8f3b9eb0 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -5,6 +5,8 @@ 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 = (userId: string, filterId: string) => `${API_V1}/filters/${userId}/${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..2ef5c8d1 100644 --- a/src/api/logStream.ts +++ b/src/api/logStream.ts @@ -1,3 +1,4 @@ +import { SavedFilterType } from '@/@types/parseable/api/savedFilters'; import { Axios } from './axios'; import { DELETE_STREAMS_URL, @@ -7,7 +8,9 @@ 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 } from './constants'; import { LogStreamData, LogStreamSchemaData } from '@/@types/parseable/api/stream'; @@ -27,6 +30,18 @@ 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 updateSavedFilters = (userId:string, filterId: string, filter: SavedFilterType) => { + return Axios().post(UPDATE_SAVED_FILTERS_URL(userId, filterId), filter); +}; + +export const deleteSavedFilter = (userId:string, filterId: string) => { + return Axios().delete(UPDATE_SAVED_FILTERS_URL(userId, 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..e5270cb7 --- /dev/null +++ b/src/hooks/useSavedFilters.tsx @@ -0,0 +1,81 @@ +import { useMutation, useQuery } from 'react-query'; +import { getSavedFilters, updateSavedFilters, deleteSavedFilter } 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 { 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: mutateSavedFilters } = useMutation( + (data: { filter: SavedFilterType; onSuccess?: () => void }) => + updateSavedFilters(username || '', 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: deleteSavedFilterMutation } = useMutation( + (data: { filter_id: string; onSuccess?: () => void }) => + deleteSavedFilter(username || '', 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, + mutateSavedFilters, + deleteSavedFilterMutation + }; +}; + +export default useSavedFiltersQuery; diff --git a/src/layouts/MainLayout/providers/AppProvider.tsx b/src/layouts/MainLayout/providers/AppProvider.tsx index 1fb9727e..d44fc28b 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); @@ -118,6 +125,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 +142,7 @@ const appStoreReducers: AppStoreReducers = { setStreamSpecificUserAccess, setInstanceConfig, toggleCreateStreamModal, + setSavedFilters, }; export { AppProvider, useAppStore, appStoreReducers }; 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..ab336e7a --- /dev/null +++ b/src/pages/Stream/components/Querier/SaveFilterModal.tsx @@ -0,0 +1,200 @@ +import { Box, Button, Modal, Stack, Switch, 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 { SavedFilterType } from '@/@types/parseable/api/savedFilters'; +import { generateRandomId } from '@/utils'; +import useSavedFiltersQuery from '@/hooks/useSavedFilters'; + +const { toggleSaveFiltersModal } = filterStoreReducers; + +interface FormObjectType extends SavedFilterType { + includeTimeRange: boolean; + isNew: boolean; + isError: boolean; +} + +const sanitizeFilterItem = (formObject: FormObjectType): SavedFilterType => { + const {stream_name, filter_name, filter_id, query, time_filter, includeTimeRange} = formObject; + return { + version: 'v1', + stream_name, + filter_id, + filter_name, + time_filter: includeTimeRange ? time_filter : {}, + query + } +} + +const SaveFilterModal = () => { + const [isSaveFiltersModalOpen, setFilterStore] = useFilterStore((store) => store.isSaveFiltersModalOpen); + 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 {mutateSavedFilters} = useSavedFiltersQuery(); + + useEffect(() => { + const selectedFilter = _.find(activeSavedFilters, (filter) => filter.filter_id === savedFilterId); + if (!currentStream || !activeMode) return; + + if (selectedFilter) { + const { time_filter } = selectedFilter; + setFormObject({ + ...selectedFilter, + time_filter: { + ...time_filter, + from: timeRange.startTime.toISOString(), + to: timeRange.endTime.toISOString(), + }, + includeTimeRange: !_.isEmpty(time_filter), + isNew: false, + isError: false + }); + } else { + setFormObject({ + filter_id: generateRandomId(6), + includeTimeRange: false, + version: 'v1', + stream_name: currentStream, + filter_name: '', + query: { + type: 'sql', + filter_query: custSearchQuery, + }, + time_filter: { + from: timeRange.startTime.toISOString(), + to: timeRange.endTime.toISOString(), + }, + isNew: true, + isError: false + }); + } + }, [custSearchQuery, savedFilterId]); + + const closeModal = useCallback(() => { + setFilterStore((store) => toggleSaveFiltersModal(store, false)); + }, []); + + const onToggleIncludeTimeRange = useCallback(() => { + setDirty(true); + setFormObject((prev) => { + if (!prev) return null; + + return { + ...prev, + includeTimeRange: !prev.includeTimeRange, + }; + }); + }, []); + + const onSubmit = useCallback(() => { + if (!formObject) return; + + if (_.isEmpty(formObject?.filter_name)) { + return setFormObject(prev => { + if (!prev) return null; + + return { + ...prev, + isError: true + }; + }) + } + + mutateSavedFilters({filter: sanitizeFilterItem(formObject), 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'} + + }> + + + + + + + Include Time Range + + {formObject?.includeTimeRange ? makeTimeRangeLabel(timeRange.startTime, timeRange.endTime) : 'Time range is not included'} + + + + + + + + Query + + + + + + + + + + + + + ); +}; + +export default SaveFilterModal; diff --git a/src/pages/Stream/components/Querier/SavedFiltersModal.tsx b/src/pages/Stream/components/Querier/SavedFiltersModal.tsx new file mode 100644 index 00000000..9f15c5ea --- /dev/null +++ b/src/pages/Stream/components/Querier/SavedFiltersModal.tsx @@ -0,0 +1,210 @@ +import { Box, Button, Modal, px, Stack, Text, Loader } from '@mantine/core'; +import { filterStoreReducers, QueryType, useFilterStore } from '../../providers/FilterProvider'; +import { SavedFilterType } from '@/@types/parseable/api/savedFilters'; +import { useCallback, useEffect, useState } from 'react'; +import { useLogsStore, logsStoreReducers } from '../../providers/LogsProvider'; +import { CodeHighlight } from '@mantine/code-highlight'; +import _ from 'lodash'; +import dayjs from 'dayjs'; +import { IconClock, IconEye, IconEyeOff, IconTrash, IconX } from '@tabler/icons-react'; +import IconButton from '@/components/Button/IconButton'; +import classes from './styles/SavedFiltersModalStyles.module.css'; +import { EmptySimple } from '@/components/Empty'; +import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import useSavedFiltersQuery from '@/hooks/useSavedFilters'; + +const { toggleSavedFiltersModal, resetFilters, parseQuery, applySavedFilters } = filterStoreReducers; +const { applyCustomQuery, updateSavedFilterId } = logsStoreReducers; + +const renderDeleteIcon = () => ; +const renderCloseIcon = () => ; +const renderEyeOffIcon = () => ; +const renderEyeIcon = () => ; + +const DeleteButton = (props: { onClick: () => void }) => { + return ; +}; + +const VisiblityToggleButton = (props: { showQuery: boolean; onClick: () => void }) => { + return ( + + ); +}; + +const getTimeRangeLabel = (startTime: string, endTime: string) => { + return `${dayjs(startTime).format('hh:mm A DD MMM YY')} to ${dayjs(endTime).format('hh:mm A DD MMM YY')}`; +}; + +const SavedFilterItem = (props: { + item: SavedFilterType; + onSqlSearchApply: (query: string, id: string) => void; + onFilterBuilderQueryApply: (query: QueryType, id: string) => void; +}) => { + const { + item: { filter_name, time_filter, query, filter_id, stream_name }, + } = props; + const hasTimeRange = _.isString(time_filter.from) && _.isString(time_filter.to); + const [showQuery, setShowQuery] = useState(false); + const [showDeletePropmt, setShowDeletePrompt] = useState(false); + const { deleteSavedFilterMutation } = useSavedFiltersQuery(); + + const toggleShowQuery = useCallback(() => { + return setShowQuery((prev) => !prev); + }, []); + + const handleDelete = useCallback(() => { + if (!showDeletePropmt) { + return setShowDeletePrompt(true); + } + deleteSavedFilterMutation({ filter_id }); + }, [showDeletePropmt]); + + const onApplyFilters = useCallback(() => { + if (_.isString(query.filter_query)) { + props.onSqlSearchApply(query.filter_query, filter_id); + } else if (query.filter_builder) { + props.onFilterBuilderQueryApply(query.filter_builder, filter_id); + } + }, []); + + return ( + + + + {filter_name} + + + + {hasTimeRange && time_filter.from && time_filter.to + ? getTimeRangeLabel(time_filter.from, time_filter.to) + : 'No selected time range'} + + + + + {showDeletePropmt ? ( + + + + + + + + + ) : ( + <> + + + + + + + )} + + + {showQuery && ( + + + + )} + + ); +}; + +const SavedFiltersModal = () => { + const [isSavedFiltersModalOpen, setFilterStore] = useFilterStore((store) => store.isSavedFiltersModalOpen); + const [, setLogsStore] = useLogsStore((_store) => null); + const [savedFilters] = useAppStore((store) => store.savedFilters); + const [activeSavedFilters] = useAppStore((store) => store.activeSavedFilters); + const [currentStream] = useAppStore((store) => store.currentStream); + const { isLoading, refetch, isError } = useSavedFiltersQuery(); + const onSqlSearchApply = useCallback((query: string, id: string) => { + setFilterStore((store) => resetFilters(store)); + setLogsStore((store) => applyCustomQuery(store, query, 'sql', id)); + }, []); + + const onFilterBuilderQueryApply = useCallback( + (query: QueryType, id: string) => { + setFilterStore((store) => resetFilters(store)); + setLogsStore((store) => updateSavedFilterId(store, id)); + setFilterStore((store) => applySavedFilters(store, query)); + }, + [currentStream], + ); + + useEffect(() => { + if (savedFilters === null) { + refetch(); + } + }, [savedFilters]); + + const closeModal = useCallback(() => { + setFilterStore((store) => toggleSavedFiltersModal(store, false)); + }, []); + + const hasNoSavedFilters = _.isEmpty(activeSavedFilters) || _.isNil(activeSavedFilters) || isError; + return ( + Saved Filters}> + + {hasNoSavedFilters ? ( + + {isLoading ? ( + + ) : ( + <> + + + No items to display + + + )} + + ) : ( + <> + {_.map(activeSavedFilters, (filterItem) => { + return ( + + ); + })} + + )} + + + ); +}; + +export default SavedFiltersModal; diff --git a/src/pages/Stream/components/Querier/index.tsx b/src/pages/Stream/components/Querier/index.tsx index 0d70c826..9778c7c4 100644 --- a/src/pages/Stream/components/Querier/index.tsx +++ b/src/pages/Stream/components/Querier/index.tsx @@ -1,5 +1,5 @@ import { Group, Menu, Modal, Stack, px } from '@mantine/core'; -import { IconChevronDown, IconCodeCircle, IconFilter } from '@tabler/icons-react'; +import { IconChevronDown, IconCodeCircle, IconFilter, IconFilterEdit, IconFilterPlus } from '@tabler/icons-react'; import classes from '../../styles/Querier.module.css'; import { Text } from '@mantine/core'; import { FilterQueryBuilder, QueryPills } from './FilterQueryBuilder'; @@ -10,8 +10,11 @@ import { useCallback, useEffect, useRef } from 'react'; import { filterStoreReducers, noValueOperators, useFilterStore } from '../../providers/FilterProvider'; import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; import { useStreamStore } from '../../providers/StreamProvider'; +import SaveFilterModal from './SaveFilterModal'; +import SavedFiltersModal from './SavedFiltersModal'; -const { setFields, parseQuery, storeAppliedQuery, resetFilters, toggleSubmitBtn } = filterStoreReducers; +const { setFields, parseQuery, storeAppliedQuery, resetFilters, toggleSubmitBtn, toggleSaveFiltersModal } = + filterStoreReducers; const { toggleQueryBuilder, toggleCustQuerySearchViewMode, applyCustomQuery, resetCustQuerySearchState } = logsStoreReducers; @@ -77,7 +80,7 @@ const QuerierModal = (props: { const Querier = () => { const [custQuerySearchState, setLogsStore] = useLogsStore((store) => store.custQuerySearchState); - const { isQuerySearchActive, viewMode, showQueryBuilder, activeMode } = custQuerySearchState; + const { isQuerySearchActive, viewMode, showQueryBuilder, activeMode, savedFilterId } = custQuerySearchState; const [currentStream] = useAppStore((store) => store.currentStream); const openBuilderModal = useCallback(() => { setLogsStore((store) => toggleQueryBuilder(store)); @@ -95,17 +98,21 @@ const Querier = () => { return setFilterStore(resetFilters); }, []); - const triggerRefetch = useCallback((query: string, mode: 'filters' | 'sql') => { - setLogsStore((store) => applyCustomQuery(store, query, mode)); + const triggerRefetch = useCallback((query: string, mode: 'filters' | 'sql', id?: string) => { + setLogsStore((store) => applyCustomQuery(store, query, mode, id)); }, []); - const onFiltersApply = useCallback(() => { - if (!currentStream) return; + const onFiltersApply = useCallback( + (opts?: { isUncontrolled?: boolean }) => { + if (!currentStream) return; + const { isUncontrolled } = opts || {}; - const { parsedQuery } = parseQuery(query, currentStream); - setFilterStore((store) => storeAppliedQuery(store)); - triggerRefetch(parsedQuery, 'filters'); - }, [query, currentStream]); + const { parsedQuery } = parseQuery(query, currentStream); + setFilterStore((store) => storeAppliedQuery(store)); + triggerRefetch(parsedQuery, 'filters', isUncontrolled && savedFilterId ? savedFilterId : undefined); + }, + [query, currentStream, savedFilterId], + ); const onSqlSearchApply = useCallback( (query: string) => { @@ -136,7 +143,7 @@ const Querier = () => { // trigger query fetch if the rules were updated by the remove btn on pills if (!showQueryBuilder && activeMode !== 'sql') { if (!shouldSumbitDisabled) { - onFiltersApply(); + onFiltersApply({ isUncontrolled: true }); } if (allValues.length === 0) { @@ -154,9 +161,16 @@ const Querier = () => { setLogsStore((store) => toggleCustQuerySearchViewMode(store, mode)); }, []); + const openSaveFiltersModal = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + setFilterStore((store) => toggleSaveFiltersModal(store, true)); + }, []); + return ( + + { - - {viewMode === 'filters' && (activeMode === 'filters' ? : )} - {viewMode === 'sql' && (activeMode === 'sql' ? : )} + + + {viewMode === 'filters' && (activeMode === 'filters' ? : )} + {viewMode === 'sql' && (activeMode === 'sql' ? : )} + + {isQuerySearchActive && ( + + {custQuerySearchState.savedFilterId ? ( + + ) : ( + + )} + + )} ); }; diff --git a/src/pages/Stream/components/Querier/styles/SavedFiltersModalStyles.module.css b/src/pages/Stream/components/Querier/styles/SavedFiltersModalStyles.module.css new file mode 100644 index 00000000..2b1bbf65 --- /dev/null +++ b/src/pages/Stream/components/Querier/styles/SavedFiltersModalStyles.module.css @@ -0,0 +1,7 @@ +.filterItemContainer { + border-bottom: 1px solid var(--mantine-color-gray-2); + + &:last-of-type { + border-bottom: none; + } +} \ No newline at end of file diff --git a/src/pages/Stream/index.tsx b/src/pages/Stream/index.tsx index a3fc176a..92662398 100644 --- a/src/pages/Stream/index.tsx +++ b/src/pages/Stream/index.tsx @@ -26,7 +26,6 @@ const Logs: FC = () => { const [sideBarOpen, setStreamStore] = useStreamStore((store) => store.sideBarOpen); const { getDataSchema, loading, error } = useGetLogStreamSchema(); - useEffect(() => { if (!_.isEmpty(currentStream)) { setStreamStore(streamChangeCleanup); diff --git a/src/pages/Stream/providers/FilterProvider.tsx b/src/pages/Stream/providers/FilterProvider.tsx index 1cbd928d..e988076d 100644 --- a/src/pages/Stream/providers/FilterProvider.tsx +++ b/src/pages/Stream/providers/FilterProvider.tsx @@ -3,6 +3,7 @@ import { generateRandomId } from '@/utils'; import initContext from '@/utils/initContext'; import { Field, RuleGroupType, RuleType, formatQuery } from 'react-querybuilder'; +// write transformer (for saved filters) if you are updating the operators below export const textFieldOperators = [ { value: '=', label: 'equals to' }, { value: '!=', label: 'not equals to' }, @@ -58,13 +59,14 @@ type UpdateRuleType = { }; type FilterStore = { - isModalOpen: boolean; fields: Field[]; query: QueryType; fieldTypeMap: FieldTypeMap; fieldNames: string[]; isSumbitDisabled: boolean; appliedQuery: QueryType; + isSaveFiltersModalOpen: boolean; + isSavedFiltersModalOpen: boolean; }; type RuleUpdateOpts = { @@ -80,13 +82,14 @@ const defaultQuery = { }; const initialState: FilterStore = { - isModalOpen: false, fields: [], query: defaultQuery, fieldTypeMap: {}, fieldNames: [], isSumbitDisabled: true, appliedQuery: defaultQuery, + isSaveFiltersModalOpen: false, + isSavedFiltersModalOpen: false, }; type ReducerOutput = Partial; @@ -124,6 +127,9 @@ type FilterStoreReducers = { updateRule: (store: FilterStore, groupId: string, ruleId: string, updateOpts: RuleUpdateOpts) => ReducerOutput; parseQuery: (query: QueryType, currentStream: string) => { where: string; parsedQuery: string }; toggleSubmitBtn: (store: FilterStore, val: boolean) => ReducerOutput; + toggleSaveFiltersModal: (_store: FilterStore, val: boolean) => ReducerOutput; + toggleSavedFiltersModal: (_store: FilterStore, val: boolean) => ReducerOutput; + applySavedFilters: (store: FilterStore, query: QueryType) => ReducerOutput; }; const { Provider: FilterProvider, useStore: useFilterStore } = initContext(initialState); @@ -288,6 +294,27 @@ const setFields = (_store: FilterStore, schema: LogStreamSchemaData) => { }; }; +const toggleSaveFiltersModal = (_store: FilterStore, val: boolean) => { + return { + isSaveFiltersModalOpen: val + } +} + +const toggleSavedFiltersModal = (_store: FilterStore, val: boolean) => { + return { + isSavedFiltersModalOpen: val + } +} + +const applySavedFilters = (store: FilterStore, query: QueryType) => { + return { + ...store, + appliedQuery: query, + query, + isSumbitDisabled: true + } +} + const filterStoreReducers: FilterStoreReducers = { storeAppliedQuery, resetFilters, @@ -300,6 +327,9 @@ const filterStoreReducers: FilterStoreReducers = { updateRule, parseQuery, toggleSubmitBtn, + toggleSaveFiltersModal, + toggleSavedFiltersModal, + applySavedFilters }; export { FilterProvider, useFilterStore, filterStoreReducers }; diff --git a/src/pages/Stream/providers/LogsProvider.tsx b/src/pages/Stream/providers/LogsProvider.tsx index 2bd0e446..5e2082ae 100644 --- a/src/pages/Stream/providers/LogsProvider.tsx +++ b/src/pages/Stream/providers/LogsProvider.tsx @@ -129,6 +129,10 @@ const getDefaultTimeRange = (duration: FixedDuration = DEFAULT_FIXED_DURATIONS) }; }; +export const makeTimeRangeLabel = (startTime: Date, endTime: Date) => { + return `${dayjs(startTime).format('hh:mm A DD MMM YY')} to ${dayjs(endTime).format('hh:mm A DD MMM YY')}` +} + const defaultQuickFilters = { search: '', filters: {}, @@ -153,6 +157,7 @@ const defaultCustQuerySearchState = { custSearchQuery: '', viewMode: 'filters', activeMode: null, + savedFilterId: null }; type LogQueryData = { @@ -166,6 +171,7 @@ type CustQuerySearchState = { custSearchQuery: string; viewMode: string; activeMode: null | 'filters' | 'sql'; + savedFilterId: string | null; }; type LogsStore = { @@ -252,13 +258,14 @@ type LogsStoreReducers = { // data reducers setData: (store: LogsStore, data: Log[], schema: LogStreamSchemaData | null) => ReducerOutput; setStreamSchema: (store: LogsStore, schema: LogStreamSchemaData) => ReducerOutput; - applyCustomQuery: (store: LogsStore, query: string, mode: 'filters' | 'sql') => ReducerOutput; + applyCustomQuery: (store: LogsStore, query: string, mode: 'filters' | 'sql', savedFilterId?: string) => ReducerOutput; getUniqueValues: (data: Log[], key: string) => string[]; makeExportData: (data: Log[], headers: string[], type: string) => Log[]; setRetention: (store: LogsStore, retention: { description: string; duration: string }) => ReducerOutput; setTableHeaders: (store: LogsStore, schema: LogStreamSchemaData) => ReducerOutput; setCleanStoreForStreamChange: (store: LogsStore) => ReducerOutput; + updateSavedFilterId: (store: LogsStore, savedFilterId: string | null) => ReducerOutput; }; const initialState: LogsStore = { @@ -630,7 +637,7 @@ const setCleanStoreForStreamChange = (store: LogsStore) => { }; }; -const applyCustomQuery = (store: LogsStore, query: string, mode: 'filters' | 'sql') => { +const applyCustomQuery = (store: LogsStore, query: string, mode: 'filters' | 'sql', savedFilterId?: string) => { const { custQuerySearchState } = store; return { custQuerySearchState: { @@ -639,11 +646,23 @@ const applyCustomQuery = (store: LogsStore, query: string, mode: 'filters' | 'sq isQuerySearchActive: true, custSearchQuery: query, activeMode: mode, + savedFilterId: savedFilterId || null, + viewMode: mode }, ...getCleanStoreForRefetch(store), }; }; +const updateSavedFilterId = (store: LogsStore, savedFilterId: string | null) => { + const { custQuerySearchState } = store; + return { + custQuerySearchState: { + ...custQuerySearchState, + savedFilterId + }, + }; +} + const setAndSortData = (store: LogsStore, sortKey: string, sortOrder: 'asc' | 'desc') => { const { data, tableOpts } = store; const filteredData = filterAndSortData({ sortKey, sortOrder, filters: tableOpts.filters }, data.rawData); @@ -767,7 +786,8 @@ const logsStoreReducers: LogsStoreReducers = { setRetention, setCleanStoreForStreamChange, toggleSideBar, - setTableHeaders + setTableHeaders, + updateSavedFilterId }; export { LogsProvider, useLogsStore, logsStoreReducers }; From b248cb47307aed0e93e16fe0ec83471263f7c42a Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Thu, 27 Jun 2024 15:27:56 +0530 Subject: [PATCH 05/12] added header to saved filters api call --- src/@types/parseable/api/savedFilters.ts | 2 +- src/api/logStream.ts | 12 +++---- src/hooks/useSavedFilters.tsx | 8 ++--- .../components/Querier/SaveFilterModal.tsx | 33 ++++++++++--------- .../components/Querier/SavedFiltersModal.tsx | 7 ++-- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/@types/parseable/api/savedFilters.ts b/src/@types/parseable/api/savedFilters.ts index 57172db0..8224528e 100644 --- a/src/@types/parseable/api/savedFilters.ts +++ b/src/@types/parseable/api/savedFilters.ts @@ -6,7 +6,7 @@ export type SavedFilterType = { filter_name: string, filter_id: string, query: { - type: 'sql' | 'builder'; + filter_type: 'sql' | 'builder'; filter_query?: string; filter_builder?: QueryType } diff --git a/src/api/logStream.ts b/src/api/logStream.ts index 2ef5c8d1..980ac508 100644 --- a/src/api/logStream.ts +++ b/src/api/logStream.ts @@ -30,16 +30,16 @@ 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 getSavedFilters = (userId: string, headers: any) => { + return Axios().get(LIST_SAVED_FILTERS_URL(userId), { headers }); }; -export const updateSavedFilters = (userId:string, filterId: string, filter: SavedFilterType) => { - return Axios().post(UPDATE_SAVED_FILTERS_URL(userId, filterId), filter); +export const updateSavedFilters = (userId: string, filterId: string, filter: SavedFilterType, headers: any) => { + return Axios().post(UPDATE_SAVED_FILTERS_URL(userId, filterId), filter, { headers }); }; -export const deleteSavedFilter = (userId:string, filterId: string) => { - return Axios().delete(UPDATE_SAVED_FILTERS_URL(userId, filterId)); +export const deleteSavedFilter = (userId: string, filterId: string, headers: any) => { + return Axios().delete(UPDATE_SAVED_FILTERS_URL(userId, filterId), { headers }); }; export const getLogStreamRetention = (streamName: string) => { diff --git a/src/hooks/useSavedFilters.tsx b/src/hooks/useSavedFilters.tsx index e5270cb7..6e12ef2b 100644 --- a/src/hooks/useSavedFilters.tsx +++ b/src/hooks/useSavedFilters.tsx @@ -10,13 +10,13 @@ import { useLogsStore, logsStoreReducers } from '@/pages/Stream/providers/LogsPr const { setSavedFilters } = appStoreReducers; const {updateSavedFilterId} = logsStoreReducers; -const useSavedFiltersQuery = () => { +const useSavedFiltersQuery = (streamName: string) => { 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 || ''), + () => getSavedFilters(username || '', { 'x-p-stream': streamName }), { retry: false, enabled: false, // not on mount @@ -29,7 +29,7 @@ const useSavedFiltersQuery = () => { const { mutate: mutateSavedFilters } = useMutation( (data: { filter: SavedFilterType; onSuccess?: () => void }) => - updateSavedFilters(username || '', data.filter.filter_id, data.filter), + updateSavedFilters(username || '', data.filter.filter_id, data.filter, { 'x-p-stream': streamName }), { onSuccess: (_data, variables) => { variables.onSuccess && variables.onSuccess(); @@ -49,7 +49,7 @@ const useSavedFiltersQuery = () => { const { mutate: deleteSavedFilterMutation } = useMutation( (data: { filter_id: string; onSuccess?: () => void }) => - deleteSavedFilter(username || '', data.filter_id), + deleteSavedFilter(username || '', data.filter_id, { 'x-p-stream': streamName }), { onSuccess: (_data, variables) => { variables.onSuccess && variables.onSuccess(); diff --git a/src/pages/Stream/components/Querier/SaveFilterModal.tsx b/src/pages/Stream/components/Querier/SaveFilterModal.tsx index ab336e7a..44be5061 100644 --- a/src/pages/Stream/components/Querier/SaveFilterModal.tsx +++ b/src/pages/Stream/components/Querier/SaveFilterModal.tsx @@ -18,16 +18,16 @@ interface FormObjectType extends SavedFilterType { } const sanitizeFilterItem = (formObject: FormObjectType): SavedFilterType => { - const {stream_name, filter_name, filter_id, query, time_filter, includeTimeRange} = formObject; + const { stream_name, filter_name, filter_id, query, time_filter, includeTimeRange } = formObject; return { version: 'v1', stream_name, filter_id, filter_name, time_filter: includeTimeRange ? time_filter : {}, - query - } -} + query, + }; +}; const SaveFilterModal = () => { const [isSaveFiltersModalOpen, setFilterStore] = useFilterStore((store) => store.isSaveFiltersModalOpen); @@ -38,7 +38,7 @@ const SaveFilterModal = () => { const [{ custSearchQuery, savedFilterId, activeMode }] = useLogsStore((store) => store.custQuerySearchState); const [isDirty, setDirty] = useState(false); - const {mutateSavedFilters} = useSavedFiltersQuery(); + const { mutateSavedFilters } = useSavedFiltersQuery(currentStream || ''); useEffect(() => { const selectedFilter = _.find(activeSavedFilters, (filter) => filter.filter_id === savedFilterId); @@ -55,7 +55,7 @@ const SaveFilterModal = () => { }, includeTimeRange: !_.isEmpty(time_filter), isNew: false, - isError: false + isError: false, }); } else { setFormObject({ @@ -65,7 +65,7 @@ const SaveFilterModal = () => { stream_name: currentStream, filter_name: '', query: { - type: 'sql', + filter_type: 'sql', filter_query: custSearchQuery, }, time_filter: { @@ -73,7 +73,7 @@ const SaveFilterModal = () => { to: timeRange.endTime.toISOString(), }, isNew: true, - isError: false + isError: false, }); } }, [custSearchQuery, savedFilterId]); @@ -98,18 +98,17 @@ const SaveFilterModal = () => { if (!formObject) return; if (_.isEmpty(formObject?.filter_name)) { - return setFormObject(prev => { + return setFormObject((prev) => { if (!prev) return null; return { ...prev, - isError: true + isError: true, }; - }) + }); } - - mutateSavedFilters({filter: sanitizeFilterItem(formObject), onSuccess: closeModal}) - + + mutateSavedFilters({ filter: sanitizeFilterItem(formObject), onSuccess: closeModal }); }, [formObject]); const onNameChange = useCallback((e: ChangeEvent) => { @@ -120,7 +119,7 @@ const SaveFilterModal = () => { return { ...prev, filter_name: e.target.value, - isError: _.isEmpty(e.target.value) + isError: _.isEmpty(e.target.value), }; }); }, []); @@ -151,7 +150,9 @@ const SaveFilterModal = () => { Include Time Range - {formObject?.includeTimeRange ? makeTimeRangeLabel(timeRange.startTime, timeRange.endTime) : 'Time range is not included'} + {formObject?.includeTimeRange + ? makeTimeRangeLabel(timeRange.startTime, timeRange.endTime) + : 'Time range is not included'} diff --git a/src/pages/Stream/components/Querier/SavedFiltersModal.tsx b/src/pages/Stream/components/Querier/SavedFiltersModal.tsx index 9f15c5ea..2a7dccba 100644 --- a/src/pages/Stream/components/Querier/SavedFiltersModal.tsx +++ b/src/pages/Stream/components/Querier/SavedFiltersModal.tsx @@ -39,14 +39,16 @@ const SavedFilterItem = (props: { item: SavedFilterType; onSqlSearchApply: (query: string, id: string) => void; onFilterBuilderQueryApply: (query: QueryType, id: string) => void; + currentStream: string; }) => { const { item: { filter_name, time_filter, query, filter_id, stream_name }, + currentStream, } = props; const hasTimeRange = _.isString(time_filter.from) && _.isString(time_filter.to); const [showQuery, setShowQuery] = useState(false); const [showDeletePropmt, setShowDeletePrompt] = useState(false); - const { deleteSavedFilterMutation } = useSavedFiltersQuery(); + const { deleteSavedFilterMutation } = useSavedFiltersQuery(currentStream); const toggleShowQuery = useCallback(() => { return setShowQuery((prev) => !prev); @@ -140,7 +142,7 @@ const SavedFiltersModal = () => { const [savedFilters] = useAppStore((store) => store.savedFilters); const [activeSavedFilters] = useAppStore((store) => store.activeSavedFilters); const [currentStream] = useAppStore((store) => store.currentStream); - const { isLoading, refetch, isError } = useSavedFiltersQuery(); + const { isLoading, refetch, isError } = useSavedFiltersQuery(currentStream || ''); const onSqlSearchApply = useCallback((query: string, id: string) => { setFilterStore((store) => resetFilters(store)); setLogsStore((store) => applyCustomQuery(store, query, 'sql', id)); @@ -197,6 +199,7 @@ const SavedFiltersModal = () => { key={filterItem.filter_id} onSqlSearchApply={onSqlSearchApply} onFilterBuilderQueryApply={onFilterBuilderQueryApply} + currentStream={currentStream || ''} /> ); })} From 5b706522592173e3ead21a5e70076ef1fb0d3616 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Thu, 27 Jun 2024 16:15:50 +0530 Subject: [PATCH 06/12] apply saved time range --- src/@types/parseable/api/savedFilters.ts | 6 ++-- .../components/Querier/SaveFilterModal.tsx | 2 +- .../components/Querier/SavedFiltersModal.tsx | 12 +++---- src/pages/Stream/components/Querier/index.tsx | 13 +++++-- src/pages/Stream/providers/LogsProvider.tsx | 35 +++++++++++++++++-- 5 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/@types/parseable/api/savedFilters.ts b/src/@types/parseable/api/savedFilters.ts index 8224528e..76e5b443 100644 --- a/src/@types/parseable/api/savedFilters.ts +++ b/src/@types/parseable/api/savedFilters.ts @@ -10,8 +10,8 @@ export type SavedFilterType = { filter_query?: string; filter_builder?: QueryType } - time_filter: { - from?: string, - to?: string + time_filter: null | { + from: string, + to: string } } diff --git a/src/pages/Stream/components/Querier/SaveFilterModal.tsx b/src/pages/Stream/components/Querier/SaveFilterModal.tsx index 44be5061..82666f22 100644 --- a/src/pages/Stream/components/Querier/SaveFilterModal.tsx +++ b/src/pages/Stream/components/Querier/SaveFilterModal.tsx @@ -24,7 +24,7 @@ const sanitizeFilterItem = (formObject: FormObjectType): SavedFilterType => { stream_name, filter_id, filter_name, - time_filter: includeTimeRange ? time_filter : {}, + time_filter: includeTimeRange ? time_filter : null, query, }; }; diff --git a/src/pages/Stream/components/Querier/SavedFiltersModal.tsx b/src/pages/Stream/components/Querier/SavedFiltersModal.tsx index 2a7dccba..d2c60436 100644 --- a/src/pages/Stream/components/Querier/SavedFiltersModal.tsx +++ b/src/pages/Stream/components/Querier/SavedFiltersModal.tsx @@ -37,7 +37,7 @@ const getTimeRangeLabel = (startTime: string, endTime: string) => { const SavedFilterItem = (props: { item: SavedFilterType; - onSqlSearchApply: (query: string, id: string) => void; + onSqlSearchApply: (query: string, id: string, time_filter: null | {from: string, to: string}) => void; onFilterBuilderQueryApply: (query: QueryType, id: string) => void; currentStream: string; }) => { @@ -45,7 +45,6 @@ const SavedFilterItem = (props: { item: { filter_name, time_filter, query, filter_id, stream_name }, currentStream, } = props; - const hasTimeRange = _.isString(time_filter.from) && _.isString(time_filter.to); const [showQuery, setShowQuery] = useState(false); const [showDeletePropmt, setShowDeletePrompt] = useState(false); const { deleteSavedFilterMutation } = useSavedFiltersQuery(currentStream); @@ -63,7 +62,7 @@ const SavedFilterItem = (props: { const onApplyFilters = useCallback(() => { if (_.isString(query.filter_query)) { - props.onSqlSearchApply(query.filter_query, filter_id); + props.onSqlSearchApply(query.filter_query, filter_id, time_filter); } else if (query.filter_builder) { props.onFilterBuilderQueryApply(query.filter_builder, filter_id); } @@ -77,7 +76,7 @@ const SavedFilterItem = (props: { - {hasTimeRange && time_filter.from && time_filter.to + {time_filter && time_filter.from && time_filter.to ? getTimeRangeLabel(time_filter.from, time_filter.to) : 'No selected time range'} @@ -143,9 +142,10 @@ const SavedFiltersModal = () => { const [activeSavedFilters] = useAppStore((store) => store.activeSavedFilters); const [currentStream] = useAppStore((store) => store.currentStream); const { isLoading, refetch, isError } = useSavedFiltersQuery(currentStream || ''); - const onSqlSearchApply = useCallback((query: string, id: string) => { + const onSqlSearchApply = useCallback((query: string, id: string, time_filter: null | {from: string, to: string}) => { setFilterStore((store) => resetFilters(store)); - setLogsStore((store) => applyCustomQuery(store, query, 'sql', id)); + + setLogsStore((store) => applyCustomQuery(store, query, 'sql', id, time_filter)); }, []); const onFilterBuilderQueryApply = useCallback( diff --git a/src/pages/Stream/components/Querier/index.tsx b/src/pages/Stream/components/Querier/index.tsx index 9778c7c4..657e5639 100644 --- a/src/pages/Stream/components/Querier/index.tsx +++ b/src/pages/Stream/components/Querier/index.tsx @@ -12,6 +12,7 @@ import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; import { useStreamStore } from '../../providers/StreamProvider'; import SaveFilterModal from './SaveFilterModal'; import SavedFiltersModal from './SavedFiltersModal'; +import _ from 'lodash'; const { setFields, parseQuery, storeAppliedQuery, resetFilters, toggleSubmitBtn, toggleSaveFiltersModal } = filterStoreReducers; @@ -82,6 +83,7 @@ const Querier = () => { const [custQuerySearchState, setLogsStore] = useLogsStore((store) => store.custQuerySearchState); const { isQuerySearchActive, viewMode, showQueryBuilder, activeMode, savedFilterId } = custQuerySearchState; const [currentStream] = useAppStore((store) => store.currentStream); + const [activeSavedFilters] = useAppStore((store) => store.activeSavedFilters); const openBuilderModal = useCallback(() => { setLogsStore((store) => toggleQueryBuilder(store)); }, []); @@ -99,8 +101,9 @@ const Querier = () => { }, []); const triggerRefetch = useCallback((query: string, mode: 'filters' | 'sql', id?: string) => { - setLogsStore((store) => applyCustomQuery(store, query, mode, id)); - }, []); + const time_filter = id ? _.find(activeSavedFilters, filter => filter.filter_id === id)?.time_filter : null + setLogsStore((store) => applyCustomQuery(store, query, mode, id, time_filter)); + }, [activeSavedFilters]); const onFiltersApply = useCallback( (opts?: { isUncontrolled?: boolean }) => { @@ -131,6 +134,7 @@ const Querier = () => { useEffect(() => { // toggle submit btn - enable / disable + // ----------------------------------- const ruleSets = query.rules.map((r) => r.rules); const allValues = ruleSets.flat().flatMap((rule) => { return noValueOperators.indexOf(rule.operator) !== -1 ? [null] : [rule.value]; @@ -139,8 +143,11 @@ const Querier = () => { if (isSumbitDisabled !== shouldSumbitDisabled) { setFilterStore((store) => toggleSubmitBtn(store, shouldSumbitDisabled)); } + // ----------------------------------- + // trigger query fetch if the rules were updated by the remove btn on pills + // ----------------------------------- if (!showQueryBuilder && activeMode !== 'sql') { if (!shouldSumbitDisabled) { onFiltersApply({ isUncontrolled: true }); @@ -150,6 +157,8 @@ const Querier = () => { onClear(); } } + // ----------------------------------- + // trigger reset when no active rules are available if (isQuerySearchActive && allValues.length === 0 && activeMode !== 'sql') { diff --git a/src/pages/Stream/providers/LogsProvider.tsx b/src/pages/Stream/providers/LogsProvider.tsx index 5e2082ae..0c1cde1f 100644 --- a/src/pages/Stream/providers/LogsProvider.tsx +++ b/src/pages/Stream/providers/LogsProvider.tsx @@ -258,7 +258,7 @@ type LogsStoreReducers = { // data reducers setData: (store: LogsStore, data: Log[], schema: LogStreamSchemaData | null) => ReducerOutput; setStreamSchema: (store: LogsStore, schema: LogStreamSchemaData) => ReducerOutput; - applyCustomQuery: (store: LogsStore, query: string, mode: 'filters' | 'sql', savedFilterId?: string) => ReducerOutput; + applyCustomQuery: (store: LogsStore, query: string, mode: 'filters' | 'sql', savedFilterId?: string, timeRangePayload?: { from: string; to: string; } | null) => ReducerOutput; getUniqueValues: (data: Log[], key: string) => string[]; makeExportData: (data: Log[], headers: string[], type: string) => Log[]; setRetention: (store: LogsStore, retention: { description: string; duration: string }) => ReducerOutput; @@ -637,8 +637,36 @@ const setCleanStoreForStreamChange = (store: LogsStore) => { }; }; -const applyCustomQuery = (store: LogsStore, query: string, mode: 'filters' | 'sql', savedFilterId?: string) => { +const applyCustomQuery = ( + store: LogsStore, + query: string, + mode: 'filters' | 'sql', + savedFilterId?: string, + timeRangePayload?: { from: string; to: string; } | null, +) => { const { custQuerySearchState } = store; + + const timeRange = (() => { + if (!timeRangePayload) { + return {}; + } else { + const startTime = dayjs(timeRangePayload.from); + const endTime = dayjs(timeRangePayload.to); + const label = `${startTime.format('hh:mm A DD MMM YY')} to ${endTime.format('hh:mm A DD MMM YY')}`; + const interval = endTime.diff(startTime, 'milliseconds'); + return { + timeRange: { + ...store.timeRange, + startTime: startTime.toDate(), + endTime: endTime.toDate(), + label, + interval, + type: 'custom' as 'custom', // always + }, + }; + } + })(); + return { custQuerySearchState: { ...custQuerySearchState, @@ -647,9 +675,10 @@ const applyCustomQuery = (store: LogsStore, query: string, mode: 'filters' | 'sq custSearchQuery: query, activeMode: mode, savedFilterId: savedFilterId || null, - viewMode: mode + viewMode: mode, }, ...getCleanStoreForRefetch(store), + ...timeRange }; }; From 96111943cd4779a9d366d9a4a1786fa5f0b2bfc5 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Thu, 27 Jun 2024 16:20:30 +0530 Subject: [PATCH 07/12] fix delete action - saved filters --- src/pages/Stream/components/Querier/SavedFiltersModal.tsx | 2 +- src/pages/Stream/components/Querier/index.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/Stream/components/Querier/SavedFiltersModal.tsx b/src/pages/Stream/components/Querier/SavedFiltersModal.tsx index d2c60436..484b1d44 100644 --- a/src/pages/Stream/components/Querier/SavedFiltersModal.tsx +++ b/src/pages/Stream/components/Querier/SavedFiltersModal.tsx @@ -86,7 +86,7 @@ const SavedFilterItem = (props: { {showDeletePropmt ? ( - diff --git a/src/pages/Stream/components/Querier/index.tsx b/src/pages/Stream/components/Querier/index.tsx index 657e5639..1b1cd02b 100644 --- a/src/pages/Stream/components/Querier/index.tsx +++ b/src/pages/Stream/components/Querier/index.tsx @@ -145,10 +145,9 @@ const Querier = () => { } // ----------------------------------- - // trigger query fetch if the rules were updated by the remove btn on pills // ----------------------------------- - if (!showQueryBuilder && activeMode !== 'sql') { + if (!showQueryBuilder && (activeMode !== 'sql' || savedFilterId)) { if (!shouldSumbitDisabled) { onFiltersApply({ isUncontrolled: true }); } @@ -159,7 +158,6 @@ const Querier = () => { } // ----------------------------------- - // trigger reset when no active rules are available if (isQuerySearchActive && allValues.length === 0 && activeMode !== 'sql') { onClear(); From 77d5b37c0e75c02b47c9884a57f9eec828963b3d Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Fri, 28 Jun 2024 15:43:30 +0530 Subject: [PATCH 08/12] http method changes for crud calls --- src/@types/parseable/api/savedFilters.ts | 15 +++++++ src/api/constants.ts | 4 +- src/api/logStream.ts | 18 +++++--- src/hooks/useSavedFilters.tsx | 41 ++++++++++++++----- .../components/Querier/SaveFilterModal.tsx | 33 ++++++++++----- 5 files changed, 83 insertions(+), 28 deletions(-) diff --git a/src/@types/parseable/api/savedFilters.ts b/src/@types/parseable/api/savedFilters.ts index 76e5b443..d45dd3c8 100644 --- a/src/@types/parseable/api/savedFilters.ts +++ b/src/@types/parseable/api/savedFilters.ts @@ -5,6 +5,21 @@ export type SavedFilterType = { 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; diff --git a/src/api/constants.ts b/src/api/constants.ts index 8f3b9eb0..00327e63 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -6,7 +6,9 @@ export const LOG_STREAMS_SCHEMA_URL = (streamName: string) => `${LOG_STREAM_LIST 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 = (userId: string, filterId: string) => `${API_V1}/filters/${userId}/${filterId}`; +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 980ac508..dc12464b 100644 --- a/src/api/logStream.ts +++ b/src/api/logStream.ts @@ -1,4 +1,4 @@ -import { SavedFilterType } from '@/@types/parseable/api/savedFilters'; +import { CreateSavedFilterType, SavedFilterType } from '@/@types/parseable/api/savedFilters'; import { Axios } from './axios'; import { DELETE_STREAMS_URL, @@ -10,7 +10,9 @@ import { CREATE_STREAM_URL, LOG_STREAMS_INFO_URL, LIST_SAVED_FILTERS_URL, - UPDATE_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'; @@ -34,12 +36,16 @@ export const getSavedFilters = (userId: string, headers: any) => { return Axios().get(LIST_SAVED_FILTERS_URL(userId), { headers }); }; -export const updateSavedFilters = (userId: string, filterId: string, filter: SavedFilterType, headers: any) => { - return Axios().post(UPDATE_SAVED_FILTERS_URL(userId, filterId), filter, { headers }); +export const putSavedFilters = (filterId: string, filter: SavedFilterType) => { + return Axios().put(UPDATE_SAVED_FILTERS_URL(filterId), filter); }; -export const deleteSavedFilter = (userId: string, filterId: string, headers: any) => { - return Axios().delete(UPDATE_SAVED_FILTERS_URL(userId, filterId), { headers }); +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) => { diff --git a/src/hooks/useSavedFilters.tsx b/src/hooks/useSavedFilters.tsx index 6e12ef2b..2052fbea 100644 --- a/src/hooks/useSavedFilters.tsx +++ b/src/hooks/useSavedFilters.tsx @@ -1,18 +1,18 @@ import { useMutation, useQuery } from 'react-query'; -import { getSavedFilters, updateSavedFilters, deleteSavedFilter } from '@/api/logStream'; +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 { SavedFilterType } from '@/@types/parseable/api/savedFilters'; +import { CreateSavedFilterType, SavedFilterType } from '@/@types/parseable/api/savedFilters'; import { useLogsStore, logsStoreReducers } from '@/pages/Stream/providers/LogsProvider'; const { setSavedFilters } = appStoreReducers; -const {updateSavedFilterId} = logsStoreReducers; +const { updateSavedFilterId } = logsStoreReducers; const useSavedFiltersQuery = (streamName: string) => { const [, setAppStore] = useAppStore((_store) => null); - const [, setLogsStore] = useLogsStore(_store => null); + const [, setLogsStore] = useLogsStore((_store) => null); const username = Cookies.get('username'); const { isError, isSuccess, isLoading, refetch } = useQuery( ['saved-filters'], @@ -27,9 +27,29 @@ const useSavedFiltersQuery = (streamName: string) => { }, ); - const { mutate: mutateSavedFilters } = useMutation( + const { mutate: updateSavedFilters } = useMutation( (data: { filter: SavedFilterType; onSuccess?: () => void }) => - updateSavedFilters(username || '', data.filter.filter_id, data.filter, { 'x-p-stream': streamName }), + 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(); @@ -49,11 +69,11 @@ const useSavedFiltersQuery = (streamName: string) => { const { mutate: deleteSavedFilterMutation } = useMutation( (data: { filter_id: string; onSuccess?: () => void }) => - deleteSavedFilter(username || '', data.filter_id, { 'x-p-stream': streamName }), + deleteSavedFilter(data.filter_id), { onSuccess: (_data, variables) => { variables.onSuccess && variables.onSuccess(); - setLogsStore(store => updateSavedFilterId(store, null)) + setLogsStore((store) => updateSavedFilterId(store, null)); refetch(); notifySuccess({ message: 'Updated Successfully' }); }, @@ -73,8 +93,9 @@ const useSavedFiltersQuery = (streamName: string) => { isSuccess, isLoading, refetch, - mutateSavedFilters, - deleteSavedFilterMutation + updateSavedFilters, + deleteSavedFilterMutation, + createSavedFilters }; }; diff --git a/src/pages/Stream/components/Querier/SaveFilterModal.tsx b/src/pages/Stream/components/Querier/SaveFilterModal.tsx index 82666f22..f286a719 100644 --- a/src/pages/Stream/components/Querier/SaveFilterModal.tsx +++ b/src/pages/Stream/components/Querier/SaveFilterModal.tsx @@ -5,31 +5,35 @@ import { makeTimeRangeLabel, useLogsStore } from '../../providers/LogsProvider'; import { CodeHighlight } from '@mantine/code-highlight'; import _ from 'lodash'; import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; -import { SavedFilterType } from '@/@types/parseable/api/savedFilters'; -import { generateRandomId } from '@/utils'; +import { CreateSavedFilterType, SavedFilterType } from '@/@types/parseable/api/savedFilters'; import useSavedFiltersQuery from '@/hooks/useSavedFilters'; +import Cookies from 'js-cookie'; const { toggleSaveFiltersModal } = filterStoreReducers; -interface FormObjectType extends SavedFilterType { +interface FormObjectType extends Omit { includeTimeRange: boolean; isNew: boolean; isError: boolean; + filter_id?: string; + version?: string; } -const sanitizeFilterItem = (formObject: FormObjectType): SavedFilterType => { - const { stream_name, filter_name, filter_id, query, time_filter, includeTimeRange } = formObject; +const sanitizeFilterItem = (formObject: FormObjectType): SavedFilterType => { + const { stream_name, filter_name, filter_id = '', query, time_filter, includeTimeRange, user_id, version = '' } = formObject; return { - version: 'v1', - stream_name, filter_id, + version, + stream_name, filter_name, time_filter: includeTimeRange ? time_filter : null, query, + user_id, }; }; const SaveFilterModal = () => { + const username = Cookies.get('username'); const [isSaveFiltersModalOpen, setFilterStore] = useFilterStore((store) => store.isSaveFiltersModalOpen); const [activeSavedFilters] = useAppStore((store) => store.activeSavedFilters); const [formObject, setFormObject] = useState(null); @@ -38,7 +42,7 @@ const SaveFilterModal = () => { const [{ custSearchQuery, savedFilterId, activeMode }] = useLogsStore((store) => store.custQuerySearchState); const [isDirty, setDirty] = useState(false); - const { mutateSavedFilters } = useSavedFiltersQuery(currentStream || ''); + const { updateSavedFilters, createSavedFilters } = useSavedFiltersQuery(currentStream || ''); useEffect(() => { const selectedFilter = _.find(activeSavedFilters, (filter) => filter.filter_id === savedFilterId); @@ -59,9 +63,7 @@ const SaveFilterModal = () => { }); } else { setFormObject({ - filter_id: generateRandomId(6), includeTimeRange: false, - version: 'v1', stream_name: currentStream, filter_name: '', query: { @@ -74,6 +76,7 @@ const SaveFilterModal = () => { }, isNew: true, isError: false, + user_id: username || '' }); } }, [custSearchQuery, savedFilterId]); @@ -108,7 +111,15 @@ const SaveFilterModal = () => { }); } - mutateSavedFilters({ filter: sanitizeFilterItem(formObject), onSuccess: closeModal }); + 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) => { From d46814c3b741a9ca0b69136b16bbcdc69a594fd0 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Fri, 28 Jun 2024 15:49:19 +0530 Subject: [PATCH 09/12] retain created item in context --- src/hooks/useSavedFilters.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSavedFilters.tsx b/src/hooks/useSavedFilters.tsx index 2052fbea..a919b3bb 100644 --- a/src/hooks/useSavedFilters.tsx +++ b/src/hooks/useSavedFilters.tsx @@ -51,8 +51,9 @@ const useSavedFiltersQuery = (streamName: string) => { (data: { filter: CreateSavedFilterType; onSuccess?: () => void }) => postSavedFilters(data.filter), { - onSuccess: (_data, variables) => { + onSuccess: (data, variables) => { variables.onSuccess && variables.onSuccess(); + setLogsStore((store) => updateSavedFilterId(store, data.data.filter_id)); refetch(); notifySuccess({ message: 'Updated Successfully' }); }, From 37d67d86caaa42aa4cb6091b0f84a2d22e68c211 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Fri, 28 Jun 2024 17:31:16 +0530 Subject: [PATCH 10/12] fix - support for override timerange in filter --- src/api/logStream.ts | 4 +-- src/hooks/useSavedFilters.tsx | 4 +-- .../components/Querier/SaveFilterModal.tsx | 28 +++++++++++++++---- .../components/Querier/SavedFiltersModal.tsx | 7 ++--- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/api/logStream.ts b/src/api/logStream.ts index dc12464b..9c1983ec 100644 --- a/src/api/logStream.ts +++ b/src/api/logStream.ts @@ -32,8 +32,8 @@ export const putLogStreamAlerts = (streamName: string, data: any) => { return Axios().put(LOG_STREAMS_ALERTS_URL(streamName), data); }; -export const getSavedFilters = (userId: string, headers: any) => { - return Axios().get(LIST_SAVED_FILTERS_URL(userId), { headers }); +export const getSavedFilters = (userId: string) => { + return Axios().get(LIST_SAVED_FILTERS_URL(userId)); }; export const putSavedFilters = (filterId: string, filter: SavedFilterType) => { diff --git a/src/hooks/useSavedFilters.tsx b/src/hooks/useSavedFilters.tsx index a919b3bb..1c58d7e4 100644 --- a/src/hooks/useSavedFilters.tsx +++ b/src/hooks/useSavedFilters.tsx @@ -10,13 +10,13 @@ import { useLogsStore, logsStoreReducers } from '@/pages/Stream/providers/LogsPr const { setSavedFilters } = appStoreReducers; const { updateSavedFilterId } = logsStoreReducers; -const useSavedFiltersQuery = (streamName: string) => { +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 || '', { 'x-p-stream': streamName }), + () => getSavedFilters(username || ''), { retry: false, enabled: false, // not on mount diff --git a/src/pages/Stream/components/Querier/SaveFilterModal.tsx b/src/pages/Stream/components/Querier/SaveFilterModal.tsx index f286a719..28024cfa 100644 --- a/src/pages/Stream/components/Querier/SaveFilterModal.tsx +++ b/src/pages/Stream/components/Querier/SaveFilterModal.tsx @@ -35,6 +35,7 @@ const sanitizeFilterItem = (formObject: FormObjectType): SavedFilterType => { 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); @@ -42,7 +43,7 @@ const SaveFilterModal = () => { const [{ custSearchQuery, savedFilterId, activeMode }] = useLogsStore((store) => store.custQuerySearchState); const [isDirty, setDirty] = useState(false); - const { updateSavedFilters, createSavedFilters } = useSavedFiltersQuery(currentStream || ''); + const { updateSavedFilters, createSavedFilters } = useSavedFiltersQuery(); useEffect(() => { const selectedFilter = _.find(activeSavedFilters, (filter) => filter.filter_id === savedFilterId); @@ -62,13 +63,14 @@ const SaveFilterModal = () => { isError: false, }); } else { + const isSqlMode = activeMode === 'sql'; setFormObject({ includeTimeRange: false, stream_name: currentStream, filter_name: '', query: { - filter_type: 'sql', - filter_query: custSearchQuery, + filter_type: isSqlMode ? 'sql' : 'builder', + ...(isSqlMode ? { filter_query: custSearchQuery } : { filter_builder: appliedQuery }), }, time_filter: { from: timeRange.startTime.toISOString(), @@ -76,10 +78,26 @@ const SaveFilterModal = () => { }, isNew: true, isError: false, - user_id: username || '' + user_id: username || '', }); } - }, [custSearchQuery, savedFilterId]); + }, [custSearchQuery, savedFilterId, activeMode]); + + useEffect(() => { + if (formObject?.includeTimeRange) { + setFormObject((prev) => { + if (!prev) return null; + + return { + ...prev, + time_filter: { + from: timeRange.startTime.toISOString(), + to: timeRange.endTime.toISOString(), + }, + }; + }); + } + }, [formObject?.includeTimeRange]) const closeModal = useCallback(() => { setFilterStore((store) => toggleSaveFiltersModal(store, false)); diff --git a/src/pages/Stream/components/Querier/SavedFiltersModal.tsx b/src/pages/Stream/components/Querier/SavedFiltersModal.tsx index 484b1d44..457d15ac 100644 --- a/src/pages/Stream/components/Querier/SavedFiltersModal.tsx +++ b/src/pages/Stream/components/Querier/SavedFiltersModal.tsx @@ -37,17 +37,16 @@ const getTimeRangeLabel = (startTime: string, endTime: string) => { const SavedFilterItem = (props: { item: SavedFilterType; - onSqlSearchApply: (query: string, id: string, time_filter: null | {from: string, to: string}) => void; + onSqlSearchApply: (query: string, id: string, time_filter: null | { from: string; to: string }) => void; onFilterBuilderQueryApply: (query: QueryType, id: string) => void; currentStream: string; }) => { const { item: { filter_name, time_filter, query, filter_id, stream_name }, - currentStream, } = props; const [showQuery, setShowQuery] = useState(false); const [showDeletePropmt, setShowDeletePrompt] = useState(false); - const { deleteSavedFilterMutation } = useSavedFiltersQuery(currentStream); + const { deleteSavedFilterMutation } = useSavedFiltersQuery(); const toggleShowQuery = useCallback(() => { return setShowQuery((prev) => !prev); @@ -141,7 +140,7 @@ const SavedFiltersModal = () => { const [savedFilters] = useAppStore((store) => store.savedFilters); const [activeSavedFilters] = useAppStore((store) => store.activeSavedFilters); const [currentStream] = useAppStore((store) => store.currentStream); - const { isLoading, refetch, isError } = useSavedFiltersQuery(currentStream || ''); + const { isLoading, refetch, isError } = useSavedFiltersQuery(); const onSqlSearchApply = useCallback((query: string, id: string, time_filter: null | {from: string, to: string}) => { setFilterStore((store) => resetFilters(store)); From 9cd57d98593d03962ab66e9aac1378402c709a81 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Sat, 29 Jun 2024 20:16:21 +0530 Subject: [PATCH 11/12] added ability to override stored timerange with current timerange --- .../MainLayout/providers/AppProvider.tsx | 5 +- .../components/Querier/SaveFilterModal.tsx | 103 ++++++++++-------- src/pages/Stream/providers/LogsProvider.tsx | 2 +- 3 files changed, 60 insertions(+), 50 deletions(-) diff --git a/src/layouts/MainLayout/providers/AppProvider.tsx b/src/layouts/MainLayout/providers/AppProvider.tsx index d44fc28b..5dcc8019 100644 --- a/src/layouts/MainLayout/providers/AppProvider.tsx +++ b/src/layouts/MainLayout/providers/AppProvider.tsx @@ -96,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) => { diff --git a/src/pages/Stream/components/Querier/SaveFilterModal.tsx b/src/pages/Stream/components/Querier/SaveFilterModal.tsx index 28024cfa..661a8b3d 100644 --- a/src/pages/Stream/components/Querier/SaveFilterModal.tsx +++ b/src/pages/Stream/components/Querier/SaveFilterModal.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Modal, Stack, Switch, Text, TextInput } from '@mantine/core'; +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'; @@ -12,26 +12,54 @@ import Cookies from 'js-cookie'; const { toggleSaveFiltersModal } = filterStoreReducers; interface FormObjectType extends Omit { - includeTimeRange: boolean; isNew: boolean; isError: boolean; filter_id?: string; version?: string; + timeRangeOptions: {value: string, label: string}[]; + selectedTimeRangeOption: {value: string, label: string} } const sanitizeFilterItem = (formObject: FormObjectType): SavedFilterType => { - const { stream_name, filter_name, filter_id = '', query, time_filter, includeTimeRange, user_id, version = '' } = formObject; + const { stream_name, filter_name, filter_id = '', query, time_filter, user_id, version = '' } = formObject; return { filter_id, version, stream_name, filter_name, - time_filter: includeTimeRange ? time_filter : null, + time_filter: time_filter ? time_filter : null, query, user_id, }; }; +const defaultTimeRangeOption = { + value: 'none', + label: 'Time range not included', +} + +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)}`, + }, + ...(selected ? [{ value: 'selected', label: `Stored - ${makeTimeRangeLabel(selected.from, selected.to)}` }] : []), + ]; +}; + +const getDefaultTimeRangeOption = (opts: {value: string, label: 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); @@ -51,21 +79,20 @@ const SaveFilterModal = () => { if (selectedFilter) { const { time_filter } = selectedFilter; + const timeRangeOptions = makeTimeRangeOptions({selected: time_filter, current: timeRange}); + const selectedTimeRangeOption = getDefaultTimeRangeOption(timeRangeOptions) setFormObject({ ...selectedFilter, - time_filter: { - ...time_filter, - from: timeRange.startTime.toISOString(), - to: timeRange.endTime.toISOString(), - }, - includeTimeRange: !_.isEmpty(time_filter), isNew: false, isError: false, + timeRangeOptions, + selectedTimeRangeOption }); } else { const isSqlMode = activeMode === 'sql'; + const timeRangeOptions = makeTimeRangeOptions({ selected: null, current: timeRange }); + const selectedTimeRangeOption = getDefaultTimeRangeOption(timeRangeOptions); setFormObject({ - includeTimeRange: false, stream_name: currentStream, filter_name: '', query: { @@ -79,41 +106,37 @@ const SaveFilterModal = () => { isNew: true, isError: false, user_id: username || '', + timeRangeOptions, + selectedTimeRangeOption }); } - }, [custSearchQuery, savedFilterId, activeMode]); - - useEffect(() => { - if (formObject?.includeTimeRange) { - setFormObject((prev) => { - if (!prev) return null; - - return { - ...prev, - time_filter: { - from: timeRange.startTime.toISOString(), - to: timeRange.endTime.toISOString(), - }, - }; - }); - } - }, [formObject?.includeTimeRange]) + }, [custSearchQuery, savedFilterId, activeMode, timeRange]); const closeModal = useCallback(() => { setFilterStore((store) => toggleSaveFiltersModal(store, false)); }, []); - const onToggleIncludeTimeRange = useCallback(() => { + const onToggleIncludeTimeRange = useCallback((value: string | null) => { setDirty(true); setFormObject((prev) => { if (!prev) return null; + const time_filter = + value === 'none' || value === null + ? null + : value === 'selected' + ? prev.time_filter + : { + from: timeRange.startTime.toISOString(), + to: timeRange.endTime.toISOString(), + }; return { ...prev, - includeTimeRange: !prev.includeTimeRange, + time_filter, + selectedTimeRangeOption: _.find(prev.timeRangeOptions, option => option.value === value) || defaultTimeRangeOption }; }); - }, []); + }, [timeRange]); const onSubmit = useCallback(() => { if (!formObject) return; @@ -177,22 +200,8 @@ const SaveFilterModal = () => { - Include Time Range - - {formObject?.includeTimeRange - ? makeTimeRangeLabel(timeRange.startTime, timeRange.endTime) - : 'Time range is not included'} - - - - + Fixed Time Range + +
URLhttps://demo.parseable.iohttps://demo.parseable.com
Username