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:
parent
5e0e5730ba
commit
9ed3cb1b7e
10 changed files with 156 additions and 45 deletions
|
|
@ -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) {
|
|||
</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
|
||||
className="px-2 relative border-l border-l-gray-lighter"
|
||||
className="px-2 relative"
|
||||
style={{ minWidth: activeTab === 'EXPORT' ? '360px' : '270px' }}
|
||||
>
|
||||
<Tabs
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ const UXTTABS = {
|
|||
|
||||
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) {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default observer(EventGroupWrapper);
|
||||
|
|
@ -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 (
|
||||
<EventGroupWrapper
|
||||
query={query}
|
||||
|
|
@ -192,6 +199,7 @@ function EventsBlock(props: IProps) {
|
|||
isLastEvent={isLastEvent}
|
||||
isLastInGroup={isLastInGroup}
|
||||
isCurrent={isCurrent}
|
||||
isSearched={isSearched}
|
||||
showSelection={!playing}
|
||||
isNote={isNote}
|
||||
isTabChange={isTabChange}
|
||||
|
|
@ -249,12 +257,14 @@ function EventsBlock(props: IProps) {
|
|||
onClick={() => setMode(MODES.SEARCH)}
|
||||
>
|
||||
<Search size={14} />
|
||||
<div>{t('Search')} {usedEvents.length} {t('events')}</div>
|
||||
<div>
|
||||
{t('Search')} {usedEvents.length} {t('events')}
|
||||
</div>
|
||||
</Button>
|
||||
<Tooltip title={t('Close Panel')} placement='bottom' >
|
||||
<Tooltip title={t('Close Panel')} placement="bottom">
|
||||
<Button
|
||||
className="ml-auto"
|
||||
type='text'
|
||||
type="text"
|
||||
onClick={() => {
|
||||
setActiveTab('');
|
||||
}}
|
||||
|
|
@ -263,19 +273,23 @@ function EventsBlock(props: IProps) {
|
|||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
{mode === MODES.SEARCH ?
|
||||
{mode === MODES.SEARCH ? (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<EventSearch
|
||||
onChange={write}
|
||||
setActiveTab={setActiveTab}
|
||||
value={query}
|
||||
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>
|
||||
: null}
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className={cn('flex-1 pb-4', styles.eventsList)}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,26 @@ import {
|
|||
MobilePlayerContext,
|
||||
} from 'Components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import stl from './timeline.module.css';
|
||||
import { getTimelinePosition } from './getTimelinePosition';
|
||||
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 +39,7 @@ function EventsList() {
|
|||
<div
|
||||
/* @ts-ignore TODO */
|
||||
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)}%` }}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -53,7 +59,7 @@ function MobileEventsList() {
|
|||
<div
|
||||
/* @ts-ignore TODO */
|
||||
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)}%` }}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, any[]> = {};
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue