fix(ui): fixed sessionitem and timezone dropdown connection to mobx
This commit is contained in:
parent
0d00cf0349
commit
be13ff5f7a
6 changed files with 155 additions and 257 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue