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 95% rename from frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js rename to frontend/app/components/Session_/EventsBlock/EventGroupWrapper.tsx index a999ecc9f..39975aa94 100644 --- a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js +++ b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.tsx @@ -11,7 +11,7 @@ import NoteEvent from './NoteEvent'; import stl from './eventGroupWrapper.module.css'; import { useTranslation } from 'react-i18next'; -function EventGroupWrapper(props) { +const EventGroupWrapper = (props: any) => { const { userStore } = useStore(); const currentUserId = userStore.account.id; @@ -19,12 +19,16 @@ function EventGroupWrapper(props) { const onCheckboxClick = (e) => props.onCheckboxClick(e, props.event); + console.log('tick'); + + const { event, isLastEvent, isLastInGroup, isSelected, isCurrent, + isSearched, isEditing, showSelection, isFirst, @@ -99,7 +103,7 @@ function EventGroupWrapper(props) { ); }; - const shadowColor = props.isPrev + const shadowColor = isSearched ? '#F0A930' : props.isPrev ? '#A7BFFF' : props.isCurrent ? '#394EFF' @@ -127,7 +131,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 +173,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 fa206ebda..a2cf37b93 100644 --- a/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx +++ b/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx @@ -2,20 +2,20 @@ 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'; import { useTranslation } from 'react-i18next'; -import { CloseOutlined } from "@ant-design/icons"; -import { Tooltip } from "antd"; -import { getDefaultFramework, frameworkIcons } from "../UnitStepsModal"; +import { CloseOutlined } from '@ant-design/icons'; +import { Tooltip } from 'antd'; +import { getDefaultFramework, frameworkIcons } from '../UnitStepsModal'; interface IProps { setActiveTab: (tab?: string) => void; @@ -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, @@ -102,10 +103,7 @@ function EventsBlock(props: IProps) { ? e.time >= zoomStartTs && e.time <= zoomEndTs : false : true - ); - } else { - return list - } + ).filter((e: any) => !e.noteId && e.type !== 'TABCHANGE' && uiPlayerStore.showOnlySearchEvents ? e.isHighlighted : true); }, [ filteredLength, query, @@ -114,6 +112,7 @@ function EventsBlock(props: IProps) { zoomEnabled, zoomStartTs, zoomEndTs, + uiPlayerStore.showOnlySearchEvents ]); const findLastFitting = React.useCallback( (time: number) => { @@ -137,7 +136,12 @@ function EventsBlock(props: IProps) { }, [usedEvents, time, endTime], ); - const currentTimeEventIndex = findLastFitting(time); + + useEffect(() => { + setCurrentTimeEventIndex(findLastFitting(time)); + console.log('CUURENT TIME EVENT INDEX'); + }, []) + // const currentTimeEventIndex = findLastFitting(time); const write = ({ target: { value }, @@ -193,6 +197,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}
{t('No Matching Results')}
)} - { + return renderGroup({ index: i }); + })} + {/* { return renderGroup({ index: i }); })} - + */} ); diff --git a/frontend/app/components/Session_/Player/Controls/EventsList.tsx b/frontend/app/components/Session_/Player/Controls/EventsList.tsx index 7488f1960..d86e1ba36 100644 --- a/frontend/app/components/Session_/Player/Controls/EventsList.tsx +++ b/frontend/app/components/Session_/Player/Controls/EventsList.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { PlayerContext, MobilePlayerContext, @@ -6,18 +6,26 @@ import { import { observer } from 'mobx-react-lite'; import stl from './timeline.module.css'; import { getTimelinePosition } from './getTimelinePosition'; +import classNames from 'classnames'; +import { useStore } from '@/mstore'; function EventsList() { const { store } = useContext(PlayerContext); + const { uiPlayerStore } = useStore(); const { eventCount, endTime } = store.get(); const { tabStates } = store.get(); const scale = 100 / endTime; const events = React.useMemo( - () => 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 +41,9 @@ function EventsList() {
))} 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..18d089a24 100644 --- a/frontend/app/components/Session_/Player/Controls/timeline.module.css +++ b/frontend/app/components/Session_/Player/Controls/timeline.module.css @@ -67,6 +67,10 @@ };*/ } +.event__highlighted { + background: #f0a930; +} + /* .event.click, .event.input { background: $green; } diff --git a/frontend/app/mstore/sessionStore.ts b/frontend/app/mstore/sessionStore.ts index a0301adee..e5ae13218 100644 --- a/frontend/app/mstore/sessionStore.ts +++ b/frontend/app/mstore/sessionStore.ts @@ -15,9 +15,7 @@ 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 = { @@ -339,7 +337,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..a853fc791 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 = 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; }