diff --git a/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx b/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx new file mode 100644 index 000000000..ff62a899a --- /dev/null +++ b/frontend/app/components/Assist/components/AssistTabs/AssistTabs.tsx @@ -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 ( +
+
+
setShowMenu(!showMenu)} + > + More Live Sessions +
+ by +
+ +
{props.userId}
+
+
+ Live Sessions by {props.userId}
} + isDisplayed={ showMenu } + content={ showMenu && } + onClose={ () => setShowMenu(false) } + /> + + ); +}; + +export default AssistTabs; \ No newline at end of file diff --git a/frontend/app/components/Assist/components/AssistTabs/assistTabs.css b/frontend/app/components/Assist/components/AssistTabs/assistTabs.css new file mode 100644 index 000000000..462879395 --- /dev/null +++ b/frontend/app/components/Assist/components/AssistTabs/assistTabs.css @@ -0,0 +1,5 @@ +.btnLink { + cursor: pointer; + color: $green; + text-decoration: underline; +} \ No newline at end of file diff --git a/frontend/app/components/Assist/components/AssistTabs/index.ts b/frontend/app/components/Assist/components/AssistTabs/index.ts new file mode 100644 index 000000000..b089b3734 --- /dev/null +++ b/frontend/app/components/Assist/components/AssistTabs/index.ts @@ -0,0 +1 @@ +export { default } from './AssistTabs'; \ No newline at end of file diff --git a/frontend/app/components/Assist/components/SessionList/SessionList.tsx b/frontend/app/components/Assist/components/SessionList/SessionList.tsx new file mode 100644 index 000000000..f556a8f1d --- /dev/null +++ b/frontend/app/components/Assist/components/SessionList/SessionList.tsx @@ -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 ( + + +
+ { props.list.map(session => ) } +
+
+
+ ); +} + +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); \ No newline at end of file diff --git a/frontend/app/components/Assist/components/SessionList/index.ts b/frontend/app/components/Assist/components/SessionList/index.ts new file mode 100644 index 000000000..779c9df2a --- /dev/null +++ b/frontend/app/components/Assist/components/SessionList/index.ts @@ -0,0 +1 @@ +export { default } from './SessionList'; \ No newline at end of file diff --git a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js index fb89f6967..7275d9ac0 100644 --- a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js +++ b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js @@ -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); diff --git a/frontend/app/components/Session/LivePlayer.js b/frontend/app/components/Session/LivePlayer.js index cbc3410cd..34c56721e 100644 --- a/frontend/app/components/Session/LivePlayer.js +++ b/frontend/app/components/Session/LivePlayer.js @@ -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 ); -} +}; 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))); \ No newline at end of file +)(WebPlayer))); diff --git a/frontend/app/components/Session/Session.js b/frontend/app/components/Session/Session.js index 6b90ff4c1..e7af00642 100644 --- a/frontend/app/components/Session/Session.js +++ b/frontend/app/components/Session/Session.js @@ -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 ( { session.isIOS ? - : (session.live ? : ) + : (session.live && !hasSessionsPath ? : ) } @@ -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, diff --git a/frontend/app/components/Session_/Fetch/Fetch.js b/frontend/app/components/Session_/Fetch/Fetch.js index 71e7a2fb9..8bc4f2117 100644 --- a/frontend/app/components/Session_/Fetch/Fetch.js +++ b/frontend/app/components/Session_/Fetch/Fetch.js @@ -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 ( } - isDisplayed={ current != null } - content={ current && + isDisplayed={ current != null && showFetchDetails } + content={ current && showFetchDetails && } onClose={ this.closeModal } @@ -88,25 +121,31 @@ export default class Fetch extends React.PureComponent {

Fetch

- +
+ {/*
+
Prev
+
Next
+
*/} + +
{[ @@ -120,7 +159,7 @@ export default class Fetch extends React.PureComponent { width: 60, }, { label: "Name", - width: 130, + width: 180, render: renderName, }, { diff --git a/frontend/app/components/Session_/Network/Network.js b/frontend/app/components/Session_/Network/Network.js index 878266bda..150cadc59 100644 --- a/frontend/app/components/Session_/Network/Network.js +++ b/frontend/app/components/Session_/Network/Network.js @@ -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 ( - { r.name } } - content={
{ r.url }
} - size="mini" - position="right center" - /> - ); -} - -const renderXHRText = () => ( - - {XHR} - - Use our Fetch plugin - {' to capture HTTP requests and responses, including status codes and bodies.'}
- We also provide support for GraphQL - {' for easy debugging of your queries.'} - - } - className="ml-1" - /> -
-); - -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 = ( -
    - { showTransferred && -
  • {`${formatBytes( r.encodedBodySize + headerSize )} transfered over network`}
  • - } -
  • {`Resource size: ${formatBytes(r.decodedBodySize)} `}
  • -
- ); - } - - return ( - { triggerText } } - content={ content } - size="mini" - position="right center" - /> +
+ { r.name }
} + content={
{ r.url }
} + size="mini" + position="right center" + /> +
{ + e.stopPropagation(); + jump(r.time) + }} + >Jump
+ ); } @@ -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 ( - {/* - - - - - - - - 0 } - /> - 0 } - /> - - - - - - {[ - { - 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, - } - ]} - - - */} ); } diff --git a/frontend/app/components/Session_/Network/network.css b/frontend/app/components/Session_/Network/network.css index eb37e49c9..e299211da 100644 --- a/frontend/app/components/Session_/Network/network.css +++ b/frontend/app/components/Session_/Network/network.css @@ -22,7 +22,7 @@ white-space: nowrap; text-overflow: ellipsis; overflow: hidden; - max-width: 100%; + max-width: 80%; width: fit-content; } .popupNameContent { diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index c25101d69..27c1de1ab 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -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) } > +
-
+
@@ -115,11 +120,17 @@ export default class PlayerBlockHeader extends React.PureComponent {
- { live && } - { !live && ( + { live && hasSessionsPath && ( +
this.props.setSessionPath('')}> + This Session is Now Continuing Live +
+ )} + { _live && } + { _live && } + { !_live && ( <> -
+
)} - { !live && jiraConfig && jiraConfig.token && } + { !_live && jiraConfig && jiraConfig.token && }
diff --git a/frontend/app/components/Session_/TimeTable/TimeTable.js b/frontend/app/components/Session_/TimeTable/TimeTable.js index 9753e3d44..e316dae56 100644 --- a/frontend/app/components/Session_/TimeTable/TimeTable.js +++ b/frontend/app/components/Session_/TimeTable/TimeTable.js @@ -135,7 +135,7 @@ export default class TimeTable extends React.PureComponent { ...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 {
onRowClick(row, index) : null } id="table-row" > @@ -223,7 +223,7 @@ export default class TimeTable extends React.PureComponent { navigation=false, referenceLines = [], additionalHeight = 0, - activeIndex + activeIndex, } = this.props; const { timewidth, @@ -247,7 +247,7 @@ export default class TimeTable extends React.PureComponent { return (
{ navigation && -
+
(
{label}
) @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 {
- +
- +
diff --git a/frontend/app/components/shared/SessionItem/sessionItem.css b/frontend/app/components/shared/SessionItem/sessionItem.css index 8f9824bec..cbf7bb2d1 100644 --- a/frontend/app/components/shared/SessionItem/sessionItem.css +++ b/frontend/app/components/shared/SessionItem/sessionItem.css @@ -92,7 +92,7 @@ display: flex; align-items: center; transition: all 0.2s; - /* opacity: 0; */ + cursor: pointer; &[data-viewed=true] { opacity: 1; } diff --git a/frontend/app/components/ui/IconButton/iconButton.css b/frontend/app/components/ui/IconButton/iconButton.css index 41ed89e45..3aaff8347 100644 --- a/frontend/app/components/ui/IconButton/iconButton.css +++ b/frontend/app/components/ui/IconButton/iconButton.css @@ -7,6 +7,7 @@ border-radius: 3px; display: flex; align-items: center; + justify-content: center; cursor: pointer; height: 36px; font-size: 14px; diff --git a/frontend/app/duck/funnels.js b/frontend/app/duck/funnels.js index 4e591237c..7dad8550a 100644 --- a/frontend/app/duck/funnels.js +++ b/frontend/app/duck/funnels.js @@ -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]; diff --git a/frontend/app/duck/sessions.js b/frontend/app/duck/sessions.js index e82602799..977af85bb 100644 --- a/frontend/app/duck/sessions.js +++ b/frontend/app/duck/sessions.js @@ -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 + } +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/MessageDistributor.ts b/frontend/app/player/MessageDistributor/MessageDistributor.ts index c742c10b5..35ada8ce3 100644 --- a/frontend/app/player/MessageDistributor/MessageDistributor.ts +++ b/frontend/app/player/MessageDistributor/MessageDistributor.ts @@ -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({}) diff --git a/frontend/app/player/singletone.js b/frontend/app/player/singletone.js index 619f9b02b..9d811023e 100644 --- a/frontend/app/player/singletone.js +++ b/frontend/app/player/singletone.js @@ -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, diff --git a/frontend/app/svg/icons/os/fedora.svg b/frontend/app/svg/icons/os/fedora.svg new file mode 100644 index 000000000..86e9f115e --- /dev/null +++ b/frontend/app/svg/icons/os/fedora.svg @@ -0,0 +1,5 @@ + + + Fedora logo (2021) + + diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index 42637a2fa..8b3301aad 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -34,8 +34,8 @@ export default Record({ startDate, endDate, - sort: undefined, - order: undefined, + sort: 'startTs', + order: 'desc', viewed: undefined, consoleLogCount: undefined, diff --git a/frontend/app/types/session/session.js b/frontend/app/types/session/session.js index 1fabc79a6..61ca9e489 100644 --- a/frontend/app/types/session/session.js +++ b/frontend/app/types/session/session.js @@ -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);