Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/@types/parseable/api/savedFilters.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 4 additions & 0 deletions src/api/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down
23 changes: 22 additions & 1 deletion src/api/logStream.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CreateSavedFilterType, SavedFilterType } from '@/@types/parseable/api/savedFilters';
import { Axios } from './axios';
import {
DELETE_STREAMS_URL,
Expand All @@ -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';

Expand All @@ -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<SavedFilterType[]>(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));
};
Expand Down
15 changes: 1 addition & 14 deletions src/components/Header/styles/LogQuery.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
103 changes: 103 additions & 0 deletions src/hooks/useSavedFilters.tsx
Original file line number Diff line number Diff line change
@@ -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;
24 changes: 20 additions & 4 deletions src/layouts/MainLayout/providers/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]: {
Expand All @@ -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 = {
Expand All @@ -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<SavedFilterType[]>) => ReducerOutput;
};

const initialState: AppStore = {
Expand All @@ -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);
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -118,6 +126,13 @@ const setInstanceConfig = (_store: AppStore, instanceConfig: AboutData | null) =
return { instanceConfig, isStandAloneMode: mode === 'Standalone' };
};

const setSavedFilters = (store: AppStore, savedFiltersResponse: AxiosResponse<SavedFilterType[]>) => {
const { currentStream } = store;
const savedFilters = savedFiltersResponse.data;
const activeSavedFilters = _.filter(savedFilters, (filter) => filter.stream_name === currentStream);
return { savedFilters, activeSavedFilters };
};

const appStoreReducers: AppStoreReducers = {
toggleMaximize,
toggleHelpModal,
Expand All @@ -128,6 +143,7 @@ const appStoreReducers: AppStoreReducers = {
setStreamSpecificUserAccess,
setInstanceConfig,
toggleCreateStreamModal,
setSavedFilters,
};

export { AppProvider, useAppStore, appStoreReducers };
19 changes: 15 additions & 4 deletions src/pages/Stream/components/EventTimeLineGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -31,7 +40,7 @@ const getCompactType = (interval: number): CompactInterval => {
} else if (totalMinutes <= 259200) {
return 'day';
} else {
return 'month'
return 'month';
}
};

Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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]);
Expand Down
14 changes: 12 additions & 2 deletions src/pages/Stream/components/PrimaryToolbar.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,19 +14,28 @@ 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 = () => <IconMaximize size={px('1rem')} stroke={1.5} />;
const renderDeleteIcon = () => <IconTrash size={px('1rem')} stroke={1.5} />;
const renderSavedFiltersIcon = () => <IconListSearch size={px('1rem')} stroke={1.5} />;

const MaximizeButton = () => {
const [_appStore, setAppStore] = useAppStore((_store) => null);
const onClick = useCallback(() => setAppStore(appStoreReducers.toggleMaximize), []);
return <IconButton renderIcon={renderMaximizeIcon} size={38} onClick={onClick} tooltipLabel="Full Screen" />;
};

const SavedFiltersButton = () => {
const [_store, setLogsStore] = useFilterStore((_store) => null);
const onClick = useCallback(() => setLogsStore((store) => toggleSavedFiltersModal(store, true)), []);
return <IconButton renderIcon={renderSavedFiltersIcon} size={38} onClick={onClick} tooltipLabel="Saved Filters" />;
};

const DeleteStreamButton = () => {
const [_appStore, setLogsStore] = useLogsStore((_store) => null);
const [_store, setLogsStore] = useLogsStore((_store) => null);
const onClick = useCallback(() => setLogsStore(toggleDeleteModal), []);
return <IconButton renderIcon={renderDeleteIcon} size={38} onClick={onClick} tooltipLabel="Delete" />;
};
Expand Down Expand Up @@ -57,6 +66,7 @@ const PrimaryToolbar = () => {
<Stack style={{ flexDirection: 'row', height: STREAM_PRIMARY_TOOLBAR_HEIGHT }} w="100%">
<StreamDropdown />
<Querier />
<SavedFiltersButton/>
<TimeRange />
<RefreshInterval />
<MaximizeButton />
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Stream/components/Querier/FilterQueryBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export const QueryPills = () => {
const { combinator, rules: ruleSets } = appliedQuery;
return (
<ScrollArea scrollbarSize={6} scrollHideDelay={0} offsetScrollbars={false}>
<Stack style={{ height: '100%' }}>
<Stack style={{ height: '100%'}}>
<Stack style={{ flexDirection: 'row' }} gap={8}>
{ruleSets.map((ruleSet, index) => {
const shouldShowCombinatorPill = ruleSets.length !== 1 && index + 1 !== ruleSets.length;
Expand Down
Loading