fix(ui): fixed sessionitem and timezone dropdown connection to mobx

This commit is contained in:
sylenien 2022-05-13 13:32:01 +02:00 committed by Delirium
parent 0d00cf0349
commit be13ff5f7a
6 changed files with 155 additions and 257 deletions

View file

@ -1,162 +0,0 @@
import { connect } from 'react-redux';
import cn from 'classnames';
import {
Link,
Icon,
CountryFlag,
Avatar,
TextEllipsis,
Label,
} from 'UI';
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
import { session as sessionRoute, liveSession as liveSessionRoute, withSiteId } from 'App/routes';
import { durationFormatted, formatTimeOrDate } from 'App/date';
import stl from './sessionItem.css';
import Counter from './Counter'
import { withRouter } from 'react-router-dom';
import SessionMetaList from './SessionMetaList';
import PlayLink from './PlayLink';
import ErrorBars from './ErrorBars';
import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from "App/routes";
import { capitalize } from 'App/utils';
const ASSIST_ROUTE = assistRoute();
const ASSIST_LIVE_SESSION = liveSession()
const SESSIONS_ROUTE = sessionsRoute();
@connect(state => ({
timezone: state.getIn(['sessions', 'timezone']),
siteId: state.getIn([ 'site', 'siteId' ]),
}), { toggleFavorite, setSessionPath })
@withRouter
export default class SessionItem extends React.PureComponent {
// eslint-disable-next-line complexity
render() {
const {
session: {
sessionId,
userBrowser,
userOs,
userId,
userAnonymousId,
userDisplayName,
userCountry,
startedAt,
duration,
eventsCount,
errorsCount,
pagesCount,
viewed,
favorite,
userDeviceType,
userUuid,
userNumericHash,
live,
metadata,
userSessionsCount,
issueTypes,
active,
},
timezone,
onUserClick = () => null,
hasUserFilter = false,
disableUser = false,
metaList = [],
showActive = false,
lastPlayedSessionId,
} = this.props;
const formattedDuration = durationFormatted(duration);
const hasUserId = userId || userAnonymousId;
const isSessions = isRoute(SESSIONS_ROUTE, this.props.location.pathname);
const isAssist = isRoute(ASSIST_ROUTE, this.props.location.pathname) || isRoute(ASSIST_LIVE_SESSION, this.props.location.pathname);
const isLastPlayed = lastPlayedSessionId === sessionId;
const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => {
const value = metadata[key];
return { label: key, value };
});
return (
<div className={ cn(stl.sessionItem, "flex flex-col p-3 mb-3") } id="session-item" >
<div className="flex items-start">
<div className={ cn('flex items-center w-full')}>
<div className="flex items-center pr-2" style={{ width: "30%"}}>
<div><Avatar seed={ userNumericHash } isAssist={isAssist} /></div>
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center">
<div
className={cn('text-lg', {'color-teal cursor-pointer': !disableUser && hasUserId, [stl.userName]: !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
>
<TextEllipsis text={userDisplayName} maxWidth={200} popupProps={{ inverted: true, size: 'tiny' }} />
</div>
</div>
</div>
<div style={{ width: "20%", height: "38px" }} className="px-2 flex flex-col justify-between">
<div>{formatTimeOrDate(startedAt, timezone) }</div>
<div className="flex items-center color-gray-medium">
{!isAssist && (
<>
<div className="color-gray-medium">
<span className="mr-1">{ eventsCount }</span>
<span>{ eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' }</span>
</div>
<div className="mx-2 text-4xl">·</div>
</>
)}
<div>{ live ? <Counter startTime={startedAt} /> : formattedDuration }</div>
</div>
</div>
<div style={{ width: "30%", height: "38px" }} className="px-2 flex flex-col justify-between">
<CountryFlag country={ userCountry } className="mr-2" label />
<div className="color-gray-medium flex items-center">
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ capitalize(userBrowser) } popupProps={{ inverted: true, size: "tiny" }} />
</span>
<div className="mx-2 text-4xl">·</div>
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ capitalize(userOs) } popupProps={{ inverted: true, size: "tiny" }} />
</span>
<div className="mx-2 text-4xl">·</div>
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ capitalize(userDeviceType) } popupProps={{ inverted: true, size: "tiny" }} />
</span>
</div>
</div>
{ isSessions && (
<div style={{ width: "10%"}} className="self-center px-2 flex items-center">
<ErrorBars count={issueTypes.length} />
</div>
)}
</div>
<div className="flex items-center">
{ isAssist && showActive && (
<Label success className={cn("bg-green color-white text-right mr-4", { 'opacity-0' : !active})}>
<span className="color-white">ACTIVE</span>
</Label>
)}
<div className={ stl.playLink } id="play-button" data-viewed={ viewed }>
{ isSessions && (
<div className="mr-4 flex-shrink-0 w-24">
{ isLastPlayed && (
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
<span className="color-gray-medium text-xs" style={{ whiteSpace: 'nowrap'}}>LAST PLAYED</span>
</Label>
)}
</div>
)}
<PlayLink
isAssist={isAssist}
sessionId={sessionId}
viewed={viewed}
/>
</div>
</div>
</div>
{ _metaList.length > 0 && (
<SessionMetaList className="mt-4" metaList={_metaList} />
)}
</div>
);
}
}

View file

@ -1,90 +1,112 @@
import React from 'react';
import { connect } from 'react-redux';
import React from 'react'
import cn from 'classnames';
import {
Link,
Icon,
import {
CountryFlag,
Avatar,
TextEllipsis,
Label,
} from 'UI';
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
import { session as sessionRoute, liveSession as liveSessionRoute, withSiteId } from 'App/routes';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { durationFormatted, formatTimeOrDate } from 'App/date';
import stl from './sessionItem.css';
import Counter from './Counter'
import { withRouter } from 'react-router-dom';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import SessionMetaList from './SessionMetaList';
import PlayLink from './PlayLink';
import ErrorBars from './ErrorBars';
import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from "App/routes";
import { capitalize } from 'App/utils';
import { SKIP_TO_ISSUE, TIMEZONE, DURATION_FILTER } from 'App/constants/storageKeys'
const ASSIST_ROUTE = assistRoute();
const ASSIST_LIVE_SESSION = liveSession()
const SESSIONS_ROUTE = sessionsRoute();
// @connect(state => ({
// timezone: state.getIn(['sessions', 'timezone']),
// siteId: state.getIn([ 'site', 'siteId' ]),
// }), { toggleFavorite, setSessionPath })
// @withRouter
function SessionItem(props) {
// render() {
const {
session: {
sessionId,
userBrowser,
userOs,
userId,
userAnonymousId,
userDisplayName,
userCountry,
startedAt,
duration,
eventsCount,
errorsCount,
pagesCount,
viewed,
favorite,
userDeviceType,
userUuid,
userNumericHash,
live,
metadata,
userSessionsCount,
issueTypes,
active,
},
timezone,
onUserClick = () => null,
hasUserFilter = false,
disableUser = false,
metaList = [],
showActive = false,
lastPlayedSessionId,
} = props;
const formattedDuration = durationFormatted(duration);
const hasUserId = userId || userAnonymousId;
const isSessions = isRoute(SESSIONS_ROUTE, props.location.pathname);
const isAssist = isRoute(ASSIST_ROUTE, props.location.pathname) || isRoute(ASSIST_LIVE_SESSION, props.location.pathname);
const isLastPlayed = lastPlayedSessionId === sessionId;
interface Props {
session: {
sessionId: string;
userBrowser: string;
userOs: string;
userId: string;
userAnonymousId: string;
userDisplayName: string;
userCountry: string;
startedAt: number;
duration: string;
eventsCount: number;
errorsCount: number;
pagesCount: number;
viewed: boolean;
favorite: boolean;
userDeviceType: string;
userUuid: string;
userNumericHash: number;
live: boolean
metadata: Record<string, any>;
userSessionsCount: number
issueTypes: [];
active: boolean;
onUserClick: (userId: string, userAnonymousId: string) => null;
hasUserFilter: boolean;
disableUser: boolean;
metaList: Array<any>;
showActive: boolean;
lastPlayedSessionId: string;
},
}
const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => {
const value = metadata[key];
return { label: key, value };
});
function SessionItem(props: RouteComponentProps<Props>) {
const { settingsStore } = useStore();
const { timezone } = settingsStore.sessionSettings;
return (
<div className={ cn(stl.sessionItem, "flex flex-col bg-white p-3 mb-3") } id="session-item" >
const {
session: {
sessionId,
userBrowser,
userOs,
userId,
userAnonymousId,
userDisplayName,
userCountry,
startedAt,
duration,
eventsCount,
viewed,
userDeviceType,
userNumericHash,
live,
metadata,
issueTypes,
active,
},
onUserClick = () => null,
hasUserFilter = false,
disableUser = false,
metaList = [],
showActive = false,
lastPlayedSessionId,
} = props;
const formattedDuration = durationFormatted(duration);
const hasUserId = userId || userAnonymousId;
const isSessions = isRoute(SESSIONS_ROUTE, props.location.pathname);
const isAssist = isRoute(ASSIST_ROUTE, props.location.pathname) || isRoute(ASSIST_LIVE_SESSION, props.location.pathname);
const isLastPlayed = lastPlayedSessionId === sessionId;
const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => {
const value = metadata[key];
return { label: key, value };
});
return (
<div className={ cn(stl.sessionItem, "flex flex-col p-3 mb-3") } id="session-item" >
<div className="flex items-start">
<div className={ cn('flex items-center w-full')}>
<div className="flex items-center pr-2" style={{ width: "30%"}}>
<div><Avatar seed={ userNumericHash } isAssist={isAssist} /></div>
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center">
<div
className={cn('text-lg', {'color-teal cursor-pointer': !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
className={cn('text-lg', {'color-teal cursor-pointer': !disableUser && hasUserId, [stl.userName]: !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
>
<TextEllipsis text={userDisplayName} maxWidth={200} popupProps={{ inverted: true, size: 'tiny' }} />
@ -111,7 +133,7 @@ function SessionItem(props) {
<div className="color-gray-medium flex items-center">
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ capitalize(userBrowser) } popupProps={{ inverted: true, size: "tiny" }} />
</span>
</span>
<div className="mx-2 text-4xl">·</div>
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ capitalize(userOs) } popupProps={{ inverted: true, size: "tiny" }} />
@ -145,9 +167,11 @@ function SessionItem(props) {
)}
</div>
)}
<Link to={ isAssist ? liveSessionRoute(sessionId) : sessionRoute(sessionId) }>
<Icon name={ !viewed && !isAssist ? 'play-fill' : 'play-circle-light' } size="42" color={isAssist ? "tealx" : "teal"} />
</Link>
<PlayLink
isAssist={isAssist}
sessionId={sessionId}
viewed={viewed}
/>
</div>
</div>
</div>
@ -155,10 +179,7 @@ function SessionItem(props) {
<SessionMetaList className="mt-4" metaList={_metaList} />
)}
</div>
);
}
)
}
export default connect(state => ({
timezone: localStorage.getItem(TIMEZONE) || '',
siteId: state.getIn([ 'site', 'siteId' ]),
}), { toggleFavorite, setSessionPath })(withRouter(SessionItem));
export default withRouter(observer(SessionItem))

View file

@ -1,11 +1,9 @@
@import 'mixins.css';
.sessionItem {
background-color: #fff;
@mixin defaultHover;
user-select: none;
border-radius: 3px;
border: solid thin #EEEEEE;
transition: all 0.4s;
& .favorite {
opacity: 0;
@ -15,6 +13,10 @@
}
&:hover {
background-color: $active-blue;
border: solid thin $active-blue-border;
transition: all 0.2s;
& .playLink {
transition: all 0.4s;
opacity: 1;
@ -103,7 +105,11 @@
letter-spacing: 1px;
}
.userName:hover {
text-decoration: underline;
text-decoration-color: $teal;
}
.userName {
text-decoration: none;
&:hover {
text-decoration: underline;
text-decoration-color: $teal;
}
}

View file

@ -5,24 +5,49 @@ import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
const str = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)
const d = str && str[1] || 'UTC';
const localMachineFormat = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)
const middlePoint = localMachineFormat && localMachineFormat[1].length - 2
const readableLocalTimezone = localMachineFormat && middlePoint ?
`${localMachineFormat[1].substring(0, 3)} ${localMachineFormat[1].substring(3, middlePoint)}:${localMachineFormat[1].substring(middlePoint)}`
: null
interface TimezonesDropdownValue {
label: string;
value: string;
}
type TimezonesDropdown = TimezonesDropdownValue[]
const timezoneOptions = [
{ label: readableLocalTimezone, value: 'local' },
{ label: 'UTC', value: 'UTC' },
]
const generateGMTZones = (): TimezonesDropdown => {
const timezones: TimezonesDropdown = []
const positiveNumbers = [...Array(12).keys()];
const negativeNumbers = [...Array(12).keys()].reverse();
negativeNumbers.pop(); // remove trailing zero since we have on in positive numbers array
const combinedArray = [...negativeNumbers, ...positiveNumbers];
for (let i = 0; i < 23; i++) {
let symbol = i < 11 ? '-' : '+';
let isUTC = i === 11
let prefix = isUTC ? 'UTC / GMT' : 'GMT';
let value = String(combinedArray[i]).padStart(2, '0');
let tz = `${prefix} ${symbol}${String(combinedArray[i]).padStart(2, '0')}:00`
let dropdownValue = `UTC${symbol}${value}`
timezones.push({ label: tz, value: isUTC ? 'UTC' : dropdownValue })
}
timezones.splice(17, 0, { label: 'GMT +05:30', value: 'GMT +05:30' })
return timezones
}
const timezoneOptions: TimezonesDropdown = [...generateGMTZones()]
function DefaultTimezone(props) {
const [changed, setChanged] = React.useState(false);
const { settingsStore } = useStore();
const [timezone, setTimezone] = React.useState(settingsStore.sessionSettings.timezone);
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
const sessionSettings = useObserver(() => settingsStore.sessionSettings);
useEffect(() => {
if (!timezone) setTimezone('local');
}, []);
return (
<>

View file

@ -20,7 +20,7 @@ export const durationFormatted = (duration: Duration):string => {
export function durationFromMsFormatted(ms: number): string {
return durationFormatted(Duration.fromMillis(ms || 0));
}
}
export const durationFormattedFull = (duration: Duration): string => {
if (duration.as('minutes') < 1) { // show in seconds
@ -35,7 +35,7 @@ export const durationFormattedFull = (duration: Duration): string => {
} else if (duration.as('months') < 1) { // show in days and hours
let d = duration.toFormat('d');
duration = d + (d > 1 ? ' days' : ' day');
} else {
} else {
let d = Math.trunc(duration.as('months'));
duration = d + (d > 1 ? ' months' : ' month');;
}
@ -49,7 +49,7 @@ export const msToSec = (ms:number): number => Math.round(ms / 1000);
export const diffFromNowString = (ts:number): string =>
durationFormattedFull(DateTime.fromMillis(Date.now()).diff(DateTime.fromMillis(ts)));
export const diffFromNowShortString = (ts: number): string =>
export const diffFromNowShortString = (ts: number): string =>
durationFormatted(DateTime.fromMillis(Date.now()).diff(DateTime.fromMillis(ts)));
export const getDateFromMill = date =>
@ -69,11 +69,19 @@ export function formatDateTimeDefault(timestamp: number): string {
return isToday(date) ? 'Today' : date.toFormat('LLL dd, yyyy') + ', ' + date.toFormat('hh:mm a')
}
/**
* Formats timestamps into readable date
* @param {Number} timestamp
* @param {String} timezone fixed offset like UTC+6
* @returns {String} formatted date (or time if its today)
*/
export function formatTimeOrDate(timestamp: number, timezone: string): string {
var date = DateTime.fromMillis(timestamp)
if (timezone === 'UTC')
date = date.toUTC();
if (timezone) {
if (timezone === 'UTC') date = date.toUTC();
date = date.setZone(timezone)
}
return isToday(date) ? date.toFormat('hh:mm a') : date.toFormat('LLL dd, yyyy, hh:mm a');
}