Merge branch 'main' of github.com:openreplay/openreplay into dev
This commit is contained in:
commit
319852e56e
35 changed files with 434 additions and 341 deletions
|
|
@ -24,6 +24,7 @@ import * as routes from './routes';
|
|||
import { OB_DEFAULT_TAB } from 'App/routes';
|
||||
import Signup from './components/Signup/Signup';
|
||||
import { fetchTenants } from 'Duck/user';
|
||||
import { setSessionPath } from 'Duck/sessions';
|
||||
|
||||
const BugFinder = withSiteIdUpdater(BugFinderPure);
|
||||
const Dashboard = withSiteIdUpdater(DashboardPure);
|
||||
|
|
@ -73,9 +74,12 @@ const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
|
|||
onboarding: state.getIn([ 'user', 'onboarding' ])
|
||||
};
|
||||
}, {
|
||||
fetchUserInfo, fetchTenants
|
||||
fetchUserInfo, fetchTenants, setSessionPath
|
||||
})
|
||||
class Router extends React.Component {
|
||||
state = {
|
||||
destinationPath: null
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
if (props.isLoggedIn) {
|
||||
|
|
@ -85,10 +89,23 @@ class Router extends React.Component {
|
|||
props.fetchTenants();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidMount() {
|
||||
const { isLoggedIn, location } = this.props;
|
||||
if (!isLoggedIn) {
|
||||
this.setState({ destinationPath: location.pathname });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.props.setSessionPath(prevProps.location.pathname)
|
||||
if (prevProps.email !== this.props.email && !this.props.email) {
|
||||
this.props.fetchTenants();
|
||||
}
|
||||
|
||||
if (!prevProps.isLoggedIn && this.props.isLoggedIn && this.state.destinationPath !== routes.login()) {
|
||||
this.props.history.push(this.state.destinationPath);
|
||||
this.setState({ destinationPath: null });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.btnLink {
|
||||
cursor: pointer;
|
||||
color: $green;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './AssistTabs';
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionList';
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export default class Dashboard extends React.PureComponent {
|
|||
<div className="page-margin container-90">
|
||||
<div className="side-menu">
|
||||
<SideMenuSection
|
||||
title="Sessions"
|
||||
title="Metrics"
|
||||
onItemClick={this.onMenuItemClick}
|
||||
items={menuList}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import cn from 'classnames';
|
|||
import { connect } from 'react-redux';
|
||||
import { Set, List as ImmutableList } from "immutable";
|
||||
import { NoContent, Loader, Checkbox, LoadMoreButton, IconButton, Input, DropdownPlain } from 'UI';
|
||||
import { merge, resolve,unresolve,ignore } from "Duck/errors";
|
||||
import { merge, resolve, unresolve, ignore, updateCurrentPage } from "Duck/errors";
|
||||
import { applyFilter } from 'Duck/filters';
|
||||
import { IGNORED, RESOLVED, UNRESOLVED } from 'Types/errorInfo';
|
||||
import SortDropdown from 'Components/BugFinder/Filters/SortDropdown';
|
||||
|
|
@ -30,18 +30,19 @@ const sortOptions = Object.entries(sortOptionsMap)
|
|||
state.getIn(["errors", "unresolve", "loading"]),
|
||||
ignoreLoading: state.getIn([ "errors", "ignore", "loading" ]),
|
||||
mergeLoading: state.getIn([ "errors", "merge", "loading" ]),
|
||||
currentPage: state.getIn(["errors", "currentPage"]),
|
||||
}), {
|
||||
merge,
|
||||
resolve,
|
||||
unresolve,
|
||||
ignore,
|
||||
applyFilter
|
||||
applyFilter,
|
||||
updateCurrentPage,
|
||||
})
|
||||
export default class List extends React.PureComponent {
|
||||
state = {
|
||||
checkedAll: false,
|
||||
checkedIds: Set(),
|
||||
showPages: 1,
|
||||
sort: {}
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +107,7 @@ export default class List extends React.PureComponent {
|
|||
this.applyToAllChecked(this.props.ignore);
|
||||
}
|
||||
|
||||
addPage = () => this.setState({ showPages: this.state.showPages + 1 })
|
||||
addPage = () => this.props.updateCurrentPage(this.props.currentPage + 1)
|
||||
|
||||
writeOption = (e, { name, value }) => {
|
||||
const [ sort, order ] = value.split('-');
|
||||
|
|
@ -123,16 +124,16 @@ export default class List extends React.PureComponent {
|
|||
resolveToggleLoading,
|
||||
mergeLoading,
|
||||
onFilterChange,
|
||||
currentPage,
|
||||
} = this.props;
|
||||
const {
|
||||
checkedAll,
|
||||
checkedIds,
|
||||
showPages,
|
||||
sort
|
||||
} = this.state;
|
||||
const someLoading = loading || ignoreLoading || resolveToggleLoading || mergeLoading;
|
||||
const currentCheckedIds = this.currentCheckedIds();
|
||||
const displayedCount = Math.min(showPages * PER_PAGE, list.size);
|
||||
const displayedCount = Math.min(currentPage * PER_PAGE, list.size);
|
||||
let _list = sort.sort ? list.sortBy(i => i[sort.sort]) : list;
|
||||
_list = sort.order === 'desc' ? _list.reverse() : _list;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -69,11 +71,17 @@ export default withRequest({
|
|||
dataName: 'assistCredendials',
|
||||
loadingName: 'loadingCredentials',
|
||||
})(withPermissions(['SESSION_REPLAY', 'ASSIST_LIVE'], '', true)(connect(
|
||||
state => ({
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
showAssist: state.getIn([ 'sessions', 'showChatWindow' ]),
|
||||
jwt: state.get('jwt'),
|
||||
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
|
||||
}),
|
||||
state => {
|
||||
const isAssist = state.getIn(['sessions', 'activeTab']).type === 'live';
|
||||
const hasSessioPath = state.getIn([ 'sessions', 'sessionPath' ]).includes('/sessions');
|
||||
return {
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
showAssist: state.getIn([ 'sessions', 'showChatWindow' ]),
|
||||
jwt: state.get('jwt'),
|
||||
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
|
||||
hasSessionsPath: hasSessioPath && !isAssist,
|
||||
isEnterprise: state.getIn([ 'user', 'client', 'edition' ]) === 'ee',
|
||||
}
|
||||
},
|
||||
{ toggleFullscreen, closeBottomBlock },
|
||||
)(WebPlayer)));
|
||||
)(WebPlayer)));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -59,11 +60,14 @@ function Session({
|
|||
|
||||
export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state, props) => {
|
||||
const { match: { params: { sessionId } } } = props;
|
||||
const isAssist = state.getIn(['sessions', 'activeTab']).type === 'live';
|
||||
const hasSessiosPath = state.getIn([ 'sessions', 'sessionPath' ]).includes('/sessions');
|
||||
return {
|
||||
sessionId,
|
||||
loading: state.getIn([ 'sessions', 'loading' ]),
|
||||
hasErrors: !!state.getIn([ 'sessions', 'errors' ]),
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
hasSessionsPath: hasSessiosPath && !isAssist,
|
||||
};
|
||||
}, {
|
||||
fetchSession,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
max-width: 80%;
|
||||
width: fit-content;
|
||||
}
|
||||
.popupNameContent {
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
@ -27,18 +28,24 @@ function capitalise(str) {
|
|||
live: state.live,
|
||||
loading: state.cssLoading || state.messagesLoading,
|
||||
}))
|
||||
@connect((state, props) => ({
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
loading: state.getIn([ 'sessions', 'toggleFavoriteRequest', 'loading' ]),
|
||||
disabled: state.getIn([ 'components', 'targetDefiner', 'inspectorMode' ]) || props.loading,
|
||||
jiraConfig: state.getIn([ 'issues', 'list' ]).first(),
|
||||
issuesFetched: state.getIn([ 'issues', 'issuesFetched' ]),
|
||||
local: state.getIn(['sessions', 'timezone']),
|
||||
funnelRef: state.getIn(['funnels', 'navRef']),
|
||||
siteId: state.getIn([ 'user', 'siteId' ]),
|
||||
funnelPage: state.getIn(['sessions', 'funnelPage']),
|
||||
}), {
|
||||
toggleFavorite, fetchListIntegration
|
||||
@connect((state, props) => {
|
||||
const isAssist = state.getIn(['sessions', 'activeTab']).type === 'live';
|
||||
const hasSessioPath = state.getIn([ 'sessions', 'sessionPath' ]).includes('/sessions');
|
||||
return {
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
sessionPath: state.getIn([ 'sessions', 'sessionPath' ]),
|
||||
loading: state.getIn([ 'sessions', 'toggleFavoriteRequest', 'loading' ]),
|
||||
disabled: state.getIn([ 'components', 'targetDefiner', 'inspectorMode' ]) || props.loading,
|
||||
jiraConfig: state.getIn([ 'issues', 'list' ]).first(),
|
||||
issuesFetched: state.getIn([ 'issues', 'issuesFetched' ]),
|
||||
local: state.getIn(['sessions', 'timezone']),
|
||||
funnelRef: state.getIn(['funnels', 'navRef']),
|
||||
siteId: state.getIn([ 'user', 'siteId' ]),
|
||||
funnelPage: state.getIn(['sessions', 'funnelPage']),
|
||||
hasSessionsPath: hasSessioPath && !isAssist,
|
||||
}
|
||||
}, {
|
||||
toggleFavorite, fetchListIntegration, setSessionPath
|
||||
})
|
||||
@withRouter
|
||||
export default class PlayerBlockHeader extends React.PureComponent {
|
||||
|
|
@ -54,16 +61,22 @@ export default class PlayerBlockHeader extends React.PureComponent {
|
|||
);
|
||||
|
||||
backHandler = () => {
|
||||
const { history, siteId, funnelPage } = this.props;
|
||||
const funnelId = funnelPage && funnelPage.get('funnelId');
|
||||
const issueId = funnelPage && funnelPage.get('issueId');
|
||||
if (funnelId || issueId) {
|
||||
if (issueId) {
|
||||
history.push(withSiteId(funnelIssueRoute(funnelId, issueId), siteId))
|
||||
} else
|
||||
history.push(withSiteId(funnelRoute(funnelId), siteId));
|
||||
} else
|
||||
const { history, siteId, funnelPage, sessionPath } = this.props;
|
||||
// alert(sessionPath)
|
||||
if (sessionPath === history.location.pathname) {
|
||||
history.push(withSiteId(SESSIONS_ROUTE), siteId);
|
||||
} else {
|
||||
history.push(sessionPath ? sessionPath : withSiteId(SESSIONS_ROUTE, siteId));
|
||||
}
|
||||
// const funnelId = funnelPage && funnelPage.get('funnelId');
|
||||
// const issueId = funnelPage && funnelPage.get('issueId');
|
||||
// if (funnelId || issueId) {
|
||||
// if (issueId) {
|
||||
// history.push(withSiteId(funnelIssueRoute(funnelId, issueId), siteId))
|
||||
// } else
|
||||
// history.push(withSiteId(funnelRoute(funnelId), siteId));
|
||||
// } else
|
||||
// history.push(withSiteId(SESSIONS_ROUTE), siteId);
|
||||
}
|
||||
|
||||
toggleFavorite = () => {
|
||||
|
|
@ -86,21 +99,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 +131,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 +165,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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -99,8 +99,6 @@ $offset: 10px;
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.activeRow {
|
||||
background-color: $teal;
|
||||
background-color: rgba(54, 108, 217, 0.1);
|
||||
}
|
||||
|
|
@ -12,8 +12,8 @@
|
|||
|
||||
.navButtons {
|
||||
position: absolute;
|
||||
right: 40px;
|
||||
top: 10px;
|
||||
right: 260px;
|
||||
top: -34px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { Duration } from 'luxon';
|
|||
|
||||
interface Props {
|
||||
startTime: any,
|
||||
className: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
function Counter({ startTime, className }: Props) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.2s;
|
||||
/* opacity: 0; */
|
||||
cursor: pointer;
|
||||
&[data-viewed=true] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const IGNORE = "errors/IGNORE";
|
|||
const MERGE = "errors/MERGE";
|
||||
const TOGGLE_FAVORITE = "errors/TOGGLE_FAVORITE";
|
||||
const FETCH_TRACE = "errors/FETCH_TRACE";
|
||||
const UPDATE_CURRENT_PAGE = "errors/UPDATE_CURRENT_PAGE";
|
||||
|
||||
function chartWrapper(chart = []) {
|
||||
return chart.map(point => ({ ...point, count: Math.max(point.count, 0) }));
|
||||
|
|
@ -33,7 +34,8 @@ const initialState = Map({
|
|||
instance: ErrorInfo(),
|
||||
instanceTrace: List(),
|
||||
stats: Map(),
|
||||
sourcemapUploaded: true
|
||||
sourcemapUploaded: true,
|
||||
currentPage: 1,
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -67,7 +69,8 @@ function reducer(state = initialState, action = {}) {
|
|||
return state.update("list", list => list.filter(e => !ids.includes(e.errorId)));
|
||||
case success(FETCH_NEW_ERRORS_COUNT):
|
||||
return state.set('stats', action.data);
|
||||
|
||||
case UPDATE_CURRENT_PAGE:
|
||||
return state.set('currentPage', action.page);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
|
@ -166,3 +169,9 @@ export function fetchNewErrorsCount(params = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
export function updateCurrentPage(page) {
|
||||
return {
|
||||
type: 'errors/UPDATE_CURRENT_PAGE',
|
||||
page,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +109,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);
|
||||
|
|
@ -117,7 +117,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({})
|
||||
|
|
|
|||
|
|
@ -94,13 +94,14 @@ export default class AssistManager {
|
|||
return;
|
||||
}
|
||||
this.setStatus(ConnectionStatus.Connecting)
|
||||
// @ts-ignore
|
||||
const urlObject = new URL(window.ENV.API_EDP)
|
||||
import('peerjs').then(({ default: Peer }) => {
|
||||
if (this.closed) {return}
|
||||
const _config = {
|
||||
// @ts-ignore
|
||||
host: new URL(window.ENV.API_EDP).host,
|
||||
host: urlObject.hostname,
|
||||
path: '/assist',
|
||||
port: location.protocol === 'https:' ? 443 : 80,
|
||||
port: urlObject.port === "" ? (location.protocol === 'https:' ? 443 : 80 ): parseInt(urlObject.port),
|
||||
}
|
||||
|
||||
if (this.config) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
5
frontend/app/svg/icons/os/fedora.svg
Normal file
5
frontend/app/svg/icons/os/fedora.svg
Normal 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 |
|
|
@ -34,8 +34,8 @@ export default Record({
|
|||
startDate,
|
||||
endDate,
|
||||
|
||||
sort: undefined,
|
||||
order: undefined,
|
||||
sort: 'startTs',
|
||||
order: 'desc',
|
||||
|
||||
viewed: undefined,
|
||||
consoleLogCount: undefined,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
- Initialize databases
|
||||
- we've to pass the --wait flag, else the db installation won't be complete. and it'll break the db init.
|
||||
- collate all dbs required
|
||||
- How to distinguish b/w enterprise and community
|
||||
- Or fist only community then enterprise
|
||||
- install db migration
|
||||
- have to have another helm chart with low hook value for higher prioriry
|
||||
- install app
|
||||
- customize values.yaml file
|
||||
|
||||
|
||||
## Installation
|
||||
helm upgrade --install databases ./databases -n db --create-namespace --wait -f ./values.yaml --atomic
|
||||
|
||||
helm upgrade --install openreplay ./openreplay -n app --create-namespace --wait -f ./values.yaml --atomic
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
## Licenses (as of October 28, 2021)
|
||||
## Licenses (as of January 16, 2022)
|
||||
|
||||
Below is the list of dependencies used in OpenReplay software. Licenses may change between versions, so please keep this up to date with every new library you use.
|
||||
|
||||
|
|
@ -28,8 +28,16 @@ Below is the list of dependencies used in OpenReplay software. Licenses may chan
|
|||
| pyjwt | MIT | Python |
|
||||
| jsbeautifier | MIT | Python |
|
||||
| psycopg2-binary | LGPL | Python |
|
||||
| pytz | MIT | Python |
|
||||
| fastapi | MIT | Python |
|
||||
| uvicorn | BSD | Python |
|
||||
| python-decouple | MIT | Python |
|
||||
| pydantic | MIT | Python |
|
||||
| apscheduler | MIT | Python |
|
||||
| python-multipart | Apache | Python |
|
||||
| elasticsearch-py | Apache2 | Python |
|
||||
| jira | BSD2 | Python |
|
||||
| clickhouse-driver | MIT | Python |
|
||||
| python3-saml | MIT | Python |
|
||||
| kubernetes | Apache2 | Python |
|
||||
| chalice | Apache2 | Python |
|
||||
| pandas | BSD3 | Python |
|
||||
|
|
@ -80,11 +88,6 @@ Below is the list of dependencies used in OpenReplay software. Licenses may chan
|
|||
| source-map | BSD3 | JavaScript |
|
||||
| aws-sdk | Apache2 | JavaScript |
|
||||
| serverless | MIT | JavaScript |
|
||||
| schedule | MIT | Python |
|
||||
| croniter | MIT | Python |
|
||||
| lib/pq | MIT | Go |
|
||||
| peerjs | MIT | JavaScript |
|
||||
| antonmedv/finder | MIT | JavaScript |
|
||||
| elasticsearch-py | Apache2 | Python |
|
||||
| sentry-python | BSD2 | Python |
|
||||
| jira | BSD2 | Python |
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue