Merge branch 'main' of github.com:openreplay/openreplay

This commit is contained in:
Shekar Siri 2021-12-28 16:46:01 +05:30
commit 5b8747d466
28 changed files with 338 additions and 286 deletions

View file

@ -0,0 +1,38 @@
import React, { useEffect, useState } from 'react';
import { SlideModal, Icon } from 'UI';
import SessionList from '../SessionList';
import stl from './assistTabs.css'
interface Props {
userId: any,
}
const AssistTabs = (props: Props) => {
const [showMenu, setShowMenu] = useState(false)
return (
<div className="relative mr-4">
<div className="flex items-center">
<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>
</div>
</div>
<SlideModal
title={ <div>Live Sessions by {props.userId}</div> }
isDisplayed={ showMenu }
content={ showMenu && <SessionList /> }
onClose={ () => setShowMenu(false) }
/>
</div>
);
};
export default AssistTabs;

View file

@ -0,0 +1,5 @@
.btnLink {
cursor: pointer;
color: $green;
text-decoration: underline;
}

View file

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

View file

@ -0,0 +1,40 @@
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { fetchLiveList } from 'Duck/sessions';
import { Loader, NoContent } from 'UI';
import SessionItem from 'Shared/SessionItem';
interface Props {
loading: boolean,
list: any,
session: any,
fetchLiveList: () => void,
}
function SessionList(props: Props) {
useEffect(() => {
props.fetchLiveList();
}, [])
return (
<Loader loading={props.loading}>
<NoContent
show={ !props.loading && (props.list.size === 0 )}
title="No live sessions."
>
<div className="p-4">
{ props.list.map(session => <SessionItem key={ session.sessionId } session={ session } />) }
</div>
</NoContent>
</Loader>
);
}
export default connect(state => {
const session = state.getIn([ 'sessions', 'current' ]);
return {
session,
list: state.getIn(['sessions', 'liveSessions'])
.filter(i => i.userId === session.userId && i.sessionId !== session.sessionId),
loading: state.getIn([ 'sessions', 'fetchLiveListRequest', 'loading' ]),
}
}, { fetchLiveList })(SessionList);

View file

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

View file

@ -119,7 +119,7 @@ export default connect(state => ({
activeFlow: state.getIn([ 'filters', 'activeFlow' ]),
captureRate: state.getIn(['watchdogs', 'captureRate']),
filters: state.getIn([ 'filters', 'appliedFilter' ]),
sessionsLoading: state.getIn([ 'sessions', 'loading' ]),
sessionsLoading: state.getIn([ 'sessions', 'fetchLiveListRequest', 'loading' ]),
}), {
fetchWatchdogStatus, setActiveFlow, clearEvents, setActiveTab, fetchSessionList
})(SessionsMenu);

View file

@ -31,17 +31,19 @@ const InitLoader = connectPlayer(state => ({
}))(Loader);
function WebPlayer ({ showAssist, session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, loadingCredentials, assistCredendials, request }) {
function WebPlayer ({ showAssist, session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, loadingCredentials, assistCredendials, request, isEnterprise, hasSessionsPath }) {
useEffect(() => {
if (!loadingCredentials) {
initPlayer(session, jwt, assistCredendials);
initPlayer(session, jwt, assistCredendials, !hasSessionsPath && session.live);
}
return () => cleanPlayer()
}, [ session.sessionId, loadingCredentials, assistCredendials ]);
// LAYOUT (TODO: local layout state - useContext or something..)
useEffect(() => {
request();
if (isEnterprise) {
request();
}
return () => {
toggleFullscreen(false);
closeBottomBlock();
@ -60,7 +62,7 @@ function WebPlayer ({ showAssist, session, toggleFullscreen, closeBottomBlock, l
</InitLoader>
</PlayerProvider>
);
}
};
export default withRequest({
initialData: null,
@ -74,6 +76,8 @@ export default withRequest({
showAssist: state.getIn([ 'sessions', 'showChatWindow' ]),
jwt: state.get('jwt'),
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
hasSessionsPath: state.getIn([ 'sessions', 'sessionPath' ]).includes('/sessions'),
isEnterprise: state.getIn([ 'user', 'client', 'edition' ]) === 'ee',
}),
{ toggleFullscreen, closeBottomBlock },
)(WebPlayer)));
)(WebPlayer)));

View file

@ -20,6 +20,7 @@ function Session({
session,
fetchSession,
fetchSlackList,
hasSessionsPath
}) {
usePageTitle("OpenReplay Session Player");
useEffect(() => {
@ -34,7 +35,7 @@ function Session({
return () => {
if (!session.exists()) return;
}
},[ sessionId ]);
},[ sessionId, hasSessionsPath ]);
return (
<NoContent
@ -50,7 +51,7 @@ function Session({
<Loader className="flex-1" loading={ loading || sessionId !== session.sessionId }>
{ session.isIOS
? <IOSPlayer session={session} />
: (session.live ? <LivePlayer /> : <WebPlayer />)
: (session.live && !hasSessionsPath ? <LivePlayer /> : <WebPlayer />)
}
</Loader>
</NoContent>
@ -64,6 +65,7 @@ export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state, pro
loading: state.getIn([ 'sessions', 'loading' ]),
hasErrors: !!state.getIn([ 'sessions', 'errors' ]),
session: state.getIn([ 'sessions', 'current' ]),
hasSessionsPath: state.getIn([ 'sessions', 'sessionPath' ]).includes('/sessions'),
};
}, {
fetchSession,

View file

@ -1,56 +1,89 @@
//import cn from 'classnames';
import { getRE } from 'App/utils';
import { Label, NoContent, Input, SlideModal, CloseButton } from 'UI';
import { connectPlayer, pause } from 'Player';
import Autoscroll from '../Autoscroll';
import { connectPlayer, pause, jump } from 'Player';
// import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock';
import TimeTable from '../TimeTable';
import FetchDetails from './FetchDetails';
import { renderName, renderDuration } from '../Network';
import { connect } from 'react-redux';
import { setTimelinePointer } from 'Duck/sessions';
@connectPlayer(state => ({
list: state.fetchList,
}))
@connect(state => ({
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
}), { setTimelinePointer })
export default class Fetch extends React.PureComponent {
state = {
filter: "",
filteredList: this.props.list,
current: null,
currentIndex: 0,
showFetchDetails: false,
hasNextError: false,
hasPreviousError: false,
}
onFilterChange = (e, { value }) => this.setState({ filter: value })
onFilterChange = (e, { value }) => {
const { list } = this.props;
const filterRE = getRE(value, 'i');
const filtered = list
.filter((r) => filterRE.test(r.name) || filterRE.test(r.url) || filterRE.test(r.method) || filterRE.test(r.status));
this.setState({ filter: value, filteredList: value ? filtered : list, currentIndex: 0 });
}
setCurrent = (item, index) => {
pause()
jump(item.time)
this.setState({ current: item, currentIndex: index });
}
closeModal = () => this.setState({ current: null})
onRowClick = (item, index) => {
pause()
this.setState({ current: item, currentIndex: index, showFetchDetails: true });
this.props.setTimelinePointer(null);
}
closeModal = () => this.setState({ current: null, showFetchDetails: false });
nextClickHander = () => {
const { list } = this.props;
const { currentIndex } = this.state;
// const { list } = this.props;
const { currentIndex, filteredList } = this.state;
if (currentIndex === list.length - 1) return;
if (currentIndex === filteredList.length - 1) return;
const newIndex = currentIndex + 1;
this.setCurrent(list[newIndex], newIndex);
this.setCurrent(filteredList[newIndex], newIndex);
this.setState({ showFetchDetails: true });
}
prevClickHander = () => {
const { list } = this.props;
const { currentIndex } = this.state;
// const { list } = this.props;
const { currentIndex, filteredList } = this.state;
if (currentIndex === 0) return;
const newIndex = currentIndex - 1;
this.setCurrent(list[newIndex], newIndex);
this.setCurrent(filteredList[newIndex], newIndex);
this.setState({ showFetchDetails: true });
}
static getDerivedStateFromProps(nextProps, prevState) {
const { filteredList } = prevState;
if (nextProps.timelinePointer) {
let activeItem = filteredList.find((r) => r.time >= nextProps.timelinePointer.time);
activeItem = activeItem || filteredList[filteredList.length - 1];
return {
current: activeItem,
currentIndex: filteredList.indexOf(activeItem),
};
}
}
render() {
const { list } = this.props;
const { filter, current, currentIndex } = this.state;
const filterRE = getRE(filter, 'i');
const filtered = list
.filter((r) => filterRE.test(r.name) || filterRE.test(r.url) || filterRE.test(r.method) || filterRE.test(r.status));
// const { list } = this.props;
const { current, currentIndex, showFetchDetails, filteredList } = this.state;
return (
<React.Fragment>
<SlideModal
@ -73,14 +106,14 @@ export default class Fetch extends React.PureComponent {
</div>
</div>
}
isDisplayed={ current != null }
content={ current &&
isDisplayed={ current != null && showFetchDetails }
content={ current && showFetchDetails &&
<FetchDetails
resource={ current }
nextClick={this.nextClickHander}
prevClick={this.prevClickHander}
first={currentIndex === 0}
last={currentIndex === filtered.length - 1}
last={currentIndex === filteredList.length - 1}
/>
}
onClose={ this.closeModal }
@ -88,25 +121,31 @@ export default class Fetch extends React.PureComponent {
<BottomBlock>
<BottomBlock.Header>
<h4 className="text-lg">Fetch</h4>
<Input
className="input-small"
placeholder="Filter"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onFilterChange }
/>
<div className="flex items-center">
{/* <div className="flex items-center mr-3 text-sm uppercase">
<div className="p-2 cursor-pointer" onClick={this.goToPrevError}>Prev</div>
<div className="p-2 cursor-pointer" onClick={this.goToNextError}>Next</div>
</div> */}
<Input
className="input-small"
placeholder="Filter"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onFilterChange }
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<NoContent
size="small"
show={ filtered.length === 0}
show={ filteredList.length === 0}
>
<TimeTable
rows={ filtered }
onRowClick={ this.setCurrent }
rows={ filteredList }
onRowClick={ this.onRowClick }
hoverable
// navigation
navigation
activeIndex={currentIndex}
>
{[
@ -120,7 +159,7 @@ export default class Fetch extends React.PureComponent {
width: 60,
}, {
label: "Name",
width: 130,
width: 180,
render: renderName,
},
{

View file

@ -3,14 +3,10 @@ import { connectPlayer, jump, pause } from 'Player';
import { QuestionMarkHint, Popup, Tabs, Input } from 'UI';
import { getRE } from 'App/utils';
import { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
import { formatMs } from 'App/date';
import TimeTable from '../TimeTable';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import stl from './network.css';
import NetworkContent from './NetworkContent';
import { connect } from 'react-redux';
import { setTimelinePointer } from 'Duck/sessions';
const ALL = 'ALL';
const XHR = 'xhr';
@ -28,73 +24,24 @@ const TAB_TO_TYPE_MAP = {
[ MEDIA ]: TYPES.MEDIA,
[ OTHER ]: TYPES.OTHER
}
const TABS = [ ALL, XHR, JS, CSS, IMG, MEDIA, OTHER ].map(tab => ({
text: tab,
key: tab,
}));
const DOM_LOADED_TIME_COLOR = "teal";
const LOAD_TIME_COLOR = "red";
export function renderName(r) {
return (
<Popup
trigger={ <div className={ stl.popupNameTrigger }>{ r.name }</div> }
content={ <div className={ stl.popupNameContent }>{ r.url }</div> }
size="mini"
position="right center"
/>
);
}
const renderXHRText = () => (
<span className="flex items-center">
{XHR}
<QuestionMarkHint
onHover
content={
<>
Use our <a className="color-teal underline" target="_blank" href="https://docs.openreplay.com/plugins/fetch">Fetch plugin</a>
{' to capture HTTP requests and responses, including status codes and bodies.'} <br/>
We also provide <a className="color-teal underline" target="_blank" href="https://docs.openreplay.com/plugins/graphql">support for GraphQL</a>
{' for easy debugging of your queries.'}
</>
}
className="ml-1"
/>
</span>
);
function renderSize(r) {
let triggerText;
let content;
if (r.decodedBodySize == null) {
triggerText = "x";
content = "Not captured";
} else {
const headerSize = r.headerSize || 0;
const encodedSize = r.encodedBodySize || 0;
const transferred = headerSize + encodedSize;
const showTransferred = r.headerSize != null;
triggerText = formatBytes(r.decodedBodySize);
content = (
<ul>
{ showTransferred &&
<li>{`${formatBytes( r.encodedBodySize + headerSize )} transfered over network`}</li>
}
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
</ul>
);
}
return (
<Popup
trigger={ <div>{ triggerText }</div> }
content={ content }
size="mini"
position="right center"
/>
<div className="flex w-full relative items-center">
<Popup
trigger={ <div className={ stl.popupNameTrigger }>{ r.name }</div> }
content={ <div className={ stl.popupNameContent }>{ r.url }</div> }
size="mini"
position="right center"
/>
<div
className="absolute right-0 text-xs uppercase p-2 color-gray-500 hover:color-black"
onClick={ (e) => {
e.stopPropagation();
jump(r.time)
}}
>Jump</div>
</div>
);
}
@ -130,14 +77,18 @@ export function renderDuration(r) {
resources: state.resourceList,
domContentLoadedTime: state.domContentLoadedTime,
loadTime: state.loadTime,
time: state.time,
// time: state.time,
playing: state.playing,
domBuildingTime: state.domBuildingTime,
fetchPresented: state.fetchList.length > 0,
}))
@connect(state => ({
timelinePointer: state.getIn(['sessions', 'timelinePointer']),
}), { setTimelinePointer })
export default class Network extends React.PureComponent {
state = {
filter: '',
filteredList: this.props.resources,
activeTab: ALL,
currentIndex: 0
}
@ -146,10 +97,29 @@ export default class Network extends React.PureComponent {
pause();
jump(e.time);
this.setState({ currentIndex: index })
this.props.setTimelinePointer(null);
}
onTabClick = activeTab => this.setState({ activeTab })
onFilterChange = (e, { value }) => this.setState({ filter: value })
onFilterChange = (e, { value }) => {
const { resources } = this.props;
const filterRE = getRE(value, 'i');
const filtered = resources.filter(({ type, name }) =>
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[ activeTab ]));
this.setState({ filter: value, filteredList: value ? filtered : resources, currentIndex: 0 });
}
static getDerivedStateFromProps(nextProps, prevState) {
const { filteredList } = prevState;
if (nextProps.timelinePointer) {
const activeItem = filteredList.find((r) => r.time >= nextProps.timelinePointer.time);
return {
currentIndex: activeItem ? filteredList.indexOf(activeItem) : filteredList.length - 1,
};
}
}
render() {
const {
@ -159,50 +129,23 @@ export default class Network extends React.PureComponent {
loadTime,
domBuildingTime,
fetchPresented,
time,
// time,
playing
} = this.props;
const { filter, activeTab, currentIndex } = this.state;
const filterRE = getRE(filter, 'i');
let filtered = resources.filter(({ type, name }) =>
filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[ activeTab ]));
// const referenceLines = [];
// if (domContentLoadedTime != null) {
// referenceLines.push({
// time: domContentLoadedTime,
// color: DOM_LOADED_TIME_COLOR,
// })
// }
// if (loadTime != null) {
// referenceLines.push({
// time: loadTime,
// color: LOAD_TIME_COLOR,
// })
// }
//
// let tabs = TABS;
// if (!fetchPresented) {
// tabs = TABS.map(tab => tab.key === XHR
// ? {
// text: renderXHRText(),
// key: XHR,
// }
// : tab
// );
// }
const resourcesSize = filtered.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0);
const transferredSize = filtered
const { filter, activeTab, currentIndex, filteredList } = this.state;
// const filterRE = getRE(filter, 'i');
// let filtered = resources.filter(({ type, name }) =>
// filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[ activeTab ]));
const resourcesSize = filteredList.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0);
const transferredSize = filteredList
.reduce((sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), 0);
return (
<React.Fragment>
<NetworkContent
// {...this.props }
time = { time }
// time = { time }
location = { location }
resources = { resources }
resources = { filteredList }
domContentLoadedTime = { domContentLoadedTime }
loadTime = { loadTime }
domBuildingTime = { domBuildingTime }
@ -210,91 +153,8 @@ export default class Network extends React.PureComponent {
resourcesSize={resourcesSize}
transferredSize={transferredSize}
onRowClick={ this.onRowClick }
currentIndex={playing ? null : currentIndex}
currentIndex={currentIndex}
/>
{/* <BottomBlock>
<BottomBlock.Header>
<Tabs
className="uppercase"
tabs={ tabs }
active={ activeTab }
onClick={ this.onTabClick }
border={ false }
/>
<Input
className="input-small"
placeholder="Filter by Name"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onFilterChange }
/>
</BottomBlock.Header>
<BottomBlock.Content>
<InfoLine>
<InfoLine.Point label={ filtered.length } value=" requests" />
<InfoLine.Point
label={ formatBytes(transferredSize) }
value="transferred"
display={ transferredSize > 0 }
/>
<InfoLine.Point
label={ formatBytes(resourcesSize) }
value="resources"
display={ resourcesSize > 0 }
/>
<InfoLine.Point
label="DOM Building Time"
value={ formatMs(domBuildingTime)}
display={ domBuildingTime != null }
/>
<InfoLine.Point
label="DOMContentLoaded"
value={ formatMs(domContentLoadedTime)}
display={ domContentLoadedTime != null }
dotColor={ DOM_LOADED_TIME_COLOR }
/>
<InfoLine.Point
label="Load"
value={ formatMs(loadTime)}
display={ loadTime != null }
dotColor={ LOAD_TIME_COLOR }
/>
</InfoLine>
<TimeTable
rows={ filtered }
referenceLines={referenceLines}
renderPopup
navigation
>
{[
{
label: "Status",
dataKey: 'status',
width: 70,
}, {
label: "Type",
dataKey: 'type',
width: 60,
}, {
label: "Name",
width: 130,
render: renderName,
},
{
label: "Size",
width: 60,
render: renderSize,
},
{
label: "Time",
width: 80,
render: renderDuration,
}
]}
</TimeTable>
</BottomBlock.Content>
</BottomBlock> */}
</React.Fragment>
);
}

View file

@ -22,7 +22,7 @@
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 100%;
max-width: 80%;
width: fit-content;
}
.popupNameContent {

View file

@ -7,6 +7,7 @@ import TimeTracker from './TimeTracker';
import { ReduxTime } from './Time';
import stl from './timeline.css';
import { TYPES } from 'Types/session/event';
import { setTimelinePointer } from 'Duck/sessions';
const getPointerIcon = (type) => {
// exception,
@ -69,7 +70,7 @@ const getPointerIcon = (type) => {
state.getIn([ 'sessions', 'current', 'clickRageTime' ]),
returningLocationTime: state.getIn([ 'sessions', 'current', 'returningLocation' ]) &&
state.getIn([ 'sessions', 'current', 'returningLocationTime' ]),
}))
}), { setTimelinePointer })
export default class Timeline extends React.PureComponent {
seekProgress = (e) => {
const { endTime } = this.props;
@ -78,9 +79,10 @@ export default class Timeline extends React.PureComponent {
this.props.jump(time);
}
createEventClickHandler = time => (e) => {
createEventClickHandler = pointer => (e) => {
e.stopPropagation();
this.props.jump(time)
this.props.jump(pointer.time);
this.props.setTimelinePointer(pointer);
}
componentDidMount() {
@ -144,7 +146,7 @@ export default class Timeline extends React.PureComponent {
//width: `${ 2000 * scale }%`
} }
className={ stl.clickRage }
onClick={ this.createEventClickHandler(iss.time) }
onClick={ this.createEventClickHandler(iss) }
>
<TimelinePointer
icon={iss.icon}
@ -165,7 +167,7 @@ export default class Timeline extends React.PureComponent {
//width: `${ 2000 * scale }%`
} }
className={ stl.clickRage }
onClick={ this.createEventClickHandler(e.time) }
onClick={ this.createEventClickHandler(e) }
>
<TimelinePointer
icon={getPointerIcon('click_rage')}
@ -259,7 +261,7 @@ export default class Timeline extends React.PureComponent {
key={ e.key }
className={ cn(stl.markup, stl.error) }
style={ { left: `${ e.time * scale }%`, top: '-30px' } }
onClick={ this.createEventClickHandler(e.time) }
onClick={ this.createEventClickHandler(e) }
>
<TimelinePointer
icon={getPointerIcon('exception')}
@ -305,7 +307,7 @@ export default class Timeline extends React.PureComponent {
//[ stl.info ]: !l.isYellow() && !l.isRed(),
}) }
style={ { left: `${ l.time * scale }%`, top: '-30px' } }
onClick={ this.createEventClickHandler(l.time) }
onClick={ this.createEventClickHandler(l) }
>
<TimelinePointer
icon={getPointerIcon('log')}
@ -360,7 +362,7 @@ export default class Timeline extends React.PureComponent {
[ stl.warning ]: r.isYellow(),
}) }
style={ { left: `${ r.time * scale }%`, top: '-30px' } }
onClick={ this.createEventClickHandler(r.time) }
onClick={ this.createEventClickHandler(r) }
>
<TimelinePointer
icon={getPointerIcon('resource')}
@ -407,7 +409,7 @@ export default class Timeline extends React.PureComponent {
key={ e.key }
className={ cn(stl.markup, stl.error) }
style={ { left: `${ e.time * scale }%`, top: '-30px' } }
onClick={ this.createEventClickHandler(e.time) }
onClick={ this.createEventClickHandler(e) }
>
<TimelinePointer
icon={getPointerIcon('fetch')}
@ -448,7 +450,7 @@ export default class Timeline extends React.PureComponent {
key={ e.key }
className={ cn(stl.markup, stl.error) }
style={ { left: `${ e.time * scale }%`, top: '-30px' } }
onClick={ this.createEventClickHandler(e.time) }
onClick={ this.createEventClickHandler(e) }
>
<TimelinePointer
icon={getPointerIcon('stack')}

View file

@ -4,17 +4,18 @@ import { browserIcon, osIcon, deviceTypeIcon } from 'App/iconNames';
import { formatTimeOrDate } from 'App/date';
import { sessions as sessionsRoute, funnel as funnelRoute, funnelIssue as funnelIssueRoute, withSiteId } from 'App/routes';
import { Icon, CountryFlag, IconButton, BackLink } from 'UI';
import { toggleFavorite } from 'Duck/sessions';
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 cls from './playerBlockHeader.css';
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';
const SESSIONS_ROUTE = sessionsRoute();
@ -37,8 +38,9 @@ function capitalise(str) {
funnelRef: state.getIn(['funnels', 'navRef']),
siteId: state.getIn([ 'user', 'siteId' ]),
funnelPage: state.getIn(['sessions', 'funnelPage']),
hasSessionsPath: state.getIn([ 'sessions', 'sessionPath' ]).includes('/sessions'),
}), {
toggleFavorite, fetchListIntegration
toggleFavorite, fetchListIntegration, setSessionPath
})
@withRouter
export default class PlayerBlockHeader extends React.PureComponent {
@ -86,21 +88,24 @@ export default class PlayerBlockHeader extends React.PureComponent {
userDevice,
userBrowserVersion,
userDeviceType,
live,
},
loading,
live,
// live,
disabled,
jiraConfig,
fullscreen,
hasSessionsPath
} = this.props;
const { history, siteId } = this.props;
// const { history, siteId } = this.props;
const _live = live && !hasSessionsPath;
return (
<div className={ cn(cls.header, "flex justify-between", { "hidden" : fullscreen}) }>
<div className={ cn(stl.header, "flex justify-between", { "hidden" : fullscreen}) }>
<div className="flex w-full">
<BackLink onClick={this.backHandler} label="Back" />
<div className={ cls.divider } />
<div className={ stl.divider } />
<div className="mx-4 flex items-center">
<CountryFlag country={ userCountry } />
@ -115,11 +120,17 @@ export default class PlayerBlockHeader extends React.PureComponent {
<HeaderInfo icon={ osIcon(userOs) } label={ userOs } />
<div className='ml-auto flex items-center'>
{ live && <AssistActions isLive userId={userId} /> }
{ !live && (
{ 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 && (
<>
<Autoplay />
<div className={ cls.divider } />
<div className={ stl.divider } />
<IconButton
className="mr-2"
tooltip="Bookmark"
@ -143,7 +154,7 @@ export default class PlayerBlockHeader extends React.PureComponent {
/>
</>
)}
{ !live && jiraConfig && jiraConfig.token && <Issues sessionId={ sessionId } /> }
{ !_live && jiraConfig && jiraConfig.token && <Issues sessionId={ sessionId } /> }
</div>
</div>
</div>

View file

@ -135,7 +135,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
...computeTimeLine(this.props.rows, this.state.firstVisibleRowIndex, this.visibleCount),
});
}
if (this.props.activeIndex && prevProps.activeIndex !== this.props.activeIndex && this.scroller.current != null) {
if (this.props.activeIndex >= 0 && prevProps.activeIndex !== this.props.activeIndex && this.scroller.current != null) {
this.scroller.current.scrollToRow(this.props.activeIndex);
}
}
@ -168,7 +168,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
<div
style={ rowStyle }
key={ key }
className={ cn('border-b border-color-gray-light-shade', stl.row, { [ stl.hoverable ]: hoverable, "error color-red": !!row.isRed && row.isRed(), 'cursor-pointer' : typeof onRowClick === "function", [stl.activeRow] : activeIndex === index, 'color-white' : activeIndex === index }) }
className={ cn('border-b border-color-gray-light-shade', stl.row, { [ stl.hoverable ]: hoverable, "error color-red": !!row.isRed && row.isRed(), 'cursor-pointer' : typeof onRowClick === "function", [stl.activeRow] : activeIndex === index }) }
onClick={ typeof onRowClick === "function" ? () => onRowClick(row, index) : null }
id="table-row"
>
@ -223,7 +223,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
navigation=false,
referenceLines = [],
additionalHeight = 0,
activeIndex
activeIndex,
} = this.props;
const {
timewidth,
@ -247,7 +247,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
return (
<div className={ cn(className, "relative") }>
{ navigation &&
<div className={ cn(autoscrollStl.navButtons, "flex items-center") } style={{ top: '-33px', right: '30px' }} >
<div className={ cn(autoscrollStl.navButtons, "flex items-center") } >
<IconButton
size="small"
icon="chevron-up"

View file

@ -99,8 +99,6 @@ $offset: 10px;
}
}
.activeRow {
background-color: $teal;
background-color: rgba(54, 108, 217, 0.1);
}

View file

@ -12,8 +12,8 @@
.navButtons {
position: absolute;
right: 40px;
top: 10px;
right: 260px;
top: -34px;
}

View file

@ -12,3 +12,15 @@
background-color: $gray-light;
}
.liveSwitchButton {
cursor: pointer;
padding: 3px 8px;
border: solid thin $green;
color: $green;
border-radius: 3px;
margin-right: 10px;
&:hover {
background-color: $green;
color: white;
}
}

View file

@ -3,7 +3,7 @@ import { Duration } from 'luxon';
interface Props {
startTime: any,
className: string
className?: string
}
function Counter({ startTime, className }: Props) {

View file

@ -10,21 +10,33 @@ import {
TextEllipsis
} from 'UI';
import { deviceTypeIcon } from 'App/iconNames';
import { toggleFavorite } from 'Duck/sessions';
import { session as sessionRoute } from 'App/routes';
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
import { session as sessionRoute, withSiteId } from 'App/routes';
import { durationFormatted, formatTimeOrDate } from 'App/date';
import stl from './sessionItem.css';
import LiveTag from 'Shared/LiveTag';
import Bookmark from 'Shared/Bookmark';
import Counter from './Counter'
import { withRouter } from 'react-router-dom';
const Label = ({ label = '', color = 'color-gray-medium'}) => (
<div className={ cn('font-light text-sm', color)}>{label}</div>
)
@connect(state => ({
timezone: state.getIn(['sessions', 'timezone'])
}), { toggleFavorite })
timezone: state.getIn(['sessions', 'timezone']),
isAssist: state.getIn(['sessions', 'activeTab']).type === 'live',
siteId: state.getIn([ 'user', 'siteId' ]),
}), { toggleFavorite, setSessionPath })
@withRouter
export default class SessionItem extends React.PureComponent {
replaySession = () => {
const { history, session: { sessionId }, siteId, isAssist } = this.props;
if (!isAssist) {
this.props.setSessionPath(history.location.pathname)
}
history.push(withSiteId(sessionRoute(sessionId), siteId))
}
// eslint-disable-next-line complexity
render() {
const {
@ -110,9 +122,9 @@ export default class SessionItem extends React.PureComponent {
</div>
<div className={ stl.playLink } id="play-button" data-viewed={ viewed }>
<Link to={ sessionRoute(sessionId) }>
<div onClick={this.replaySession}>
<Icon name={ viewed ? 'play-fill' : 'play-circle-light' } size="30" color="teal" />
</Link>
</div>
</div>
</div>
</div>

View file

@ -92,7 +92,7 @@
display: flex;
align-items: center;
transition: all 0.2s;
/* opacity: 0; */
cursor: pointer;
&[data-viewed=true] {
opacity: 1;
}

View file

@ -7,6 +7,7 @@
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
height: 36px;
font-size: 14px;

View file

@ -119,7 +119,6 @@ const reducer = (state = initialState, action = {}) => {
let stages = [];
if (action.isRefresh) {
const activeStages = state.get('activeStages');
console.log('test', activeStages);
const oldInsights = state.get('insights');
const lastStage = action.data.stages[action.data.stages.length - 1]
const lastStageIndex = activeStages.toJS()[1];

View file

@ -8,7 +8,6 @@ import { getRE } from 'App/utils';
import { LAST_7_DAYS } from 'Types/app/period';
import { getDateRangeFromValue } from 'App/dateRange';
const INIT = 'sessions/INIT';
const FETCH_LIST = new RequestTypes('sessions/FETCH_LIST');
@ -25,6 +24,8 @@ const SET_EVENT_QUERY = 'sessions/SET_EVENT_QUERY';
const SET_AUTOPLAY_VALUES = 'sessions/SET_AUTOPLAY_VALUES';
const TOGGLE_CHAT_WINDOW = 'sessions/TOGGLE_CHAT_WINDOW';
const SET_FUNNEL_PAGE_FLAG = 'sessions/SET_FUNNEL_PAGE_FLAG';
const SET_TIMELINE_POINTER = 'sessions/SET_TIMELINE_POINTER';
const SET_SESSION_PATH = 'sessions/SET_SESSION_PATH';
const SET_ACTIVE_TAB = 'sessions/SET_ACTIVE_TAB';
@ -57,6 +58,8 @@ const initialState = Map({
insightFilters: defaultDateFilters,
host: '',
funnelPage: Map(),
timelinePointer: null,
sessionPath: '',
});
const reducer = (state = initialState, action = {}) => {
@ -242,13 +245,18 @@ const reducer = (state = initialState, action = {}) => {
return state.set('insights', List(action.data).sort((a, b) => b.count - a.count));
case SET_FUNNEL_PAGE_FLAG:
return state.set('funnelPage', action.funnelPage ? Map(action.funnelPage) : false);
case SET_TIMELINE_POINTER:
return state.set('timelinePointer', action.pointer);
case SET_SESSION_PATH:
return state.set('sessionPath', action.path);
default:
return state;
}
};
export default withRequestState({
_: [ FETCH, FETCH_LIST, FETCH_LIVE_LIST ],
_: [ FETCH, FETCH_LIST ],
fetchLiveListRequest: FETCH_LIVE_LIST,
fetchFavoriteListRequest: FETCH_FAVORITE_LIST,
toggleFavoriteRequest: TOGGLE_FAVORITE,
fetchErrorStackList: FETCH_ERROR_STACK,
@ -262,10 +270,10 @@ function init(session) {
}
}
export const fetchList = (params = {}, clear = false) => (dispatch, getState) => {
export const fetchList = (params = {}, clear = false, live = false) => (dispatch, getState) => {
const activeTab = getState().getIn([ 'sessions', 'activeTab' ]);
return dispatch(activeTab && activeTab.type === 'live' ? {
return dispatch((activeTab && activeTab.type === 'live' || live )? {
types: FETCH_LIVE_LIST.toArray(),
call: client => client.post('/assist/sessions', params),
} : {
@ -376,3 +384,16 @@ export function setFunnelPage(funnelPage) {
}
}
export function setTimelinePointer(pointer) {
return {
type: SET_TIMELINE_POINTER,
pointer
}
}
export function setSessionPath(path) {
return {
type: SET_SESSION_PATH,
path
}
}

View file

@ -118,7 +118,7 @@ export default class MessageDistributor extends StatedScreen {
private navigationStartOffset: number = 0;
private lastMessageTime: number = 0;
constructor(private readonly session: any /*Session*/, jwt: string, config) {
constructor(private readonly session: any /*Session*/, jwt: string, config, live: boolean) {
super();
this.pagesManager = new PagesManager(this, this.session.isMobile)
this.mouseManager = new MouseManager(this);
@ -126,7 +126,7 @@ export default class MessageDistributor extends StatedScreen {
this.sessionStart = this.session.startedAt;
if (this.session.live) {
if (live) {
// const sockUrl = `wss://live.openreplay.com/1/${ this.session.siteId }/${ this.session.sessionId }/${ jwt }`;
// this.subscribeOnMessages(sockUrl);
initListsDepr({})

View file

@ -28,11 +28,11 @@ document.addEventListener("visibilitychange", function() {
}
});
export function init(session, jwt, config) {
const live = session.live;
export function init(session, jwt, config, live = false) {
// const live = session.live;
const endTime = !live && session.duration.valueOf();
instance = new Player(session, jwt, config);
instance = new Player(session, jwt, config, live);
update({
initialized: true,
live,

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 204.7 200.9" xmlns="http://www.w3.org/2000/svg">
<title>Fedora logo (2021)</title>
<path d="m102.7 1.987c-55.41 0-100.3 44.21-100.4 98.79h-0.01773v76.47h0.01773c0.02659 12.38 10.22 22.4 22.8 22.4h77.58c55.42-0.0349 100.3-44.24 100.3-98.79 8e-5 -54.58-44.91-98.79-100.4-98.79zm20.39 40.68c16.85 0 32.76 12.7 32.76 30.23 0 1.625 0.01 3.252-0.26 5.095-0.4668 4.662-4.794 8.012-9.505 7.355-4.711-0.6649-7.909-5.07-7.037-9.679 0.0799-0.526 0.1083-1.352 0.1083-2.772 0-9.938-8.257-13.77-16.06-13.77-7.805 0-14.84 6.462-14.85 13.77 0.1349 8.455 0 16.84 0 25.29l14.49-0.1067c11.31-0.2306 11.44 16.54 0.1305 16.46l-14.61 0.1067c-0.0354 6.801 0.0532 5.571 0.0178 8.996 0 0 0.1225 8.318-0.1296 14.62-1.749 18.52-17.76 33.32-37 33.32-20.4 0-37.2-16.41-37.2-36.54 0.6124-20.7 17.38-36.99 38.5-36.8l11.78-0.08737v16.43l-11.78 0.1066h-0.06216c-11.6 0.3382-21.55 8.1-21.74 20.34 0 11.15 9.148 20.08 20.5 20.08 11.34 0 20.42-8.124 20.42-20.06l-0.01772-62.23c0.0058-1.155 0.04435-2.073 0.1731-3.347 1.914-15.22 15.74-26.82 31.39-26.82z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -34,8 +34,8 @@ export default Record({
startDate,
endDate,
sort: undefined,
order: undefined,
sort: 'startTs',
order: 'desc',
viewed: undefined,
consoleLogCount: undefined,

View file

@ -88,6 +88,7 @@ export default Record({
...session
}) => {
const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration);
const durationSeconds = duration.valueOf();
const startedAt = +startTs;
const userDevice = session.userDevice || session.userDeviceType || 'Other';
@ -96,7 +97,7 @@ export default Record({
const events = List(session.events)
.map(e => SessionEvent({ ...e, time: e.timestamp - startedAt }))
.filter(({ type }) => type !== TYPES.CONSOLE);
.filter(({ type, time }) => type !== TYPES.CONSOLE && time <= durationSeconds);
let resources = List(session.resources)
.map(Resource);