feat(ui) - assist ui - wip
This commit is contained in:
parent
40b88446d1
commit
250eaf1eb6
24 changed files with 325 additions and 140 deletions
|
|
@ -2,8 +2,10 @@ import React from 'react';
|
|||
import LiveSessionList from 'Shared/LiveSessionList';
|
||||
import LiveSessionSearch from 'Shared/LiveSessionSearch';
|
||||
import cn from 'classnames'
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
|
||||
export default function Assist() {
|
||||
// @withPageTitle("Assist - OpenReplay")
|
||||
function Assist() {
|
||||
return (
|
||||
<div className="page-margin container-90 flex relative">
|
||||
<div className="flex-1 flex">
|
||||
|
|
@ -18,3 +20,5 @@ export default function Assist() {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withPageTitle("Assist - OpenReplay")(Assist);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { Popup, Icon } from 'UI'
|
||||
import { Popup, Icon, IconButton } from 'UI'
|
||||
import { connect } from 'react-redux'
|
||||
import cn from 'classnames'
|
||||
import { toggleChatWindow } from 'Duck/sessions';
|
||||
|
|
@ -77,9 +77,28 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
|
|||
|
||||
const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting
|
||||
const cannotCall = (peerConnectionStatus !== ConnectionStatus.Connected) || (isEnterprise && !hasPermission)
|
||||
const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={
|
||||
cn(
|
||||
'cursor-pointer p-2 mr-2 flex items-center',
|
||||
)
|
||||
}
|
||||
onClick={ requestReleaseRemoteControl }
|
||||
role="button"
|
||||
>
|
||||
{/* <Icon
|
||||
name="remote-control"
|
||||
size="20"
|
||||
color={ remoteControlStatus === RemoteControlStatus.Enabled ? "green" : "gray-darkest"}
|
||||
/>
|
||||
<span className={cn("ml-2", { 'color-green' : remoteControlStatus === RemoteControlStatus.Enabled })}>{ 'Remote Control' }</span> */}
|
||||
<IconButton label={`${remoteActive ? 'Stop ' : ''} Remote Control`} icon="remote-control" primaryText redText={remoteActive} />
|
||||
</div>
|
||||
<div className="divider" />
|
||||
<Popup
|
||||
trigger={
|
||||
<div
|
||||
|
|
@ -92,12 +111,13 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
|
|||
onClick={ onCall ? callObject?.end : confirmCall}
|
||||
role="button"
|
||||
>
|
||||
<Icon
|
||||
{/* <Icon
|
||||
name="headset"
|
||||
size="20"
|
||||
color={ onCall ? "red" : "gray-darkest" }
|
||||
/>
|
||||
<span className={cn("ml-2", { 'color-red' : onCall })}>{ onCall ? 'End Call' : 'Call' }</span>
|
||||
<span className={cn("ml-2", { 'color-red' : onCall })}>{ onCall ? 'End Call' : 'Call' }</span> */}
|
||||
<IconButton size="small" primary={!onCall} red={onCall} label="Call" icon="headset" />
|
||||
</div>
|
||||
}
|
||||
content={ cannotCall ? "You don’t have the permissions to perform this action." : `Call ${userId ? userId : 'User'}` }
|
||||
|
|
@ -105,22 +125,7 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
|
|||
inverted
|
||||
position="top right"
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
cn(
|
||||
'cursor-pointer p-2 mr-2 flex items-center',
|
||||
)
|
||||
}
|
||||
onClick={ requestReleaseRemoteControl }
|
||||
role="button"
|
||||
>
|
||||
<Icon
|
||||
name="remote-control"
|
||||
size="20"
|
||||
color={ remoteControlStatus === RemoteControlStatus.Enabled ? "green" : "gray-darkest"}
|
||||
/>
|
||||
<span className={cn("ml-2", { 'color-green' : remoteControlStatus === RemoteControlStatus.Enabled })}>{ 'Remote Control' }</span>
|
||||
</div>
|
||||
|
||||
<div className="fixed ml-3 left-0 top-0" style={{ zIndex: 999 }}>
|
||||
{ onCall && callObject && <ChatWindow endCall={callObject.end} userId={userId} incomeStream={incomeStream} localStream={localStream} /> }
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,16 +15,15 @@ const AssistTabs = (props: Props) => {
|
|||
<div className="flex items-center">
|
||||
{props.userId && (
|
||||
<>
|
||||
<div className="flex items-center mr-3">
|
||||
<Icon name="user-alt" color="gray-darkest" />
|
||||
<div className="ml-2">{props.userId}</div>
|
||||
</div>
|
||||
<div
|
||||
className={stl.btnLink}
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
>
|
||||
More Live Sessions
|
||||
</div>
|
||||
<span className="mx-3 color-gray-medium">by</span>
|
||||
<div className="flex items-center">
|
||||
<Icon name="user-alt" color="gray-darkest" />
|
||||
<div className="ml-2">{props.userId}</div>
|
||||
All Active Sessions
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -102,13 +102,13 @@ export default class BugFinder extends React.PureComponent {
|
|||
// };
|
||||
// });
|
||||
// // TODO should cache the response
|
||||
props.fetchIntegrationVariables().then(() => {
|
||||
defaultFilters[5] = {
|
||||
category: 'Metadata',
|
||||
type: 'custom',
|
||||
keys: this.props.variables.map(({ key }) => ({ type: 'METADATA', key, label: key, icon: 'filters/metadata', isFilter: true })).toJS()
|
||||
};
|
||||
});
|
||||
// props.fetchIntegrationVariables().then(() => {
|
||||
// defaultFilters[5] = {
|
||||
// category: 'Metadata',
|
||||
// type: 'custom',
|
||||
// keys: this.props.variables.map(({ key }) => ({ type: 'METADATA', key, label: key, icon: 'filters/metadata', isFilter: true })).toJS()
|
||||
// };
|
||||
// });
|
||||
|
||||
props.fetchSessions();
|
||||
props.resetFunnel();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import styles from './siteDropdown.css';
|
|||
import cn from 'classnames';
|
||||
import NewSiteForm from '../Client/Sites/NewSiteForm';
|
||||
import { clearSearch } from 'Duck/search';
|
||||
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
|
||||
|
||||
@withRouter
|
||||
@connect(state => ({
|
||||
|
|
@ -21,6 +22,7 @@ import { clearSearch } from 'Duck/search';
|
|||
pushNewSite,
|
||||
init,
|
||||
clearSearch,
|
||||
fetchIntegrationVariables,
|
||||
})
|
||||
export default class SiteDropdown extends React.PureComponent {
|
||||
state = { showProductModal: false }
|
||||
|
|
@ -37,6 +39,7 @@ export default class SiteDropdown extends React.PureComponent {
|
|||
switchSite = (siteId) => {
|
||||
this.props.setSiteId(siteId);
|
||||
this.props.clearSearch();
|
||||
this.props.fetchIntegrationVariables();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
|||
|
|
@ -3,19 +3,21 @@ import { withRouter } from 'react-router-dom';
|
|||
import { browserIcon, osIcon, deviceTypeIcon } from 'App/iconNames';
|
||||
import { formatTimeOrDate } from 'App/date';
|
||||
import { sessions as sessionsRoute, withSiteId } from 'App/routes';
|
||||
import { Icon, CountryFlag, IconButton, BackLink } from 'UI';
|
||||
import { Icon, CountryFlag, IconButton, BackLink, Popup } from 'UI';
|
||||
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
|
||||
import cn from 'classnames';
|
||||
import { connectPlayer } from 'Player';
|
||||
import HeaderInfo from './HeaderInfo';
|
||||
import SharePopup from '../shared/SharePopup/SharePopup';
|
||||
import { fetchList as fetchListIntegration } from 'Duck/integrations/actions';
|
||||
import { countries } from 'App/constants';
|
||||
|
||||
import stl from './playerBlockHeader.css';
|
||||
import Issues from './Issues/Issues';
|
||||
import Autoplay from './Autoplay';
|
||||
import AssistActions from '../Assist/components/AssistActions';
|
||||
import AssistTabs from '../Assist/components/AssistTabs';
|
||||
import SessionInfoItem from './SessionInfoItem'
|
||||
|
||||
const SESSIONS_ROUTE = sessionsRoute();
|
||||
|
||||
|
|
@ -53,11 +55,13 @@ export default class PlayerBlockHeader extends React.PureComponent {
|
|||
this.props.fetchListIntegration('issues')
|
||||
}
|
||||
|
||||
getDimension = (width, height) => (
|
||||
<div className="flex items-center">
|
||||
{ width || 'x' } <Icon name="close" size="12" className="mx-1" /> { height || 'x' }
|
||||
</div>
|
||||
);
|
||||
getDimension = (width, height) => {
|
||||
return width && height ? (
|
||||
<div className="flex items-center">
|
||||
{ width || 'x' } <Icon name="close" size="12" className="mx-1" /> { height || 'x' }
|
||||
</div>
|
||||
) : <span className="text-sm">Not Available</span>;
|
||||
}
|
||||
|
||||
backHandler = () => {
|
||||
const { history, siteId, sessionPath } = this.props;
|
||||
|
|
@ -85,6 +89,7 @@ export default class PlayerBlockHeader extends React.PureComponent {
|
|||
startedAt,
|
||||
userBrowser,
|
||||
userOs,
|
||||
userOsVersion,
|
||||
userDevice,
|
||||
userBrowserVersion,
|
||||
userDeviceType,
|
||||
|
|
@ -102,12 +107,13 @@ export default class PlayerBlockHeader extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div className={ cn(stl.header, "flex justify-between", { "hidden" : fullscreen}) }>
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-full items-center">
|
||||
<BackLink onClick={this.backHandler} label="Back" />
|
||||
|
||||
<div className={ stl.divider } />
|
||||
{ _live && <AssistTabs userId={userId} />}
|
||||
|
||||
<div className="mx-4 flex items-center">
|
||||
{/* <div className="mx-4 flex items-center">
|
||||
<CountryFlag country={ userCountry } />
|
||||
<div className="ml-2 font-normal color-gray-dark mt-1 text-sm">
|
||||
{ formatTimeOrDate(startedAt) } <span>{ this.props.local === 'UTC' ? 'UTC' : ''}</span>
|
||||
|
|
@ -117,15 +123,33 @@ export default class PlayerBlockHeader extends React.PureComponent {
|
|||
<HeaderInfo icon={ browserIcon(userBrowser) } label={ `v${ userBrowserVersion }` } />
|
||||
<HeaderInfo icon={ deviceTypeIcon(userDeviceType) } label={ capitalise(userDevice) } />
|
||||
<HeaderInfo icon="expand-wide" label={ this.getDimension(width, height) } />
|
||||
<HeaderInfo icon={ osIcon(userOs) } label={ userOs } />
|
||||
<HeaderInfo icon={ osIcon(userOs) } label={ userOs } /> */}
|
||||
|
||||
<div className='ml-auto flex items-center'>
|
||||
<div className={ stl.divider } />
|
||||
<Popup
|
||||
trigger={(
|
||||
<IconButton icon="info-circle" primaryText label="More Info" disabled={disabled} />
|
||||
)}
|
||||
content={(
|
||||
<div className=''>
|
||||
<SessionInfoItem comp={<CountryFlag country={ userCountry } />} label={countries[userCountry]} value={ formatTimeOrDate(startedAt) } />
|
||||
<SessionInfoItem icon={browserIcon(userBrowser)} label={userBrowser} value={ `v${ userBrowserVersion }` } />
|
||||
<SessionInfoItem icon={osIcon(userOs)} label={userOs} value={ userOsVersion } />
|
||||
<SessionInfoItem icon={deviceTypeIcon(userDeviceType)} label={userDeviceType} value={ this.getDimension(width, height) } isLast />
|
||||
</div>
|
||||
)}
|
||||
on="click"
|
||||
position="top center"
|
||||
hideOnScroll
|
||||
/>
|
||||
<div className={ stl.divider } />
|
||||
|
||||
{ live && hasSessionsPath && (
|
||||
<div className={stl.liveSwitchButton} onClick={() => this.props.setSessionPath('')}>
|
||||
This Session is Now Continuing Live
|
||||
</div>
|
||||
)}
|
||||
{ _live && <AssistTabs userId={userId} />}
|
||||
{ _live && <AssistActions isLive userId={userId} /> }
|
||||
{ !_live && (
|
||||
<>
|
||||
|
|
@ -164,3 +188,4 @@ export default class PlayerBlockHeader extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import { Icon } from 'UI'
|
||||
import cn from 'classnames'
|
||||
|
||||
interface Props {
|
||||
label: string,
|
||||
icon?: string,
|
||||
comp?: React.ReactNode,
|
||||
value: string,
|
||||
isLast?: boolean,
|
||||
}
|
||||
export default function SessionInfoItem(props: Props) {
|
||||
const { label, icon, value, comp, isLast = false } = props
|
||||
return (
|
||||
<div className={cn("flex items-center w-full py-2", {'border-b' : !isLast})}>
|
||||
<div className="px-2 capitalize" style={{ width: '30px' }}>
|
||||
{ icon && <Icon name={icon} size="16" /> }
|
||||
{ comp && comp }
|
||||
</div>
|
||||
<div className="px-2 whitespace-nowrap capitalize" style={{ minWidth: '130px' }}>{label}</div>
|
||||
<div className="color-gray-medium px-2" style={{ minWidth: '100px' }}>{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionInfoItem';
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
.header {
|
||||
height: 50px;
|
||||
border-bottom: solid thin $gray-light;
|
||||
padding: 10px 15px;
|
||||
padding: 0px 15px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
height: 49px;
|
||||
margin: 0 15px;
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
.bar {
|
||||
height: 2px;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import stl from './ErrorBars.css'
|
||||
|
||||
const GOOD = 'Good'
|
||||
const LESS_CRITICAL = 'Less Critical'
|
||||
|
|
@ -16,21 +17,23 @@ interface Props {
|
|||
}
|
||||
export default function ErrorBars(props: Props) {
|
||||
const { count = 2 } = props
|
||||
const state = React.useCallback(() => getErrorState(count), [count])()
|
||||
const state = React.useMemo(() => getErrorState(count), [count])
|
||||
const showSecondBar = (state === GOOD || state === LESS_CRITICAL || state === CRITICAL)
|
||||
const showThirdBar = (state === GOOD || state === CRITICAL);
|
||||
const bgColor = { 'bg-red' : state === CRITICAL, 'bg-green' : state === GOOD, 'bg-red2' : state === LESS_CRITICAL }
|
||||
return (
|
||||
<div className="relative" style={{ width: '80px' }}>
|
||||
<div className="relative" style={{ width: '60px' }}>
|
||||
<div className="grid grid-cols-3 gap-1 absolute inset-0" style={{ opacity: '1'}}>
|
||||
<div className={cn("h-1 rounded-tl rounded-bl", bgColor)}></div>
|
||||
{ (state === GOOD || state === LESS_CRITICAL || state === CRITICAL) && <div className={cn("h-1 rounded-tl rounded-bl", bgColor)}></div> }
|
||||
{ (state === GOOD || state === CRITICAL) && <div className={cn("h-1 rounded-tl rounded-bl", bgColor)}></div> }
|
||||
<div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div>
|
||||
{ showSecondBar && <div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div> }
|
||||
{ showThirdBar && <div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div> }
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1" style={{ opacity: '0.3'}}>
|
||||
<div className={cn("h-1 rounded-tl rounded-bl", bgColor)}></div>
|
||||
<div className={cn("h-1", bgColor)}></div>
|
||||
<div className={cn("h-1 rounded-tr rounded-br", bgColor)}></div>
|
||||
<div className={cn("rounded-tl rounded-bl", bgColor, stl.bar)}></div>
|
||||
<div className={cn(bgColor, stl.bar)}></div>
|
||||
<div className={cn("rounded-tr rounded-br", bgColor, stl.bar)}></div>
|
||||
</div>
|
||||
<div className="mt-2">{state}</div>
|
||||
<div className="mt-1 color-gray-medium text-sm">{state}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { TextEllipsis } from 'UI'
|
||||
|
||||
interface Props {
|
||||
className?: string,
|
||||
label: string,
|
||||
value?: string,
|
||||
}
|
||||
export default function MetaItem(props: Props) {
|
||||
const { className = '', label, value } = props
|
||||
return (
|
||||
<div className={cn("flex items-center rounded", className)}>
|
||||
<span className="rounded-tl rounded-bl bg-gray-light-shade px-2 color-gray-medium capitalize" style={{ maxWidth: "80px"}}>
|
||||
<TextEllipsis text={label} className="p-0" popupProps={{ size: 'small' }} />
|
||||
</span>
|
||||
<span className="rounded-tr rounded-br bg-gray-lightest px-2 color-gray-dark capitalize" style={{ maxWidth: "100px"}}>
|
||||
<TextEllipsis text={value} className="p-0" popupProps={{ size: 'small' }} />
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetaItem';
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react'
|
||||
import { Popup } from 'UI'
|
||||
import MetaItem from '../MetaItem'
|
||||
|
||||
interface Props {
|
||||
list: any[],
|
||||
maxLength: number,
|
||||
}
|
||||
export default function MetaMoreButton(props: Props) {
|
||||
const { list, maxLength } = props
|
||||
return (
|
||||
<Popup
|
||||
trigger={ (
|
||||
<div className="flex items-center">
|
||||
<span className="rounded bg-active-blue color-teal px-2 color-gray-dark cursor-pointer">
|
||||
+{list.length - maxLength} More
|
||||
</span>
|
||||
</div>
|
||||
) }
|
||||
content={
|
||||
<div className="flex flex-col">
|
||||
{list.slice(maxLength).map(({ label, value }, index) => (
|
||||
<MetaItem key={index} label={label} value={value} className="mb-2" />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
on="click"
|
||||
position="top right"
|
||||
hideOnScroll
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetaMoreButton';
|
||||
|
|
@ -20,13 +20,15 @@ import Counter from './Counter'
|
|||
import { withRouter } from 'react-router-dom';
|
||||
import SessionMetaList from './SessionMetaList';
|
||||
import ErrorBars from './ErrorBars';
|
||||
import { assist as assistRoute, isRoute } from "App/routes";
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
|
||||
const Label = ({ label = '', color = 'color-gray-medium'}) => (
|
||||
<div className={ cn('font-light text-sm', color)}>{label}</div>
|
||||
)
|
||||
@connect(state => ({
|
||||
timezone: state.getIn(['sessions', 'timezone']),
|
||||
isAssist: state.getIn(['sessions', 'activeTab']).type === 'live',
|
||||
siteId: state.getIn([ 'user', 'siteId' ]),
|
||||
}), { toggleFavorite, setSessionPath })
|
||||
@withRouter
|
||||
|
|
@ -52,7 +54,8 @@ export default class SessionItem extends React.PureComponent {
|
|||
userDeviceType,
|
||||
userUuid,
|
||||
userNumericHash,
|
||||
live
|
||||
live,
|
||||
metadata,
|
||||
},
|
||||
timezone,
|
||||
onUserClick = () => null,
|
||||
|
|
@ -61,71 +64,89 @@ export default class SessionItem extends React.PureComponent {
|
|||
} = this.props;
|
||||
const formattedDuration = durationFormatted(duration);
|
||||
const hasUserId = userId || userAnonymousId;
|
||||
const isAssist = isRoute(ASSIST_ROUTE, this.props.location.pathname);
|
||||
console.log('metadata', metadata);
|
||||
|
||||
const _metaList = Object.keys(metadata).map(key => {
|
||||
const value = metadata[key];
|
||||
return { label: key, value };
|
||||
});
|
||||
|
||||
console.log('SessionItem', _metaList);
|
||||
|
||||
return (
|
||||
<div className={ cn(stl.sessionItem, "flex flex-col bg-white 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" style={{ width: "40%"}}>
|
||||
<div><Avatar seed={ userNumericHash } /></div>
|
||||
<div className="flex flex-col overflow-hidden color-gray-medium ml-3">
|
||||
<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"> */}
|
||||
<div style={{ height: "38px" }} className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between">
|
||||
<div
|
||||
className={cn({'color-teal cursor-pointer': !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
|
||||
onClick={() => (!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)}
|
||||
>
|
||||
{userDisplayName}
|
||||
</div>
|
||||
<div className="color-gray-medium">30 Sessions</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: "20%"}}>
|
||||
<div>{formatTimeOrDate(startedAt, timezone) }</div>
|
||||
<div className="flex items-center color-gray-medium">
|
||||
{!live && (
|
||||
<div className="color-gray-medium">
|
||||
<span className="mr-1">{ eventsCount }</span>
|
||||
<span>{ eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' }</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="mx-1">-</span>
|
||||
<div>{ live ? <Counter startTime={startedAt} /> : formattedDuration }</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: "20%"}}>
|
||||
<div className="">
|
||||
<CountryFlag country={ userCountry } className="mr-6" />
|
||||
<div className="color-gray-medium">
|
||||
<span>{userBrowser}</span> -
|
||||
<span>{userOs}</span> -
|
||||
<span>{userDeviceType}</span>
|
||||
<div
|
||||
className="color-gray-medium text-dotted-underline cursor-pointer"
|
||||
onClick={() => (!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)}
|
||||
>
|
||||
30 Sessions
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: "10%"}} className="self-center">
|
||||
<ErrorBars count={errorsCount} />
|
||||
<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: "40%", height: "38px" }} className="px-2 flex flex-col justify-between">
|
||||
{/* <div className="flex flex-col"> */}
|
||||
<CountryFlag country={ userCountry } className="mr-2" label />
|
||||
<div className="color-gray-medium flex items-center">
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ userBrowser } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
<div className="mx-2 text-4xl">·</div>
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ userOs } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
<div className="mx-2 text-4xl">·</div>
|
||||
<span className="capitalize" style={{ maxWidth: '70px'}}>
|
||||
<TextEllipsis text={ userDeviceType } popupProps={{ inverted: true, size: "tiny" }} />
|
||||
</span>
|
||||
</div>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
{ !isAssist && (
|
||||
<div style={{ width: "10%"}} className="self-center px-2 flex items-center">
|
||||
<ErrorBars count={errorsCount} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
{/* { live && <LiveTag isLive={true} /> } */}
|
||||
<div className={ cn(stl.iconDetails, stl.favorite, 'px-4') } data-favourite={favorite} >
|
||||
<Bookmark sessionId={sessionId} favorite={favorite} />
|
||||
</div>
|
||||
|
||||
<div className={ stl.playLink } id="play-button" data-viewed={ viewed }>
|
||||
<Link to={ sessionRoute(sessionId) }>
|
||||
<Icon name={ viewed ? 'play-fill' : 'play-circle-light' } size="30" color="teal" />
|
||||
<Icon name={ viewed ? 'play-fill' : 'play-circle-light' } size="30" color={isAssist ? "tealx" : "teal"} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SessionMetaList className="pt-3" metaList={[
|
||||
{ label: 'Pages', value: pagesCount },
|
||||
{ label: 'Errors', value: errorsCount },
|
||||
{ label: 'Events', value: eventsCount },
|
||||
{ label: 'Events', value: eventsCount },
|
||||
{ label: 'Events', value: eventsCount },
|
||||
]} />
|
||||
{ isAssist && (
|
||||
<SessionMetaList className="mt-4" metaList={_metaList} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react'
|
||||
import { Popup } from 'UI'
|
||||
import cn from 'classnames'
|
||||
import MetaItem from '../MetaItem';
|
||||
import MetaMoreButton from '../MetaMoreButton';
|
||||
|
||||
interface Props {
|
||||
className?: string,
|
||||
|
|
@ -12,36 +14,11 @@ export default function SessionMetaList(props: Props) {
|
|||
return (
|
||||
<div className={cn("text-sm flex items-start", className)}>
|
||||
{metaList.slice(0, MAX_LENGTH).map(({ label, value }, index) => (
|
||||
<div key={index} className="flex items-center rounded mr-3">
|
||||
<span className="rounded-tl rounded-bl bg-gray-light-shade px-2 color-gray-medium">{label}</span>
|
||||
<span className="rounded-tr rounded-br bg-gray-lightest px-2 color-gray-dark">{value}</span>
|
||||
</div>
|
||||
<MetaItem key={index} label={label} value={''+value} className="mr-3" />
|
||||
))}
|
||||
|
||||
{metaList.length > MAX_LENGTH && (
|
||||
<Popup
|
||||
trigger={ (
|
||||
<div className="flex items-center">
|
||||
<span className="rounded bg-active-blue color-teal px-2 color-gray-dark cursor-pointer">
|
||||
+{metaList.length - MAX_LENGTH} More
|
||||
</span>
|
||||
</div>
|
||||
) }
|
||||
content={
|
||||
<div className="flex flex-col">
|
||||
{metaList.slice(MAX_LENGTH).map(({ label, value }, index) => (
|
||||
<div key={index} className="flex items-center rounded mb-2">
|
||||
<span className="rounded-tl rounded-bl bg-gray-light-shade px-2 color-gray-medium">{label}</span>
|
||||
<span className="rounded-tr rounded-br bg-gray-lightest px-2 color-gray-dark">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
on="click"
|
||||
position="top right"
|
||||
// className={ styles.popup }
|
||||
hideOnScroll
|
||||
/>
|
||||
<MetaMoreButton list={metaList} maxLength={MAX_LENGTH} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,14 +11,15 @@ const ICON_LIST = ['icn_chameleon', 'icn_fox', 'icn_gorilla', 'icn_hippo', 'icn_
|
|||
'icn_wild1', 'icn_wild_bore']
|
||||
|
||||
|
||||
const Avatar = ({ className, width = "38px", height = "38px", iconSize = 26, seed }) => {
|
||||
const Avatar = ({ isAssist = false, className, width = "38px", height = "38px", iconSize = 26, seed }) => {
|
||||
var iconName = avatarIconName(seed);
|
||||
return (
|
||||
<div
|
||||
className={ cn(stl.wrapper, "p-2 border flex items-center justify-center rounded-full")}
|
||||
className={ cn(stl.wrapper, "p-2 border flex items-center justify-center rounded-full relative")}
|
||||
style={{ width, height }}
|
||||
>
|
||||
<Icon name={iconName} size={iconSize} color="tealx"/>
|
||||
{isAssist && <div className="w-2 h-2 bg-green rounded-full absolute right-0 bottom-0" style={{ marginRight: '3px', marginBottom: '3px'}} /> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,22 +3,26 @@ import { countries } from 'App/constants';
|
|||
import { Popup } from 'UI';
|
||||
import stl from './countryFlag.css';
|
||||
|
||||
const CountryFlag = ({ country, className }) => {
|
||||
const CountryFlag = React.memo(({ country, className, style = {}, label = false }) => {
|
||||
const knownCountry = !!country && country !== 'UN';
|
||||
const countryFlag = knownCountry ? country.toLowerCase() : '';
|
||||
const countryName = knownCountry ? countries[ country ] : 'Unknown Country';
|
||||
const countryFlag = knownCountry ? country.toLowerCase() : '';
|
||||
const countryName = knownCountry ? countries[ country ] : 'Unknown Country';
|
||||
|
||||
return (
|
||||
<Popup
|
||||
trigger={ knownCountry
|
||||
? <span className={ cn(`flag flag-${ countryFlag }`, className, stl.default) } />
|
||||
: <span className={ className } >{ "N/A" }</span>
|
||||
}
|
||||
content={ countryName }
|
||||
inverted
|
||||
size="tiny"
|
||||
/>
|
||||
<div className="flex items-center" style={style}>
|
||||
<Popup
|
||||
trigger={ knownCountry
|
||||
? <div className={ cn(`flag flag-${ countryFlag }`, className, stl.default) } />
|
||||
: <div className={ cn('text-sm', className) }>{ "N/A" }</div>
|
||||
}
|
||||
content={ countryName }
|
||||
inverted
|
||||
size="tiny"
|
||||
/>
|
||||
{ knownCountry && label && <div className={ stl.label }>{ countryName }</div> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
CountryFlag.displayName = "CountryFlag";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
.default {
|
||||
width: 22px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
.label {
|
||||
line-height: 0 !important;
|
||||
}
|
||||
|
|
@ -9,8 +9,10 @@ const IconButton = React.forwardRef(({
|
|||
onClick,
|
||||
plain = false,
|
||||
shadow = false,
|
||||
red = false,
|
||||
primary = false,
|
||||
primaryText = false,
|
||||
redText = false,
|
||||
outline = false,
|
||||
loading = false,
|
||||
roundedOutline = false,
|
||||
|
|
@ -40,7 +42,9 @@ const IconButton = React.forwardRef(({
|
|||
[ stl.active ]: active,
|
||||
[ stl.shadow ]: shadow,
|
||||
[ stl.primary ]: primary,
|
||||
[ stl.red ]: red,
|
||||
[ stl.primaryText ]: primaryText,
|
||||
[ stl.redText ]: redText,
|
||||
[ stl.outline ]: outline,
|
||||
[ stl.circle ]: circle,
|
||||
[ stl.roundedOutline ]: roundedOutline,
|
||||
|
|
|
|||
|
|
@ -73,11 +73,41 @@
|
|||
fill: white;
|
||||
}
|
||||
|
||||
& svg {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
& .label {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $teal-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&.red {
|
||||
background-color: $red;
|
||||
box-shadow: 0 0 0 1px $red inset !important;
|
||||
|
||||
& .icon {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
& svg {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
& .label {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $red;
|
||||
filter: brightness(90%);
|
||||
}
|
||||
}
|
||||
|
||||
&.outline {
|
||||
box-shadow: 0 0 0 1px $teal inset !important;
|
||||
& .label {
|
||||
|
|
@ -116,4 +146,14 @@
|
|||
|
||||
.primaryText .label {
|
||||
color: $teal !important;
|
||||
}
|
||||
|
||||
.redText {
|
||||
& .label {
|
||||
color: $red !important;
|
||||
}
|
||||
|
||||
& svg {
|
||||
fill: $red;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
.textEllipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
/* display: inline-block; */
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
@ -123,4 +123,15 @@
|
|||
&:hover {
|
||||
background-color: $active-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.text-dotted-underline {
|
||||
text-decoration: underline dotted !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 49px;
|
||||
margin: 0 15px;
|
||||
background-color: $gray-light;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue