fixed sessions layout (#3138)

This commit is contained in:
Andrey Babushkin 2025-03-12 09:21:16 +01:00 committed by GitHub
parent e027a2d016
commit 5df934c9ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 109 additions and 116 deletions

View file

@ -208,7 +208,7 @@ function Login({ location }: LoginProps) {
{t('Login')} {t('Login')}
</Button> </Button>
<div className="my-8 text-center"> <div className="my-8 flex justify-center items-center flex-wrap">
<span className="color-gray-medium"> <span className="color-gray-medium">
{t('Having trouble logging in?')} {t('Having trouble logging in?')}
</span>{' '} </span>{' '}

View file

@ -52,7 +52,7 @@ export default function ErrorBars(props: Props) {
{/* <div className={cn("rounded-tr rounded-br", bgColor, stl.bar)}></div> */} {/* <div className={cn("rounded-tr rounded-br", bgColor, stl.bar)}></div> */}
</div> </div>
</div> </div>
<div className="mt-1 color-gray-medium text-sm">{t(state)}</div> <div className="mt-1 color-gray-medium text-sm truncate">{t(state)}</div>
</div> </div>
); );
} }

View file

@ -10,7 +10,7 @@ import {
assist as assistRoute, assist as assistRoute,
isRoute, isRoute,
liveSession, liveSession,
sessions as sessionsRoute sessions as sessionsRoute,
} from 'App/routes'; } from 'App/routes';
import { capitalize } from 'App/utils'; import { capitalize } from 'App/utils';
import { Avatar, CountryFlag, Icon, Label, TextEllipsis } from 'UI'; import { Avatar, CountryFlag, Icon, Label, TextEllipsis } from 'UI';
@ -76,7 +76,7 @@ interface Props {
const PREFETCH_STATE = { const PREFETCH_STATE = {
none: 0, none: 0,
loading: 1, loading: 1,
fetched: 2 fetched: 2,
}; };
function SessionItem(props: RouteComponentProps & Props) { function SessionItem(props: RouteComponentProps & Props) {
@ -102,7 +102,7 @@ function SessionItem(props: RouteComponentProps & Props) {
// location, // location,
isDisabled, isDisabled,
live: propsLive, live: propsLive,
isAdd isAdd,
} = props; } = props;
const { const {
@ -128,43 +128,47 @@ function SessionItem(props: RouteComponentProps & Props) {
platform, platform,
timezone: userTimezone, timezone: userTimezone,
isCallActive, isCallActive,
agentIds agentIds,
} = session; } = session;
// Memoize derived values // Memoize derived values
const queryParams = useMemo( const queryParams = useMemo(
() => Object.fromEntries(new URLSearchParams(location.search)), () => Object.fromEntries(new URLSearchParams(location.search)),
[location.search] [location.search],
); );
const isMobile = platform !== 'web'; const isMobile = platform !== 'web';
const formattedDuration = useMemo(() => durationFormatted(duration), [duration]); const formattedDuration = useMemo(
() => durationFormatted(duration),
[duration],
);
const hasUserId = userId || userAnonymousId; const hasUserId = userId || userAnonymousId;
const isSessions = useMemo( const isSessions = useMemo(
() => isRoute(SESSIONS_ROUTE, location.pathname), () => isRoute(SESSIONS_ROUTE, location.pathname),
[location.pathname] [location.pathname],
); );
const isAssist = useMemo(() => { const isAssist = useMemo(() => {
return ( return (
(!ignoreAssist && (!ignoreAssist &&
(isRoute(ASSIST_ROUTE, location.pathname) || (isRoute(ASSIST_ROUTE, location.pathname) ||
isRoute(ASSIST_LIVE_SESSION, location.pathname) || isRoute(ASSIST_LIVE_SESSION, location.pathname) ||
location.pathname.includes('multiview'))) || location.pathname.includes('multiview'))) ||
propsLive propsLive
); );
}, [ignoreAssist, location.pathname, propsLive]); }, [ignoreAssist, location.pathname, propsLive]);
const isLastPlayed = lastPlayedSessionId === sessionId; const isLastPlayed = lastPlayedSessionId === sessionId;
const live = sessionLive || propsLive; const live = sessionLive || propsLive;
const isMultiviewDisabled = isDisabled && location.pathname.includes('multiview'); const isMultiviewDisabled =
isDisabled && location.pathname.includes('multiview');
// Memoize metadata list creation // Memoize metadata list creation
const _metaList = useMemo(() => { const _metaList = useMemo(() => {
return Object.keys(metadata).map((key) => ({ return Object.keys(metadata).map((key) => ({
label: key, label: key,
value: metadata[key] value: metadata[key],
})); }));
}, [metadata]); }, [metadata]);
@ -193,7 +197,14 @@ function SessionItem(props: RouteComponentProps & Props) {
if (!disableUser && !hasUserFilter && hasUserId) { if (!disableUser && !hasUserFilter && hasUserId) {
onUserClick(userId, userAnonymousId); onUserClick(userId, userAnonymousId);
} }
}, [disableUser, hasUserFilter, hasUserId, onUserClick, userId, userAnonymousId]); }, [
disableUser,
hasUserFilter,
hasUserId,
onUserClick,
userId,
userAnonymousId,
]);
const handleAddClick = useCallback(() => { const handleAddClick = useCallback(() => {
if (!isDisabled && onClick) { if (!isDisabled && onClick) {
@ -205,11 +216,11 @@ function SessionItem(props: RouteComponentProps & Props) {
const formattedTime = useMemo(() => { const formattedTime = useMemo(() => {
const timezoneToUse = const timezoneToUse =
shownTimezone === 'user' && userTimezone shownTimezone === 'user' && userTimezone
? { ? {
label: userTimezone.split('+').join(' +'), label: userTimezone.split('+').join(' +'),
value: userTimezone.split(':')[0] value: userTimezone.split(':')[0],
} }
: timezone; : timezone;
return formatTimeOrDate(startedAt, timezoneToUse); return formatTimeOrDate(startedAt, timezoneToUse);
}, [startedAt, shownTimezone, userTimezone, timezone]); }, [startedAt, shownTimezone, userTimezone, timezone]);
@ -229,9 +240,9 @@ function SessionItem(props: RouteComponentProps & Props) {
startedAt, startedAt,
{ {
label: userTimezone.split('+').join(' +'), label: userTimezone.split('+').join(' +'),
value: userTimezone.split(':')[0] value: userTimezone.split(':')[0],
}, },
true true,
)}{' '} )}{' '}
{userTimezone} {userTimezone}
</span> </span>
@ -243,7 +254,9 @@ function SessionItem(props: RouteComponentProps & Props) {
return ( return (
<Tooltip <Tooltip
title={ title={
!isMultiviewDisabled ? '' : t(`Session already added into the multiview`) !isMultiviewDisabled
? ''
: t(`Session already added into the multiview`)
} }
> >
<div <div
@ -261,7 +274,7 @@ function SessionItem(props: RouteComponentProps & Props) {
{!compact && ( {!compact && (
<div <div
className={ className={
'flex flex-col shrink-0 pr-2 gap-2 w-full lg:w-[40%]' 'flex flex-col shrink-0 pr-2 gap-2 w-full lg:w-[40%] max-w-[260px]'
} }
> >
<div className="flex items-center pr-2 shrink-0"> <div className="flex items-center pr-2 shrink-0">
@ -393,7 +406,7 @@ function SessionItem(props: RouteComponentProps & Props) {
)} )}
</div> </div>
<div className="flex items-center m-auto"> <div className="flex items-center m-auto w-[15%] justify-end">
<div <div
className={cn( className={cn(
stl.playLink, stl.playLink,
@ -416,14 +429,10 @@ function SessionItem(props: RouteComponentProps & Props) {
</Label> </Label>
</div> </div>
)} )}
{isSessions && ( {isSessions && isLastPlayed && (
<div className="flex-shrink-0 w-24 mb-2 lg:mb-0"> <Label className="bg-neutral-100 p-1 py-0 text-xs whitespace-nowrap rounded-xl text-neutral-400 ms-auto">
{isLastPlayed && ( {t('LAST PLAYED')}
<Label className="bg-neutral-100 p-1 py-0 text-xs whitespace-nowrap rounded-xl text-neutral-400 ms-auto"> </Label>
{t('LAST PLAYED')}
</Label>
)}
</div>
)} )}
{isAdd ? ( {isAdd ? (
<div <div
@ -453,4 +462,4 @@ function SessionItem(props: RouteComponentProps & Props) {
); );
} }
export default withRouter(observer(SessionItem)); export default withRouter(observer(SessionItem));

View file

@ -1,14 +1,17 @@
import React from 'react'; import React from 'react';
import Period from 'Types/app/period'; import Period from 'Types/app/period';
import SelectDateRange from 'Shared/SelectDateRange'; import SelectDateRange from 'Shared/SelectDateRange';
import { Space } from 'antd'; import { Grid, Space } from 'antd';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import SessionSort from '../SessionSort'; import SessionSort from '../SessionSort';
import SessionTags from '../SessionTags'; import SessionTags from '../SessionTags';
const { useBreakpoint } = Grid;
function SessionHeader() { function SessionHeader() {
const { searchStore } = useStore(); const { searchStore } = useStore();
const screens = useBreakpoint();
const { startDate, endDate, rangeValue } = searchStore.instance; const { startDate, endDate, rangeValue } = searchStore.instance;
const period = Period({ const period = Period({
@ -25,10 +28,14 @@ function SessionHeader() {
return ( return (
<div className="flex items-center px-4 py-3 justify-between w-full"> <div className="flex items-center px-4 py-3 justify-between w-full">
<div className="flex items-center w-full justify-end"> <div
className={`flex w-full flex-wrap gap-2 ${screens.md ? 'justify-between' : 'justify-start'}`}
>
<SessionTags /> <SessionTags />
<div className="mr-auto" /> <div
<Space> style={{ flexDirection: screens.md ? 'row' : 'column' }}
className={'flex items-start'}
>
<SelectDateRange <SelectDateRange
isAnt isAnt
period={period} period={period}
@ -36,7 +43,7 @@ function SessionHeader() {
right right
/> />
<SessionSort /> <SessionSort />
</Space> </div>
</div> </div>
</div> </div>
); );

View file

@ -63,12 +63,14 @@ function SessionSort() {
const defaultOption = `${sort}-${order}`; const defaultOption = `${sort}-${order}`;
return ( return (
<SortDropdown <div className="px-[7px]">
defaultOption={defaultOption} <SortDropdown
onSort={onSort} defaultOption={defaultOption}
sortOptions={sortOptions(t)} onSort={onSort}
current={sortOptionsMap(t)[defaultOption]} sortOptions={sortOptions(t)}
/> current={sortOptionsMap(t)[defaultOption]}
/>
</div>
); );
} }

View file

@ -1,11 +1,13 @@
import { issues_types, types } from 'Types/session/issue'; import { issues_types, types } from 'Types/session/issue';
import { Segmented } from 'antd'; import { Grid, Segmented } from 'antd';
import { Angry, CircleAlert, Skull, WifiOff, ChevronDown } from 'lucide-react'; import { Angry, CircleAlert, Skull, WifiOff, ChevronDown } from 'lucide-react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const { useBreakpoint } = Grid;
const tagIcons = { const tagIcons = {
[types.ALL]: undefined, [types.ALL]: undefined,
[types.JS_EXCEPTION]: <CircleAlert size={14} />, [types.JS_EXCEPTION]: <CircleAlert size={14} />,
@ -17,6 +19,7 @@ const tagIcons = {
function SessionTags() { function SessionTags() {
const { t } = useTranslation(); const { t } = useTranslation();
const screens = useBreakpoint();
const { projectsStore, sessionStore, searchStore } = useStore(); const { projectsStore, sessionStore, searchStore } = useStore();
const total = sessionStore.total; const total = sessionStore.total;
const platform = projectsStore.active?.platform || ''; const platform = projectsStore.active?.platform || '';
@ -87,51 +90,25 @@ function SessionTags() {
return ( return (
<div className="flex items-center"> <div className="flex items-center">
{isMobile ? ( <Segmented
<div className="relative" ref={dropdownRef}> vertical={!screens.md}
<button options={issues_types
onClick={() => setIsDropdownOpen(!isDropdownOpen)} .filter(
className="flex items-center text-start justify-between w-full px-3 py-2 text-base bg-white border rounded-lg focus:outline-none" (tag) =>
> tag.type !== 'mouse_thrashing' &&
<div className="flex items-center"> (platform === 'web'
{activeOption.icon && ( ? tag.type !== types.TAP_RAGE
<span className="mr-2">{activeOption.icon}</span> : tag.type !== types.CLICK_RAGE),
)} )
<span className="text-start">{activeOption.label}</span> .map((tag: any) => ({
</div> value: tag.type,
<ChevronDown icon: tagIcons[tag.type],
size={16} label: t(tag.name),
className={`ml-2 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} }))}
/> value={activeTab[0]}
</button> onChange={(value: any) => searchStore.toggleTag(value)}
size="small"
{isDropdownOpen && ( />
<div className="absolute z-10 w-full min-w-40 mt-1 bg-white border rounded-xl max-h-60 overflow-auto">
{filteredOptions.map((option) => (
<div
key={option.value}
className={`flex items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 ${
option.value === activeTab[0]
? 'bg-gray-50 font-medium'
: ''
}`}
onClick={() => handleSelectOption(option.value)}
>
{option.icon && <span className="mr-2">{option.icon}</span>}
<span>{option.label}</span>
</div>
))}
</div>
)}
</div>
) : (
<Segmented
options={filteredOptions}
value={activeTab[0]}
onChange={(value) => searchStore.toggleTag(value as any)}
size={'small'}
/>
)}
</div> </div>
); );
} }

View file

@ -23,7 +23,7 @@ function Layout(props: Props) {
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
const isMobile = window.innerWidth < 1200; const isMobile = window.innerWidth < 1280;
if (isMobile) { if (isMobile) {
setCollapsed(true); setCollapsed(true);
} else { } else {

View file

@ -48,7 +48,7 @@ function TopHeader() {
settingsStore.updateMenuCollapsed(!settingsStore.menuCollapsed); settingsStore.updateMenuCollapsed(!settingsStore.menuCollapsed);
}} }}
style={{ paddingTop: '4px' }} style={{ paddingTop: '4px' }}
className="cursor-pointer" className="cursor-pointer xl:block hidden"
> >
<Tooltip <Tooltip
title={ title={

View file

@ -1307,7 +1307,7 @@
"User's Time": "Hora del usuario", "User's Time": "Hora del usuario",
"Event": "Evento", "Event": "Evento",
"CALL IN PROGRESS": "LLAMADA EN CURSO", "CALL IN PROGRESS": "LLAMADA EN CURSO",
"LAST PLAYED": "ÚLTIMA REPRODUCCIÓN", "LAST PLAYED": "VISTO",
"Sessions Settings": "Configuración de Sesiones", "Sessions Settings": "Configuración de Sesiones",
"The percentage of session you want to capture": "El porcentaje de sesiones que deseas capturar", "The percentage of session you want to capture": "El porcentaje de sesiones que deseas capturar",
"Sessions exceeding this specified limit will not be captured or stored.": "Las sesiones que superen este límite especificado no se capturarán ni almacenarán.", "Sessions exceeding this specified limit will not be captured or stored.": "Las sesiones que superen este límite especificado no se capturarán ni almacenarán.",

View file

@ -1307,7 +1307,7 @@
"User's Time": "Heure de l'utilisateur", "User's Time": "Heure de l'utilisateur",
"Event": "Événement", "Event": "Événement",
"CALL IN PROGRESS": "APPEL EN COURS", "CALL IN PROGRESS": "APPEL EN COURS",
"LAST PLAYED": "DERNIÈRE LECTURE", "LAST PLAYED": "VISIONNÉ",
"Sessions Settings": "Paramètres des sessions", "Sessions Settings": "Paramètres des sessions",
"The percentage of session you want to capture": "Le pourcentage de sessions que vous souhaitez capturer", "The percentage of session you want to capture": "Le pourcentage de sessions que vous souhaitez capturer",
"Sessions exceeding this specified limit will not be captured or stored.": "Les sessions dépassant cette limite ne seront ni capturées ni stockées.", "Sessions exceeding this specified limit will not be captured or stored.": "Les sessions dépassant cette limite ne seront ni capturées ni stockées.",

View file

@ -1307,8 +1307,8 @@
"Local Time:": "Местное время:", "Local Time:": "Местное время:",
"User's Time": "Время пользователя", "User's Time": "Время пользователя",
"Event": "Событие", "Event": "Событие",
"CALL IN PROGRESS": "ЗВОНОК В ПРОЦЕССЕ", "CALL IN PROGRESS": "ИДЕТ ЗВОНОК",
"LAST PLAYED": ОСЛЕДНЕЕ ВОСПРОИЗВЕДЕНИЕ", "LAST PLAYED": РОСМОТРЕНО",
"Sessions Settings": "Настройки сессий", "Sessions Settings": "Настройки сессий",
"The percentage of session you want to capture": "Процент сессий, которые вы хотите захватывать", "The percentage of session you want to capture": "Процент сессий, которые вы хотите захватывать",
"Condition Set": "Набор условий", "Condition Set": "Набор условий",

View file

@ -7,26 +7,24 @@ deprecatedDefaults.forEach(color => {
}) })
module.exports = { module.exports = {
content: [ mode: 'jit',
'./app/**/*.tsx', content: ['./app/**/*.tsx', './app/**/*.js'],
'./app/**/*.js'
],
theme: { theme: {
colors: { colors: {
...defaultColors, ...defaultColors,
...colors ...colors,
}, },
extend: { extend: {
keyframes: { keyframes: {
'fade-in': { 'fade-in': {
'0%': { '0%': {
opacity: '0' opacity: '0',
// transform: 'translateY(-10px)' // transform: 'translateY(-10px)'
}, },
'100%': { '100%': {
opacity: '1' opacity: '1',
// transform: 'translateY(0)' // transform: 'translateY(0)'
} },
}, },
'bg-spin': { 'bg-spin': {
'0%': { '0%': {
@ -37,31 +35,31 @@ module.exports = {
}, },
'100%': { '100%': {
backgroundPosition: '0 50%', backgroundPosition: '0 50%',
} },
} },
}, },
animation: { animation: {
'fade-in': 'fade-in 0.2s ease-out', 'fade-in': 'fade-in 0.2s ease-out',
'bg-spin': 'bg-spin 1s ease infinite' 'bg-spin': 'bg-spin 1s ease infinite',
}, },
colors: { colors: {
'disabled-text': 'rgba(0,0,0, 0.38)' 'disabled-text': 'rgba(0,0,0, 0.38)',
}, },
boxShadow: { boxShadow: {
'border-blue': `0 0 0 1px ${colors['active-blue-border']}`, 'border-blue': `0 0 0 1px ${colors['active-blue-border']}`,
'border-main': `0 0 0 1px ${colors['main']}`, 'border-main': `0 0 0 1px ${colors['main']}`,
'border-gray': '0 0 0 1px #999' 'border-gray': '0 0 0 1px #999',
}, },
button: { button: {
'background-color': 'red' 'background-color': 'red',
} },
} },
}, },
variants: { variants: {
visibility: ['responsive', 'hover', 'focus', 'group-hover'] visibility: ['responsive', 'hover', 'focus', 'group-hover'],
}, },
plugins: [], plugins: [],
corePlugins: { corePlugins: {
preflight: false preflight: false,
} },
}; };