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}
- {mode === MODES.SEARCH ?
+ {mode === MODES.SEARCH ? (
-
+
- : 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;
}