Add searched events (#3361)

* add filtered events to search

* removed consoles

* changed styles to tailwind

* changed styles to tailwind

* fixed errors
This commit is contained in:
Andrey Babushkin 2025-05-05 17:40:10 +02:00 committed by GitHub
parent 5e0e5730ba
commit 9ed3cb1b7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 156 additions and 45 deletions

View file

@ -16,12 +16,14 @@ import { IFRAME } from 'App/constants/storageKeys';
import stl from './playerBlockHeader.module.css'; import stl from './playerBlockHeader.module.css';
import UserCard from './EventsBlock/UserCard'; import UserCard from './EventsBlock/UserCard';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Switch } from 'antd';
const SESSIONS_ROUTE = sessionsRoute(); const SESSIONS_ROUTE = sessionsRoute();
function PlayerBlockHeader(props: any) { function PlayerBlockHeader(props: any) {
const { t } = useTranslation(); const { t } = useTranslation();
const [hideBack, setHideBack] = React.useState(false); const [hideBack, setHideBack] = React.useState(false);
const { uiPlayerStore } = useStore();
const { player, store } = React.useContext(PlayerContext); const { player, store } = React.useContext(PlayerContext);
const { uxtestingStore, customFieldStore, projectsStore, sessionStore } = const { uxtestingStore, customFieldStore, projectsStore, sessionStore } =
useStore(); useStore();
@ -123,9 +125,25 @@ function PlayerBlockHeader(props: any) {
</div> </div>
)} )}
</div> </div>
{uiPlayerStore.showSearchEventsSwitchButton ? (
<div className="px-2 relative flex items-center border-r border-r-gray-lighter">
<Switch
checked={uiPlayerStore.showOnlySearchEvents}
onChange={uiPlayerStore.setShowOnlySearchEvents}
style={{
background: uiPlayerStore.showOnlySearchEvents
? '#f0a930'
: 'rgba(0, 0, 0, 0.25)',
}}
/>
<span className="ml-2 whitespace-nowrap">
{t('Search Events Only')}
</span>
</div>
) : null}
</div> </div>
<div <div
className="px-2 relative border-l border-l-gray-lighter" className="px-2 relative"
style={{ minWidth: activeTab === 'EXPORT' ? '360px' : '270px' }} style={{ minWidth: activeTab === 'EXPORT' ? '360px' : '270px' }}
> >
<Tabs <Tabs

View file

@ -31,6 +31,10 @@ const UXTTABS = {
let playerInst: IPlayerContext['player'] | undefined; let playerInst: IPlayerContext['player'] | undefined;
const isDefaultEventsFilterSearch = (filters: FilterItem[]) => {
return filters.length === 1 && filters[0].key === 'location' && filters[0].value[0] === '';
}
function WebPlayer(props: any) { function WebPlayer(props: any) {
const { const {
notesStore, notesStore,
@ -38,6 +42,7 @@ function WebPlayer(props: any) {
uxtestingStore, uxtestingStore,
uiPlayerStore, uiPlayerStore,
integrationsStore, integrationsStore,
searchStore,
} = useStore(); } = useStore();
const devTools = sessionStore.devTools const devTools = sessionStore.devTools
const session = sessionStore.current; const session = sessionStore.current;
@ -57,6 +62,17 @@ function WebPlayer(props: any) {
const [fullView, setFullView] = useState(false); const [fullView, setFullView] = useState(false);
React.useEffect(() => { 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 = () => { const handleActivation = () => {
if (!document.hidden) { if (!document.hidden) {
setWindowActive(true); setWindowActive(true);

View file

@ -25,6 +25,7 @@ function EventGroupWrapper(props) {
isLastInGroup, isLastInGroup,
isSelected, isSelected,
isCurrent, isCurrent,
isSearched,
isEditing, isEditing,
showSelection, showSelection,
isFirst, isFirst,
@ -99,7 +100,7 @@ function EventGroupWrapper(props) {
); );
}; };
const shadowColor = props.isPrev const shadowColor = isSearched ? '#F0A930' : props.isPrev
? '#A7BFFF' ? '#A7BFFF'
: props.isCurrent : props.isCurrent
? '#394EFF' ? '#394EFF'
@ -127,7 +128,7 @@ function EventGroupWrapper(props) {
width: 10, width: 10,
height: 10, height: 10,
transform: 'rotate(45deg) translate(0, -50%)', transform: 'rotate(45deg) translate(0, -50%)',
background: '#394EFF', background: isSearched ? '#F0A930' : '#394EFF',
zIndex: 99, zIndex: 99,
borderRadius: '.15rem', borderRadius: '.15rem',
}} }}
@ -169,6 +170,6 @@ function TabChange({ from, to, activeUrl, onClick }) {
</div> </div>
</div> </div>
); );
} };
export default observer(EventGroupWrapper); export default observer(EventGroupWrapper);

View file

@ -2,13 +2,13 @@ import { mergeEventLists, sortEvents } from 'Types/session';
import { TYPES } from 'Types/session/event'; import { TYPES } from 'Types/session/event';
import cn from 'classnames'; import cn from 'classnames';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import React from 'react'; import React, { useEffect } from 'react';
import { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from 'virtua';
import { Button } from 'antd' import { Button } from 'antd';
import { PlayerContext } from 'App/components/Session/playerContext'; import { PlayerContext } from 'App/components/Session/playerContext';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { Icon } from 'UI'; import { Icon } from 'UI';
import { Search } from 'lucide-react' import { Search } from 'lucide-react';
import EventGroupWrapper from './EventGroupWrapper'; import EventGroupWrapper from './EventGroupWrapper';
import EventSearch from './EventSearch/EventSearch'; import EventSearch from './EventSearch/EventSearch';
import styles from './eventsBlock.module.css'; import styles from './eventsBlock.module.css';
@ -25,7 +25,7 @@ const MODES = {
SELECT: 'select', SELECT: 'select',
SEARCH: 'search', SEARCH: 'search',
EXPORT: 'export', EXPORT: 'export',
} };
function EventsBlock(props: IProps) { function EventsBlock(props: IProps) {
const defaultFramework = getDefaultFramework(); const defaultFramework = getDefaultFramework();
@ -47,6 +47,7 @@ function EventsBlock(props: IProps) {
const zoomStartTs = uiPlayerStore.timelineZoom.startTs; const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
const zoomEndTs = uiPlayerStore.timelineZoom.endTs; const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
const { store, player } = React.useContext(PlayerContext); const { store, player } = React.useContext(PlayerContext);
const [currentTimeEventIndex, setCurrentTimeEventIndex] = React.useState(0);
const { const {
time, time,
@ -94,8 +95,8 @@ function EventsBlock(props: IProps) {
? 'time' in e ? 'time' in e
? e.time >= zoomStartTs && e.time <= zoomEndTs ? e.time >= zoomStartTs && e.time <= zoomEndTs
: false : false
: true, : true
); ).filter((e: any) => !e.noteId && e.type !== 'TABCHANGE' && uiPlayerStore.showOnlySearchEvents ? e.isHighlighted : true);
}, [ }, [
filteredLength, filteredLength,
notesWithEvtsLength, notesWithEvtsLength,
@ -103,6 +104,7 @@ function EventsBlock(props: IProps) {
zoomEnabled, zoomEnabled,
zoomStartTs, zoomStartTs,
zoomEndTs, zoomEndTs,
uiPlayerStore.showOnlySearchEvents
]); ]);
const findLastFitting = React.useCallback( const findLastFitting = React.useCallback(
(time: number) => { (time: number) => {
@ -126,7 +128,10 @@ function EventsBlock(props: IProps) {
}, },
[usedEvents, time, endTime], [usedEvents, time, endTime],
); );
const currentTimeEventIndex = findLastFitting(time);
useEffect(() => {
setCurrentTimeEventIndex(findLastFitting(time));
}, [])
const write = ({ const write = ({
target: { value }, target: { value },
@ -182,6 +187,8 @@ function EventsBlock(props: IProps) {
const isTabChange = 'type' in event && event.type === 'TABCHANGE'; const isTabChange = 'type' in event && event.type === 'TABCHANGE';
const isCurrent = index === currentTimeEventIndex; const isCurrent = index === currentTimeEventIndex;
const isPrev = index < currentTimeEventIndex; const isPrev = index < currentTimeEventIndex;
const isSearched = event.isHighlighted
return ( return (
<EventGroupWrapper <EventGroupWrapper
query={query} query={query}
@ -192,6 +199,7 @@ function EventsBlock(props: IProps) {
isLastEvent={isLastEvent} isLastEvent={isLastEvent}
isLastInGroup={isLastInGroup} isLastInGroup={isLastInGroup}
isCurrent={isCurrent} isCurrent={isCurrent}
isSearched={isSearched}
showSelection={!playing} showSelection={!playing}
isNote={isNote} isNote={isNote}
isTabChange={isTabChange} isTabChange={isTabChange}
@ -249,12 +257,14 @@ function EventsBlock(props: IProps) {
onClick={() => setMode(MODES.SEARCH)} onClick={() => setMode(MODES.SEARCH)}
> >
<Search size={14} /> <Search size={14} />
<div>{t('Search')}&nbsp;{usedEvents.length}&nbsp;{t('events')}</div> <div>
{t('Search')}&nbsp;{usedEvents.length}&nbsp;{t('events')}
</div>
</Button> </Button>
<Tooltip title={t('Close Panel')} placement='bottom' > <Tooltip title={t('Close Panel')} placement="bottom">
<Button <Button
className="ml-auto" className="ml-auto"
type='text' type="text"
onClick={() => { onClick={() => {
setActiveTab(''); setActiveTab('');
}} }}
@ -263,19 +273,23 @@ function EventsBlock(props: IProps) {
</Tooltip> </Tooltip>
</div> </div>
) : null} ) : null}
{mode === MODES.SEARCH ? {mode === MODES.SEARCH ? (
<div className={'flex items-center gap-2'}> <div className={'flex items-center gap-2'}>
<EventSearch <EventSearch
onChange={write} onChange={write}
setActiveTab={setActiveTab} setActiveTab={setActiveTab}
value={query} value={query}
eventsText={ eventsText={
usedEvents.length ? `${usedEvents.length} ${t('Events')}` : `0 ${t('Events')}` usedEvents.length
? `${usedEvents.length} ${t('Events')}`
: `0 ${t('Events')}`
} }
/> />
<Button type={'text'} onClick={() => setMode(MODES.SELECT)}>{t('Cancel')}</Button> <Button type={'text'} onClick={() => setMode(MODES.SELECT)}>
{t('Cancel')}
</Button>
</div> </div>
: null} ) : null}
</div> </div>
<div <div
className={cn('flex-1 pb-4', styles.eventsList)} className={cn('flex-1 pb-4', styles.eventsList)}

View file

@ -4,20 +4,26 @@ import {
MobilePlayerContext, MobilePlayerContext,
} from 'Components/Session/playerContext'; } from 'Components/Session/playerContext';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import stl from './timeline.module.css';
import { getTimelinePosition } from './getTimelinePosition'; import { getTimelinePosition } from './getTimelinePosition';
import { useStore } from '@/mstore';
function EventsList() { function EventsList() {
const { store } = useContext(PlayerContext); const { store } = useContext(PlayerContext);
const { uiPlayerStore } = useStore();
const { eventCount, endTime } = store.get(); const { eventCount, endTime } = store.get();
const { tabStates } = store.get(); const { tabStates } = store.get();
const scale = 100 / endTime; const scale = 100 / endTime;
const events = React.useMemo( const events = React.useMemo(
() => Object.values(tabStates)[0]?.eventList.filter((e) => e.time) || [], () => Object.values(tabStates)[0]?.eventList.filter((e) => {
[eventCount], if (uiPlayerStore.showOnlySearchEvents) {
return e.time && (e as any).isHighlighted
} else {
return e.time
}
}) || [],
[eventCount, uiPlayerStore.showOnlySearchEvents],
); );
React.useEffect(() => { React.useEffect(() => {
const hasDuplicates = events.some( const hasDuplicates = events.some(
(e, i) => (e, i) =>
@ -33,7 +39,7 @@ function EventsList() {
<div <div
/* @ts-ignore TODO */ /* @ts-ignore TODO */
key={`${e.key}_${e.time}`} key={`${e.key}_${e.time}`}
className={stl.event} className={`absolute w-[2px] h-[10px] z-[3] pointer-events-none ${e.isHighlighted ? 'bg-[#f0a930]' : 'bg-[#394eff]'}`}
style={{ left: `${getTimelinePosition(e.time, scale)}%` }} style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
/> />
))} ))}
@ -53,7 +59,7 @@ function MobileEventsList() {
<div <div
/* @ts-ignore TODO */ /* @ts-ignore TODO */
key={`${e.key}_${e.time}`} key={`${e.key}_${e.time}`}
className={stl.event} className={`absolute w-[2px] h-[10px] z-[3] pointer-events-none ${e.isHighlighted ? 'bg-[#f0a930]' : 'bg-[#394eff]'}`}
style={{ left: `${getTimelinePosition(e.time, scale)}%` }} style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
/> />
))} ))}

View file

@ -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)),
};

View file

@ -49,24 +49,6 @@
z-index: 2; 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 { /* .event.click, .event.input {
background: $green; background: $green;
} }

View file

@ -15,9 +15,8 @@ import { loadFile } from 'App/player/web/network/loadFiles';
import { LAST_7_DAYS } from 'Types/app/period'; import { LAST_7_DAYS } from 'Types/app/period';
import { filterMap } from 'App/mstore/searchStore'; import { filterMap } from 'App/mstore/searchStore';
import { getDateRangeFromValue } from 'App/dateRange'; import { getDateRangeFromValue } from 'App/dateRange';
import { clean as cleanParams } from '../api_client';
import { searchStore, searchStoreLive } from './index'; import { searchStore, searchStoreLive } from './index';
import { checkEventWithFilters } from '@/components/Session_/Player/Controls/checkEventWithFilters';
const range = getDateRangeFromValue(LAST_7_DAYS); const range = getDateRangeFromValue(LAST_7_DAYS);
const defaultDateFilters = { const defaultDateFilters = {
@ -342,7 +341,14 @@ export default class SessionStore {
const eventsData: Record<string, any[]> = {}; const eventsData: Record<string, any[]> = {};
try { try {
const evData = await sessionService.getSessionEvents(sessionId); 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) { } catch (e) {
console.error('Failed to fetch events', e); console.error('Failed to fetch events', e);
} }

View file

@ -53,6 +53,8 @@ export const blockValues = [
export default class UiPlayerStore { export default class UiPlayerStore {
fullscreen = false; fullscreen = false;
showOnlySearchEvents = false;
showSearchEventsSwitchButton = false;
bottomBlock = 0; bottomBlock = 0;
@ -145,4 +147,12 @@ export default class UiPlayerStore {
setZoomTab = (tab: 'overview' | 'journey' | 'issues' | 'errors') => { setZoomTab = (tab: 'overview' | 'journey' | 'issues' | 'errors') => {
this.zoomTab = tab; this.zoomTab = tab;
}; };
setShowOnlySearchEvents = (show: boolean) => {
this.showOnlySearchEvents = show;
};
setSearchEventsSwitchButton = (show: boolean) => {
this.showSearchEventsSwitchButton = show;
};
} }

View file

@ -51,6 +51,7 @@ interface IEvent {
path: string; path: string;
label: string; label: string;
}; };
isHighlighted?: boolean;
} }
interface ConsoleEvent extends IEvent { interface ConsoleEvent extends IEvent {
@ -118,6 +119,8 @@ class Event {
messageId: IEvent['messageId']; messageId: IEvent['messageId'];
isHighlighted: IEvent['isHighlighted'];
constructor(event: IEvent) { constructor(event: IEvent) {
Object.assign(this, { Object.assign(this, {
time: event.time, time: event.time,
@ -125,6 +128,7 @@ class Event {
key: event.key, key: event.key,
tabId: event.tabId, tabId: event.tabId,
messageId: event.messageId, messageId: event.messageId,
isHighlighted: event.isHighlighted,
target: { target: {
path: event.target?.path || event.targetPath, path: event.target?.path || event.targetPath,
label: event.target?.label, label: event.target?.label,
@ -178,12 +182,15 @@ export class Click extends Event {
selector: string; selector: string;
isHighlighted: boolean | undefined = false;
constructor(evt: ClickEvent, isClickRage?: boolean) { constructor(evt: ClickEvent, isClickRage?: boolean) {
super(evt); super(evt);
this.targetContent = evt.targetContent; this.targetContent = evt.targetContent;
this.count = evt.count; this.count = evt.count;
this.hesitation = evt.hesitation; this.hesitation = evt.hesitation;
this.selector = evt.selector; this.selector = evt.selector;
this.isHighlighted = evt.isHighlighted;
if (isClickRage) { if (isClickRage) {
this.type = CLICKRAGE; this.type = CLICKRAGE;
} }