From 30fbc62f9b7aeacb97d974aac4b5d8c71e254202 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Wed, 2 Jul 2025 14:16:40 +0300 Subject: [PATCH] feat: group id clickable in event search (#10277) Now when pressing the group id, the query params get updated. Also the FilterItem appears and it is possible to discard the group id selection through it. ![image](https://github.com/user-attachments/assets/83d5446f-4823-4c25-9fdc-870c2e4811d8) --- .../component/events/EventCard/EventCard.tsx | 25 +++++++++++++ .../events/EventLog/EventLogFilters.test.tsx | 36 ++++++++++++++++-- .../events/EventLog/EventLogFilters.tsx | 37 +++++++++++++++++++ .../events/EventLog/useEventLogSearch.ts | 2 +- frontend/src/openapi/models/eventSchema.ts | 4 ++ 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/frontend/src/component/events/EventCard/EventCard.tsx b/frontend/src/component/events/EventCard/EventCard.tsx index 99846faa65..535492dd87 100644 --- a/frontend/src/component/events/EventCard/EventCard.tsx +++ b/frontend/src/component/events/EventCard/EventCard.tsx @@ -6,6 +6,7 @@ import { Link } from 'react-router-dom'; import { styled } from '@mui/material'; import type { EventSchema } from 'openapi'; import { useUiFlag } from 'hooks/useUiFlag'; +import { useLocation } from 'react-router-dom'; interface IEventCardProps { entry: EventSchema; @@ -74,17 +75,41 @@ export const StyledCodeSection = styled('div')(({ theme }) => ({ const EventCard = ({ entry }: IEventCardProps) => { const { locationSettings } = useLocationSettings(); const eventGroupingEnabled = useUiFlag('eventGrouping'); + const location = useLocation(); const createdAtFormatted = formatDateYMDHMS( entry.createdAt, locationSettings.locale, ); + const getGroupIdLink = () => { + const searchParams = new URLSearchParams(location.search); + searchParams.set('groupId', `IS:${entry.groupId}`); + return `${location.pathname}?${searchParams.toString()}`; + }; + return (
Event id:
{entry.id}
+ + + Group id: + +
+ + {entry.groupId} + +
+ + } + /> Changed at:
{createdAtFormatted}
Event: diff --git a/frontend/src/component/events/EventLog/EventLogFilters.test.tsx b/frontend/src/component/events/EventLog/EventLogFilters.test.tsx index e87c081cfd..dc7ac64905 100644 --- a/frontend/src/component/events/EventLog/EventLogFilters.test.tsx +++ b/frontend/src/component/events/EventLog/EventLogFilters.test.tsx @@ -1,12 +1,23 @@ import { renderHook } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import { useEventLogFilters } from './EventLogFilters.tsx'; const allFilterKeys = ['from', 'to', 'createdBy', 'type', 'project', 'feature']; allFilterKeys.sort(); +const renderWithRouter = (callback: () => any, initialEntries = ['/']) => { + return renderHook(callback, { + wrapper: ({ children }) => ( + + {children} + + ), + }); +}; + test('When you have no projects or flags, you should not get a project or flag filters', () => { - const { result } = renderHook(() => useEventLogFilters([], [])); + const { result } = renderWithRouter(() => useEventLogFilters([], [])); const filterKeys = result.current.map((filter) => filter.filterKey); filterKeys.sort(); @@ -15,7 +26,7 @@ test('When you have no projects or flags, you should not get a project or flag f }); test('When you have no projects, you should not get a project filter', () => { - const { result } = renderHook(() => + const { result } = renderWithRouter(() => useEventLogFilters( [], // @ts-expect-error: omitting other properties we don't need @@ -29,7 +40,7 @@ test('When you have no projects, you should not get a project filter', () => { }); test('When you have only one project, you should not get a project filter', () => { - const { result } = renderHook(() => + const { result } = renderWithRouter(() => useEventLogFilters([{ id: 'a', name: 'A' }], []), ); const filterKeys = result.current.map((filter) => filter.filterKey); @@ -39,7 +50,7 @@ test('When you have only one project, you should not get a project filter', () = }); test('When you have two one project, you should not get a project filter', () => { - const { result } = renderHook(() => + const { result } = renderWithRouter(() => useEventLogFilters( [ { id: 'a', name: 'A' }, @@ -53,3 +64,20 @@ test('When you have two one project, you should not get a project filter', () => expect(filterKeys).toContain('project'); }); + +test('When groupId is in URL params, should include groupId filter', () => { + const { result } = renderWithRouter( + () => useEventLogFilters([], []), + ['/?groupId=IS:123'], + ); + const filterKeys = result.current.map((filter) => filter.filterKey); + + expect(filterKeys).toContain('groupId'); +}); + +test('When no groupId in URL params, should not include groupId filter', () => { + const { result } = renderWithRouter(() => useEventLogFilters([], [])); + const filterKeys = result.current.map((filter) => filter.filterKey); + + expect(filterKeys).not.toContain('groupId'); +}); diff --git a/frontend/src/component/events/EventLog/EventLogFilters.tsx b/frontend/src/component/events/EventLog/EventLogFilters.tsx index b0ffa3c7a7..5d089fe0bf 100644 --- a/frontend/src/component/events/EventLog/EventLogFilters.tsx +++ b/frontend/src/component/events/EventLog/EventLogFilters.tsx @@ -10,6 +10,8 @@ import { EventSchemaType, type FeatureSearchResponseSchema } from 'openapi'; import type { ProjectSchema } from 'openapi'; import { useEventCreators } from 'hooks/api/getters/useEventCreators/useEventCreators'; import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; +import { useLocation } from 'react-router-dom'; +import { FilterItemParam } from 'utils/serializeQueryParams'; export const useEventLogFilters = ( projects: ProjectSchema[], @@ -17,9 +19,14 @@ export const useEventLogFilters = ( ) => { const { environments } = useEnvironments(); const { eventCreators } = useEventCreators(); + const location = useLocation(); const [availableFilters, setAvailableFilters] = useState([]); useEffect(() => { + const searchParams = new URLSearchParams(location.search); + const hasGroupId = searchParams.has('groupId'); + const groupIdValue = searchParams.get('groupId'); + const projectOptions = projects?.map((project: ProjectSchema) => ({ label: project.name, @@ -50,6 +57,22 @@ export const useEventLogFilters = ( value: env.name, })) ?? []; + const groupIdOptions = + hasGroupId && groupIdValue + ? (() => { + const parsedGroupId = + FilterItemParam.decode(groupIdValue); + return parsedGroupId + ? [ + { + label: parsedGroupId.values[0], + value: parsedGroupId.values[0], + }, + ] + : []; + })() + : []; + const availableFilters: IFilterItem[] = [ { label: 'Date From', @@ -87,6 +110,19 @@ export const useEventLogFilters = ( singularOperators: ['IS'], pluralOperators: ['IS_ANY_OF'], }, + ...(hasGroupId + ? ([ + { + label: 'Group ID', + icon: 'group', + options: groupIdOptions, + filterKey: 'groupId', + singularOperators: ['IS'], + pluralOperators: ['IS_ANY_OF'], + persistent: false, + }, + ] as IFilterItem[]) + : []), ...(projectOptions.length > 1 ? ([ { @@ -131,6 +167,7 @@ export const useEventLogFilters = ( JSON.stringify(projects), JSON.stringify(eventCreators), JSON.stringify(environments), + location.search, ]); return availableFilters; diff --git a/frontend/src/component/events/EventLog/useEventLogSearch.ts b/frontend/src/component/events/EventLog/useEventLogSearch.ts index cc98ffc7f6..6e853c7eec 100644 --- a/frontend/src/component/events/EventLog/useEventLogSearch.ts +++ b/frontend/src/component/events/EventLog/useEventLogSearch.ts @@ -73,7 +73,7 @@ export const useEventLogSearch = ( type: FilterItemParam, environment: FilterItemParam, id: StringParam, - groupId: StringParam, + groupId: FilterItemParam, ...extraParameters(logType), }; diff --git a/frontend/src/openapi/models/eventSchema.ts b/frontend/src/openapi/models/eventSchema.ts index c9889b5f37..47e2cdf9f6 100644 --- a/frontend/src/openapi/models/eventSchema.ts +++ b/frontend/src/openapi/models/eventSchema.ts @@ -46,6 +46,10 @@ export interface EventSchema { * @nullable */ ip?: string | null; + /** + * The event group ID. + */ + groupId?: string; /** * The concise, human-readable name of the event. * @nullable