feat(ui) - assist ui - wip

This commit is contained in:
Shekar Siri 2022-02-18 15:37:14 +01:00
parent df240bc7c4
commit 40b88446d1
16 changed files with 318 additions and 71 deletions

View file

@ -11,6 +11,7 @@ import UpdatePassword from 'Components/UpdatePassword/UpdatePassword';
import ClientPure from 'Components/Client/Client';
import OnboardingPure from 'Components/Onboarding/Onboarding';
import SessionPure from 'Components/Session/Session';
import AssistPure from 'Components/Assist';
import BugFinderPure from 'Components/BugFinder/BugFinder';
import DashboardPure from 'Components/Dashboard/Dashboard';
import ErrorsPure from 'Components/Errors/Errors';
@ -29,6 +30,7 @@ import { setSessionPath } from 'Duck/sessions';
const BugFinder = withSiteIdUpdater(BugFinderPure);
const Dashboard = withSiteIdUpdater(DashboardPure);
const Session = withSiteIdUpdater(SessionPure);
const Assist = withSiteIdUpdater(AssistPure);
const Client = withSiteIdUpdater(ClientPure);
const Onboarding = withSiteIdUpdater(OnboardingPure);
const Errors = withSiteIdUpdater(ErrorsPure);
@ -39,6 +41,7 @@ const withObTab = routes.withObTab;
const DASHBOARD_PATH = routes.dashboard();
const SESSIONS_PATH = routes.sessions();
const ASSIST_PATH = routes.assist();
const ERRORS_PATH = routes.errors();
const ERROR_PATH = routes.error();
const FUNNEL_PATH = routes.funnel();
@ -145,6 +148,7 @@ class Router extends React.Component {
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
}
<Route exact strict path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />
<Route exact strict path={ withSiteId(FUNNEL_PATH, siteIdList) } component={ Funnels } />

View file

@ -1,11 +1,20 @@
import React from 'react';
import ChatWindow from './ChatWindow';
import LiveSessionList from 'Shared/LiveSessionList';
import LiveSessionSearch from 'Shared/LiveSessionSearch';
import cn from 'classnames'
export default function Assist() {
return (
<div className="absolute">
{/* <ChatWindow /> */}
<div className="page-margin container-90 flex relative">
<div className="flex-1 flex">
{/* <div className="side-menu">
</div> */}
<div className={cn("w-full mx-auto")} style={{ maxWidth: '1300px'}}>
<LiveSessionSearch />
<div className="my-4" />
<LiveSessionList />
</div>
</div>
</div>
)
}

View file

@ -73,7 +73,7 @@ function SessionsMenu(props) {
/>
))}
<div className={stl.divider} />
{/* <div className={stl.divider} />
<div className="my-3">
<SideMenuitem
title={
@ -94,7 +94,7 @@ function SessionsMenu(props) {
onClick={() => onMenuItemClick({ name: 'Assist', type: 'live' })}
/>
</div>
</div> */}
<div className={stl.divider} />
<div className="my-3">

View file

@ -4,6 +4,7 @@ import { NavLink, withRouter } from 'react-router-dom';
import cn from 'classnames';
import {
sessions,
assist,
client,
errors,
dashboard,
@ -27,6 +28,7 @@ import Alerts from '../Alerts/Alerts';
const DASHBOARD_PATH = dashboard();
const SESSIONS_PATH = sessions();
const ASSIST_PATH = assist();
const ERRORS_PATH = errors();
const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
const AUTOREFRESH_INTERVAL = 30 * 1000;
@ -86,6 +88,13 @@ const Header = (props) => {
>
{ 'Sessions' }
</NavLink>
<NavLink
to={ withSiteId(ASSIST_PATH, siteId) }
className={ styles.nav }
activeClassName={ styles.active }
>
{ 'Assist' }
</NavLink>
<NavLink
to={ withSiteId(ERRORS_PATH, siteId) }
className={ styles.nav }

View file

@ -9,9 +9,7 @@ import {
init as initPlayer,
clean as cleanPlayer,
} from 'Player';
import withPermissions from 'HOCs/withPermissions'
import Assist from 'Components/Assist'
import withPermissions from 'HOCs/withPermissions';
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
import EventsBlock from '../Session_/EventsBlock';
@ -54,7 +52,6 @@ function WebPlayer ({ showAssist, session, toggleFullscreen, closeBottomBlock, l
return (
<PlayerProvider>
<InitLoader className="flex-1 p-3">
{ showAssist && <Assist session={session} /> }
<PlayerBlockHeader fullscreen={fullscreen}/>
<div className={ styles.session } data-fullscreen={fullscreen}>
<PlayerBlock />

View file

@ -4,9 +4,9 @@ import LiveFilterModal from '../LiveFilterModal';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import { Icon } from 'UI';
import { connect } from 'react-redux';
import { dashboard as dashboardRoute, isRoute } from "App/routes";
import { assist as assistRoute, isRoute } from "App/routes";
const DASHBOARD_ROUTE = dashboardRoute();
const ASSIST_ROUTE = assistRoute();
interface Props {
filter?: any; // event/filter
@ -43,7 +43,7 @@ function FilterSelection(props: Props) {
</OutsideClickDetectingDiv>
{showModal && (
<div className="absolute left-0 top-20 border shadow rounded bg-white z-50">
{ (isLive && !isRoute(DASHBOARD_ROUTE, window.location.pathname)) ? <LiveFilterModal onFilterClick={onFilterClick} /> : <FilterModal onFilterClick={onFilterClick} /> }
{ isRoute(ASSIST_ROUTE, window.location.pathname) ? <LiveFilterModal onFilterClick={onFilterClick} /> : <FilterModal onFilterClick={onFilterClick} /> }
</div>
)}
</div>

View file

@ -0,0 +1,132 @@
import React, { useEffect } from 'react';
import { fetchLiveList } from 'Duck/sessions';
import { connect } from 'react-redux';
import { NoContent, Loader, LoadMoreButton } from 'UI';
import { List, Map } from 'immutable';
import SessionItem from 'Shared/SessionItem';
import withPermissions from 'HOCs/withPermissions'
import { KEYS } from 'Types/filter/customFilter';
import { applyFilter, addAttribute } from 'Duck/filters';
import { FilterCategory, FilterKey } from 'App/types/filter/filterType';
import { addFilterByKeyAndValue, updateCurrentPage } from 'Duck/liveSearch';
const AUTOREFRESH_INTERVAL = .5 * 60 * 1000
const PER_PAGE = 20;
interface Props {
loading: Boolean,
list: List<any>,
fetchLiveList: () => Promise<void>,
applyFilter: () => void,
filters: any,
addAttribute: (obj) => void,
addFilterByKeyAndValue: (key: FilterKey, value: string) => void,
updateCurrentPage: (page: number) => void,
currentPage: number,
}
function LiveSessionList(props: Props) {
const { loading, filters, list, currentPage } = props;
var timeoutId;
const hasUserFilter = filters.map(i => i.key).includes(KEYS.USERID);
const [sessions, setSessions] = React.useState(list);
const displayedCount = Math.min(currentPage * PER_PAGE, sessions.size);
const addPage = () => props.updateCurrentPage(props.currentPage + 1)
useEffect(() => {
if (filters.size === 0) {
props.addFilterByKeyAndValue(FilterKey.USERID, '');
}
}, []);
useEffect(() => {
const filteredSessions = filters.size > 0 ? props.list.filter(session => {
let hasValidFilter = true;
filters.forEach(filter => {
if (!hasValidFilter) return;
const _values = filter.value.filter(i => i !== '' && i !== null && i !== undefined).map(i => i.toLowerCase());
if (filter.key === FilterKey.USERID) {
const _userId = session.userId ? session.userId.toLowerCase() : '';
hasValidFilter = _values.length > 0 ? (_values.includes(_userId) && hasValidFilter) || _values.some(i => _userId.includes(i)) : hasValidFilter;
}
if (filter.category === FilterCategory.METADATA) {
const _source = session.metadata[filter.key] ? session.metadata[filter.key].toLowerCase() : '';
hasValidFilter = _values.length > 0 ? (_values.includes(_source) && hasValidFilter) || _values.some(i => _source.includes(i)) : hasValidFilter;
}
})
return hasValidFilter;
}) : props.list;
setSessions(filteredSessions);
}, [filters, list]);
useEffect(() => {
props.fetchLiveList();
timeout();
return () => {
clearTimeout(timeoutId)
}
}, [])
const onUserClick = (userId, userAnonymousId) => {
if (userId) {
props.addFilterByKeyAndValue(FilterKey.USERID, userId);
} else {
props.addFilterByKeyAndValue(FilterKey.USERANONYMOUSID, userAnonymousId);
}
}
const timeout = () => {
timeoutId = setTimeout(() => {
props.fetchLiveList();
timeout();
}, AUTOREFRESH_INTERVAL);
}
return (
<div>
<NoContent
title={"No live sessions."}
subtext={
<span>
See how to <a target="_blank" className="link" href="https://docs.openreplay.com/plugins/assist">{'enable Assist'}</a> and ensure you're using tracker-assist <span className="font-medium">v3.5.0</span> or higher.
</span>
}
image={<img src="/img/live-sessions.png"
style={{ width: '70%', marginBottom: '30px' }}/>}
show={ !loading && sessions && sessions.size === 0}
>
<Loader loading={ loading }>
{sessions && sessions.take(displayedCount).map(session => (
<SessionItem
key={ session.sessionId }
session={ session }
live
hasUserFilter={hasUserFilter}
onUserClick={onUserClick}
/>
))}
<LoadMoreButton
className="mt-3"
displayedCount={displayedCount}
totalCount={sessions.size}
onClick={addPage}
/>
</Loader>
</NoContent>
</div>
)
}
export default withPermissions(['ASSIST_LIVE', 'SESSION_REPLAY'])(connect(
(state) => ({
list: state.getIn(['sessions', 'liveSessions']),
loading: state.getIn([ 'sessions', 'loading' ]),
filters: state.getIn([ 'liveSearch', 'instance', 'filters' ]),
currentPage: state.getIn(["liveSearch", "currentPage"]),
}),
{ fetchLiveList, applyFilter, addAttribute, addFilterByKeyAndValue, updateCurrentPage }
)(LiveSessionList));

View file

@ -0,0 +1 @@
export { default } from './LiveSessionList'

View file

@ -0,0 +1,36 @@
import React from 'react'
import cn from 'classnames'
const GOOD = 'Good'
const LESS_CRITICAL = 'Less Critical'
const CRITICAL = 'Critical'
const getErrorState = (count: number) => {
if (count === 0) { return GOOD }
if (count < 2) { return LESS_CRITICAL }
return CRITICAL
}
interface Props {
count?: number
}
export default function ErrorBars(props: Props) {
const { count = 2 } = props
const state = React.useCallback(() => getErrorState(count), [count])()
const bgColor = { 'bg-red' : state === CRITICAL, 'bg-green' : state === GOOD, 'bg-red2' : state === LESS_CRITICAL }
return (
<div className="relative" style={{ width: '80px' }}>
<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>
<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>
<div className="mt-2">{state}</div>
</div>
)
}

View file

@ -0,0 +1 @@
export { default } from './ErrorBars';

View file

@ -18,6 +18,8 @@ import LiveTag from 'Shared/LiveTag';
import Bookmark from 'Shared/Bookmark';
import Counter from './Counter'
import { withRouter } from 'react-router-dom';
import SessionMetaList from './SessionMetaList';
import ErrorBars from './ErrorBars';
const Label = ({ label = '', color = 'color-gray-medium'}) => (
<div className={ cn('font-light text-sm', color)}>{label}</div>
@ -61,64 +63,69 @@ export default class SessionItem extends React.PureComponent {
const hasUserId = userId || userAnonymousId;
return (
<div className={ stl.sessionItem } id="session-item" >
<div className={ cn('flex items-center mr-auto')}>
<div className="flex items-center mr-6" style={{ width: '200px' }}>
<Avatar seed={ userNumericHash } />
<div className="flex flex-col ml-3 overflow-hidden">
<div
className={cn({'color-teal cursor-pointer': !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
onClick={() => (!disableUser && !hasUserFilter && hasUserId) && onUserClick(userId, userAnonymousId)}
>
<TextEllipsis text={ userDisplayName } noHint />
<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={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>
<Label label={ formatTimeOrDate(startedAt, timezone) } />
</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>
</div>
</div>
<div style={{ width: "10%"}} className="self-center">
<ErrorBars count={errorsCount} />
</div>
</div>
<div className={ cn(stl.iconStack, 'flex-1') }>
<div className={ stl.icons }>
<CountryFlag country={ userCountry } className="mr-6" />
<BrowserIcon browser={ userBrowser } size="16" className="mr-6" />
<OsIcon os={ userOs } size="16" className="mr-6" />
<Icon name={ deviceTypeIcon(userDeviceType) } size="16" className="mr-6" />
</div>
</div>
<div className="flex flex-col items-center px-4" style={{ width: '150px'}}>
<div className="text-xl">
{ live ? <Counter startTime={startedAt} /> : formattedDuration }
</div>
<Label label="Duration" />
</div>
{!live && (
<div className="flex flex-col items-center px-4">
<div className={ stl.count }>{ eventsCount }</div>
<Label label={ eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' } />
<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>
<div className="flex items-center">
{!live && (
<div className="flex flex-col items-center px-4">
<div className={ cn(stl.count, { "color-gray-medium": errorsCount === 0 }) } >{ errorsCount }</div>
<Label label="Errors" color={errorsCount > 0 ? '' : 'color-gray-medium'} />
<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" />
</Link>
</div>
)}
{ 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" />
</Link>
</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 },
]} />
</div>
);
}

View file

@ -0,0 +1,48 @@
import React from 'react'
import { Popup } from 'UI'
import cn from 'classnames'
interface Props {
className?: string,
metaList: []
}
const MAX_LENGTH = 3;
export default function SessionMetaList(props: Props) {
const { className = '', metaList } = 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>
))}
{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
/>
)}
</div>
)
}

View file

@ -0,0 +1 @@
export { default } from './SessionMetaList';

View file

@ -12,12 +12,12 @@
user-select: none;
@mixin defaultHover;
border-radius: 3px;
padding: 10px 10px;
padding-right: 15px;
margin-bottom: 15px;
background-color: white;
display: flex;
align-items: center;
/* padding: 10px 10px; */
/* padding-right: 15px; */
/* margin-bottom: 15px; */
/* background-color: white; */
/* display: flex; */
/* align-items: center; */
border: solid thin #EEEEEE;
& .favorite {

View file

@ -82,6 +82,7 @@ const routerOBTabString = `:activeTab(${ Object.values(OB_TABS).join('|') })`;
export const onboarding = (tab = routerOBTabString) => `/onboarding/${ tab }`;
export const sessions = params => queried('/sessions', params);
export const assist = params => queried('/assist', params);
export const session = (sessionId = ':sessionId', hash) => hashed(`/session/${ sessionId }`, hash);
export const liveSession = (sessionId = ':sessionId', hash) => hashed(`/live/session/${ sessionId }`, hash);
@ -105,7 +106,7 @@ export const METRICS_QUERY_KEY = 'metrics';
export const SOURCE_QUERY_KEY = 'source';
export const WIDGET_QUERY_KEY = 'widget';
const REQUIRED_SITE_ID_ROUTES = [ liveSession(''), session(''), sessions(), dashboard(''), error(''), errors(), onboarding(''), funnel(''), funnelIssue(''), ];
const REQUIRED_SITE_ID_ROUTES = [ liveSession(''), session(''), sessions(), assist(), dashboard(''), error(''), errors(), onboarding(''), funnel(''), funnelIssue(''), ];
const routeNeedsSiteId = path => REQUIRED_SITE_ID_ROUTES.some(r => path.startsWith(r));
const siteIdToUrl = (siteId = ':siteId') => {
if (Array.isArray(siteId)) {
@ -128,7 +129,7 @@ export function isRoute(route, path){
routeParts.every((p, i) => p.startsWith(':') || p === pathParts[ i ]);
}
const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), dashboard(), errors(), onboarding('')];
const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), assist(), dashboard(), errors(), onboarding('')];
export const siteChangeAvaliable = path => SITE_CHANGE_AVALIABLE_ROUTES.some(r => isRoute(r, path));

View file

@ -34,6 +34,7 @@ export default Record({
rangeValue,
startDate,
endDate,
// groupByUser: true,
sort: 'startTs',
order: 'desc',