From 9ed3cb1b7edae196dfdcc5ad8e90fb35895cf02d Mon Sep 17 00:00:00 2001 From: Andrey Babushkin <55714097+reyand43@users.noreply.github.com> Date: Mon, 5 May 2025 17:40:10 +0200 Subject: [PATCH] Add searched events (#3361) * add filtered events to search * removed consoles * changed styles to tailwind * changed styles to tailwind * fixed errors --- .../Player/ReplayPlayer/PlayerBlockHeader.tsx | 20 +++++++- frontend/app/components/Session/WebPlayer.tsx | 16 ++++++ ...tGroupWrapper.js => EventGroupWrapper.tsx} | 7 +-- .../Session_/EventsBlock/EventsBlock.tsx | 42 ++++++++++----- .../Session_/Player/Controls/EventsList.tsx | 18 ++++--- .../Player/Controls/checkEventWithFilters.ts | 51 +++++++++++++++++++ .../Player/Controls/timeline.module.css | 18 ------- frontend/app/mstore/sessionStore.ts | 12 +++-- frontend/app/mstore/uiPlayerStore.ts | 10 ++++ frontend/app/types/session/event.ts | 7 +++ 10 files changed, 156 insertions(+), 45 deletions(-) rename frontend/app/components/Session_/EventsBlock/{EventGroupWrapper.js => EventGroupWrapper.tsx} (96%) create mode 100644 frontend/app/components/Session_/Player/Controls/checkEventWithFilters.ts diff --git a/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx b/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx index c3784794b..ef769ec5f 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/PlayerBlockHeader.tsx @@ -16,12 +16,14 @@ import { IFRAME } from 'App/constants/storageKeys'; import stl from './playerBlockHeader.module.css'; import UserCard from './EventsBlock/UserCard'; import { useTranslation } from 'react-i18next'; +import { Switch } from 'antd'; const SESSIONS_ROUTE = sessionsRoute(); function PlayerBlockHeader(props: any) { const { t } = useTranslation(); const [hideBack, setHideBack] = React.useState(false); + const { uiPlayerStore } = useStore(); const { player, store } = React.useContext(PlayerContext); const { uxtestingStore, customFieldStore, projectsStore, sessionStore } = useStore(); @@ -123,9 +125,25 @@ function PlayerBlockHeader(props: any) { )} + {uiPlayerStore.showSearchEventsSwitchButton ? ( +
+ + + {t('Search Events Only')} + +
+ ) : null}
{ + return filters.length === 1 && filters[0].key === 'location' && filters[0].value[0] === ''; +} + function WebPlayer(props: any) { const { notesStore, @@ -38,6 +42,7 @@ function WebPlayer(props: any) { uxtestingStore, uiPlayerStore, integrationsStore, + searchStore, } = useStore(); const devTools = sessionStore.devTools const session = sessionStore.current; @@ -57,6 +62,17 @@ function WebPlayer(props: any) { const [fullView, setFullView] = useState(false); React.useEffect(() => { + if (searchStore.instance.filters?.length && !isDefaultEventsFilterSearch(searchStore.instance.filters)) { + uiPlayerStore.setSearchEventsSwitchButton(true); + uiPlayerStore.setShowOnlySearchEvents(true); + } else { + uiPlayerStore.setSearchEventsSwitchButton(false); + uiPlayerStore.setShowOnlySearchEvents(false); + } + }, [searchStore.instance.filters]); + + React.useEffect(() => { + openedAt.current = Date.now(); const handleActivation = () => { if (!document.hidden) { setWindowActive(true); diff --git a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.tsx similarity index 96% rename from frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js rename to frontend/app/components/Session_/EventsBlock/EventGroupWrapper.tsx index a999ecc9f..92f89bcea 100644 --- a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js +++ b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.tsx @@ -25,6 +25,7 @@ function EventGroupWrapper(props) { isLastInGroup, isSelected, isCurrent, + isSearched, isEditing, showSelection, isFirst, @@ -99,7 +100,7 @@ function EventGroupWrapper(props) { ); }; - const shadowColor = props.isPrev + const shadowColor = isSearched ? '#F0A930' : props.isPrev ? '#A7BFFF' : props.isCurrent ? '#394EFF' @@ -127,7 +128,7 @@ function EventGroupWrapper(props) { width: 10, height: 10, transform: 'rotate(45deg) translate(0, -50%)', - background: '#394EFF', + background: isSearched ? '#F0A930' : '#394EFF', zIndex: 99, borderRadius: '.15rem', }} @@ -169,6 +170,6 @@ function TabChange({ from, to, activeUrl, onClick }) {
); -} +}; export default observer(EventGroupWrapper); diff --git a/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx b/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx index 67767572a..7b0d24818 100644 --- a/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx +++ b/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx @@ -2,13 +2,13 @@ import { mergeEventLists, sortEvents } from 'Types/session'; import { TYPES } from 'Types/session/event'; import cn from 'classnames'; import { observer } from 'mobx-react-lite'; -import React from 'react'; +import React, { useEffect } from 'react'; import { VList, VListHandle } from 'virtua'; -import { Button } from 'antd' +import { Button } from 'antd'; import { PlayerContext } from 'App/components/Session/playerContext'; import { useStore } from 'App/mstore'; import { Icon } from 'UI'; -import { Search } from 'lucide-react' +import { Search } from 'lucide-react'; import EventGroupWrapper from './EventGroupWrapper'; import EventSearch from './EventSearch/EventSearch'; import styles from './eventsBlock.module.css'; @@ -25,7 +25,7 @@ const MODES = { SELECT: 'select', SEARCH: 'search', EXPORT: 'export', -} +}; function EventsBlock(props: IProps) { const defaultFramework = getDefaultFramework(); @@ -47,6 +47,7 @@ function EventsBlock(props: IProps) { const zoomStartTs = uiPlayerStore.timelineZoom.startTs; const zoomEndTs = uiPlayerStore.timelineZoom.endTs; const { store, player } = React.useContext(PlayerContext); + const [currentTimeEventIndex, setCurrentTimeEventIndex] = React.useState(0); const { time, @@ -94,8 +95,8 @@ function EventsBlock(props: IProps) { ? 'time' in e ? e.time >= zoomStartTs && e.time <= zoomEndTs : false - : true, - ); + : true + ).filter((e: any) => !e.noteId && e.type !== 'TABCHANGE' && uiPlayerStore.showOnlySearchEvents ? e.isHighlighted : true); }, [ filteredLength, notesWithEvtsLength, @@ -103,6 +104,7 @@ function EventsBlock(props: IProps) { zoomEnabled, zoomStartTs, zoomEndTs, + uiPlayerStore.showOnlySearchEvents ]); const findLastFitting = React.useCallback( (time: number) => { @@ -126,7 +128,10 @@ function EventsBlock(props: IProps) { }, [usedEvents, time, endTime], ); - const currentTimeEventIndex = findLastFitting(time); + + useEffect(() => { + setCurrentTimeEventIndex(findLastFitting(time)); + }, []) const write = ({ target: { value }, @@ -182,6 +187,8 @@ function EventsBlock(props: IProps) { const isTabChange = 'type' in event && event.type === 'TABCHANGE'; const isCurrent = index === currentTimeEventIndex; const isPrev = index < currentTimeEventIndex; + const isSearched = event.isHighlighted + return ( setMode(MODES.SEARCH)} > -
{t('Search')} {usedEvents.length} {t('events')}
+
+ {t('Search')} {usedEvents.length} {t('events')} +
- + + - : null} + ) : null}
Object.values(tabStates)[0]?.eventList.filter((e) => e.time) || [], - [eventCount], + () => Object.values(tabStates)[0]?.eventList.filter((e) => { + if (uiPlayerStore.showOnlySearchEvents) { + return e.time && (e as any).isHighlighted + } else { + return e.time + } + }) || [], + [eventCount, uiPlayerStore.showOnlySearchEvents], ); - React.useEffect(() => { const hasDuplicates = events.some( (e, i) => @@ -33,7 +39,7 @@ function EventsList() {
))} @@ -53,7 +59,7 @@ function MobileEventsList() {
))} diff --git a/frontend/app/components/Session_/Player/Controls/checkEventWithFilters.ts b/frontend/app/components/Session_/Player/Controls/checkEventWithFilters.ts new file mode 100644 index 000000000..fb39264fb --- /dev/null +++ b/frontend/app/components/Session_/Player/Controls/checkEventWithFilters.ts @@ -0,0 +1,51 @@ +import FilterItem from '@/mstore/types/filterItem'; + +export const checkEventWithFilters = (event: Event, filters: FilterItem[]) => { + let result = false; + filters.forEach((filter) => { + if (filter.key.toUpperCase() === event.type.toUpperCase()) { + if (filter.operator) { + const operator = operators[filter.operator]; + if (operator) { + result = !!operator(event.label, filter.value); + } + } + } + }); + return result +}; + +const operators = { + is: (val: string, target: string[]) => target.some((t) => val.includes(t)), + isAny: () => true, + isNot: (val: string, target: string[]) => + !target.some((t) => val.includes(t)), + contains: (val: string, target: string[]) => + target.some((t) => val.includes(t)), + notContains: (val: string, target: string[]) => + !target.some((t) => val.includes(t)), + startsWith: (val: string, target: string[]) => + target.some((t) => val.startsWith(t)), + endsWith: (val: string, target: string[]) => + target.some((t) => val.endsWith(t)), + greaterThan: (val: number, target: number) => val > target, + greaterOrEqual: (val: number, target: number) => val >= target, + lessOrEqual: (val: number, target: number) => val <= target, + lessThan: (val: number, target: number) => val < target, + on: (val: string, target: string[]) => target.some((t) => val.includes(t)), + notOn: (val: string, target: string[]) => + !target.some((t) => val.includes(t)), + onAny: () => true, + selectorIs: (val: string, target: string[]) => target.some((t) => val.includes(t)), + selectorIsAny: () => true, + selectorIsNot: (val: string, target: string[]) => + !target.some((t) => val.includes(t)), + selectorContains: (val: string, target: string[]) => + target.some((t) => val.includes(t)), + selectorNotContains: (val: string, target: string[]) => + !target.some((t) => val.includes(t)), + selectorStartsWith: (val: string, target: string[]) => + target.some((t) => val.startsWith(t)), + selectorEndsWith: (val: string, target: string[]) => + target.some((t) => val.endsWith(t)), +}; diff --git a/frontend/app/components/Session_/Player/Controls/timeline.module.css b/frontend/app/components/Session_/Player/Controls/timeline.module.css index 4830dac96..a067d6b8e 100644 --- a/frontend/app/components/Session_/Player/Controls/timeline.module.css +++ b/frontend/app/components/Session_/Player/Controls/timeline.module.css @@ -49,24 +49,6 @@ z-index: 2; } - -.event { - position: absolute; - width: 2px; - height: 10px; - background: $main; - z-index: 3; - pointer-events: none; - /* top: 0; */ - /* bottom: 0; */ - /* &:hover { - width: 10px; - height: 10px; - margin-left: -6px; - z-index: 1; - };*/ -} - /* .event.click, .event.input { background: $green; } diff --git a/frontend/app/mstore/sessionStore.ts b/frontend/app/mstore/sessionStore.ts index e6c51c8e2..cc754fb6b 100644 --- a/frontend/app/mstore/sessionStore.ts +++ b/frontend/app/mstore/sessionStore.ts @@ -15,9 +15,8 @@ import { loadFile } from 'App/player/web/network/loadFiles'; import { LAST_7_DAYS } from 'Types/app/period'; import { filterMap } from 'App/mstore/searchStore'; import { getDateRangeFromValue } from 'App/dateRange'; -import { clean as cleanParams } from '../api_client'; import { searchStore, searchStoreLive } from './index'; - +import { checkEventWithFilters } from '@/components/Session_/Player/Controls/checkEventWithFilters'; const range = getDateRangeFromValue(LAST_7_DAYS); const defaultDateFilters = { @@ -342,7 +341,14 @@ export default class SessionStore { const eventsData: Record = {}; try { const evData = await sessionService.getSessionEvents(sessionId); - Object.assign(eventsData, evData); + + Object.assign(eventsData, { + ...evData, + events: evData.events.map((e) => ({ + ...e, + isHighlighted: checkEventWithFilters(e, searchStore.instance.filters) + })) + }); } catch (e) { console.error('Failed to fetch events', e); } diff --git a/frontend/app/mstore/uiPlayerStore.ts b/frontend/app/mstore/uiPlayerStore.ts index 3f9466b08..172b85b38 100644 --- a/frontend/app/mstore/uiPlayerStore.ts +++ b/frontend/app/mstore/uiPlayerStore.ts @@ -53,6 +53,8 @@ export const blockValues = [ export default class UiPlayerStore { fullscreen = false; + showOnlySearchEvents = false; + showSearchEventsSwitchButton = false; bottomBlock = 0; @@ -145,4 +147,12 @@ export default class UiPlayerStore { setZoomTab = (tab: 'overview' | 'journey' | 'issues' | 'errors') => { this.zoomTab = tab; }; + + setShowOnlySearchEvents = (show: boolean) => { + this.showOnlySearchEvents = show; + }; + + setSearchEventsSwitchButton = (show: boolean) => { + this.showSearchEventsSwitchButton = show; + }; } diff --git a/frontend/app/types/session/event.ts b/frontend/app/types/session/event.ts index 982d3ca2b..2eeb568c4 100644 --- a/frontend/app/types/session/event.ts +++ b/frontend/app/types/session/event.ts @@ -51,6 +51,7 @@ interface IEvent { path: string; label: string; }; + isHighlighted?: boolean; } interface ConsoleEvent extends IEvent { @@ -118,6 +119,8 @@ class Event { messageId: IEvent['messageId']; + isHighlighted: IEvent['isHighlighted']; + constructor(event: IEvent) { Object.assign(this, { time: event.time, @@ -125,6 +128,7 @@ class Event { key: event.key, tabId: event.tabId, messageId: event.messageId, + isHighlighted: event.isHighlighted, target: { path: event.target?.path || event.targetPath, label: event.target?.label, @@ -178,12 +182,15 @@ export class Click extends Event { selector: string; + isHighlighted: boolean | undefined = false; + constructor(evt: ClickEvent, isClickRage?: boolean) { super(evt); this.targetContent = evt.targetContent; this.count = evt.count; this.hesitation = evt.hesitation; this.selector = evt.selector; + this.isHighlighted = evt.isHighlighted; if (isClickRage) { this.type = CLICKRAGE; }