commit
6045f1c7f0
48 changed files with 33638 additions and 309 deletions
|
|
@ -2,10 +2,13 @@ import { configure, addDecorator } from '@storybook/react';
|
|||
import { Provider } from 'react-redux';
|
||||
import store from '../app/store';
|
||||
import { MemoryRouter } from "react-router"
|
||||
import { PlayerProvider } from '../app/player/store'
|
||||
|
||||
const withProvider = (story) => (
|
||||
<Provider store={store}>
|
||||
{ story() }
|
||||
<PlayerProvider>
|
||||
{ story() }
|
||||
</PlayerProvider>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ const siteIdRequiredPaths = [
|
|||
'/sourcemaps',
|
||||
'/errors',
|
||||
'/funnels',
|
||||
'/assist'
|
||||
'/assist',
|
||||
'/heatmaps'
|
||||
];
|
||||
|
||||
const noStoringFetchPathStarts = [
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ const FunnelHeader = (props) => {
|
|||
endDate={funnelFilters.endDate}
|
||||
onDateChange={onDateChange}
|
||||
customRangeRight
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,12 +31,7 @@ function Layout({ children, player, toolbar }) {
|
|||
</div>
|
||||
{ !player.fullscreen.enabled && <ToolPanel player={ player } toolbar={ toolbar }/> }
|
||||
</div>
|
||||
{ !player.fullscreen.enabled &&
|
||||
<Events
|
||||
style={{ width: "270px" }}
|
||||
player={ player }
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
43
frontend/app/components/Session/RightBlock.tsx
Normal file
43
frontend/app/components/Session/RightBlock.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React, { useState } from 'react'
|
||||
import EventsBlock from '../Session_/EventsBlock';
|
||||
import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel'
|
||||
import { Controls as PlayerControls } from 'Player';
|
||||
import { Tabs } from 'UI';
|
||||
import { connectPlayer } from 'Player';
|
||||
|
||||
const EVENTS = 'Events';
|
||||
const HEATMAPS = 'Heatmaps';
|
||||
|
||||
const TABS = [ EVENTS, HEATMAPS ].map(tab => ({ text: tab, key: tab }));
|
||||
|
||||
|
||||
const EventsBlockConnected = connectPlayer(state => ({
|
||||
currentTimeEventIndex: state.eventListNow.length > 0 ? state.eventListNow.length - 1 : 0,
|
||||
playing: state.playing,
|
||||
}))(EventsBlock)
|
||||
|
||||
export default function RightBlock() {
|
||||
const [activeTab, setActiveTab] = useState(EVENTS)
|
||||
|
||||
const renderActiveTab = (tab) => {
|
||||
switch(tab) {
|
||||
case EVENTS:
|
||||
return <EventsBlockConnected player={PlayerControls}/>
|
||||
case HEATMAPS:
|
||||
return <PageInsightsPanel />
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={{ width: '270px', height: 'calc(100vh- 50px)'}} className="flex flex-col">
|
||||
<Tabs
|
||||
tabs={ TABS }
|
||||
active={ activeTab }
|
||||
onClick={ (tab) => setActiveTab(tab) }
|
||||
border={ true }
|
||||
/>
|
||||
{
|
||||
renderActiveTab(activeTab)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,22 +8,13 @@ import {
|
|||
init as initPlayer,
|
||||
clean as cleanPlayer,
|
||||
} from 'Player';
|
||||
import { Controls as PlayerControls, toggleEvents } from 'Player';
|
||||
import cn from 'classnames'
|
||||
import RightBlock from './RightBlock'
|
||||
|
||||
|
||||
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
|
||||
import EventsBlock from '../Session_/EventsBlock';
|
||||
import PlayerBlock from '../Session_/PlayerBlock';
|
||||
import styles from '../Session_/session.css';
|
||||
import EventsToggleButton from './EventsToggleButton';
|
||||
|
||||
|
||||
|
||||
const EventsBlockConnected = connectPlayer(state => ({
|
||||
currentTimeEventIndex: state.eventListNow.length > 0 ? state.eventListNow.length - 1 : 0,
|
||||
playing: state.playing,
|
||||
}))(EventsBlock)
|
||||
|
||||
|
||||
const InitLoader = connectPlayer(state => ({
|
||||
|
|
@ -32,14 +23,14 @@ const InitLoader = connectPlayer(state => ({
|
|||
|
||||
const PlayerContentConnected = connectPlayer(state => ({
|
||||
showEvents: !state.showEvents
|
||||
}), { toggleEvents })(PlayerContent);
|
||||
}))(PlayerContent);
|
||||
|
||||
|
||||
function PlayerContent({ live, fullscreen, showEvents, toggleEvents }) {
|
||||
function PlayerContent({ live, fullscreen, showEvents }) {
|
||||
return (
|
||||
<div className={ cn(styles.session, 'relative') } data-fullscreen={fullscreen}>
|
||||
<PlayerBlock />
|
||||
{ showEvents && !live && !fullscreen && <EventsBlockConnected player={PlayerControls}/> }
|
||||
{ showEvents && !live && !fullscreen && <RightBlock /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './AutoplayTimer'
|
||||
|
|
@ -6,7 +6,7 @@ export default function EventSearch(props) {
|
|||
const [showSearch, setShowSearch] = useState(false)
|
||||
return (
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex-1 relative">
|
||||
<div className="flex flex-1 relative items-center" style={{ height: '32px' }}>
|
||||
{ showSearch ?
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from "react-virtualized";
|
||||
import { Avatar, Input, Dropdown, Icon } from 'UI';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { setSelected } from 'Duck/events';
|
||||
import { setEventFilter } from 'Duck/sessions';
|
||||
|
|
@ -18,8 +17,7 @@ import EventSearch from './EventSearch/EventSearch';
|
|||
eventsIndex: state.getIn([ 'sessions', 'eventsIndex' ]),
|
||||
selectedEvents: state.getIn([ 'events', 'selected' ]),
|
||||
targetDefinerDisplayed: state.getIn([ 'components', 'targetDefiner', 'isDisplayed' ]),
|
||||
testsAvaliable: false,
|
||||
//state.getIn([ 'user', 'account', 'appearance', 'tests' ]),
|
||||
testsAvaliable: false,
|
||||
}), {
|
||||
showTargetDefiner,
|
||||
setSelected,
|
||||
|
|
@ -74,9 +72,6 @@ export default class EventsBlock extends React.PureComponent {
|
|||
this.setState({ editingEvent: null });
|
||||
}
|
||||
if (prevProps.session !== this.props.session) { // Doesn't happen
|
||||
// this.setState({
|
||||
// groups: groupEvents(this.props.session.events),
|
||||
// });
|
||||
this.cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
defaultHeight: 300
|
||||
|
|
@ -148,8 +143,7 @@ export default class EventsBlock extends React.PureComponent {
|
|||
<CellMeasurer
|
||||
key={key}
|
||||
cache={this.cache}
|
||||
parent={parent}
|
||||
//columnIndex={0}
|
||||
parent={parent}
|
||||
rowIndex={index}
|
||||
>
|
||||
{({measure, registerChild}) => (
|
||||
|
|
@ -176,14 +170,12 @@ export default class EventsBlock extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { query } = this.state;
|
||||
const {
|
||||
playing,
|
||||
const {
|
||||
testsAvaliable,
|
||||
session: {
|
||||
events,
|
||||
userNumericHash,
|
||||
userDisplayName,
|
||||
userUuid,
|
||||
userDisplayName,
|
||||
userId,
|
||||
userAnonymousId
|
||||
},
|
||||
|
|
@ -193,7 +185,7 @@ export default class EventsBlock extends React.PureComponent {
|
|||
const _events = filteredEvents || events;
|
||||
|
||||
return (
|
||||
<div className={ cn("flex flex-col", styles.eventsBlock) }>
|
||||
<>
|
||||
<div className={ cn(styles.header, 'p-3') }>
|
||||
<UserCard
|
||||
className=""
|
||||
|
|
@ -203,8 +195,7 @@ export default class EventsBlock extends React.PureComponent {
|
|||
userAnonymousId={userAnonymousId}
|
||||
/>
|
||||
|
||||
<div className={ cn(styles.hAndProgress, 'mt-3') }>
|
||||
{/* <div className="text-lg">{ `User Events (${ events.size })` }</div> */}
|
||||
<div className={ cn(styles.hAndProgress, 'mt-3') }>
|
||||
<EventSearch
|
||||
onChange={this.write}
|
||||
clearSearch={this.clearSearch}
|
||||
|
|
@ -213,29 +204,7 @@ export default class EventsBlock extends React.PureComponent {
|
|||
<div className="text-lg">{ `User Events (${ events.size })` }</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex mt-3">
|
||||
{/* <Dropdown
|
||||
trigger={
|
||||
<div className={cn("py-3 px-3 bg-white flex items-center text-sm mb-2 border rounded ml-2")} style={{ height: '32px' }}>
|
||||
<Icon name="filter" size="12" color="teal" />
|
||||
</div>
|
||||
}
|
||||
options={ [
|
||||
// { text: 'Visited', value: TYPES.LOCATION },
|
||||
{ text: 'Clicked', value: TYPES.CLICK },
|
||||
{ text: 'Input', value: TYPES.INPUT },
|
||||
] }
|
||||
name="filter"
|
||||
icon={null}
|
||||
onChange={this.onSetEventFilter}
|
||||
basic
|
||||
direction="left"
|
||||
scrolling
|
||||
selectOnBlur={true}
|
||||
closeOnChange={true}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={ cn("flex-1 px-3 pb-3", styles.eventsList) }
|
||||
|
|
@ -263,7 +232,7 @@ export default class EventsBlock extends React.PureComponent {
|
|||
</AutoSizer>
|
||||
</div>
|
||||
{ testsAvaliable && <AutomateButton /> }
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { Dropdown, Loader } from 'UI'
|
||||
import DateRange from 'Shared/DateRange';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchInsights } from 'Duck/sessions';
|
||||
import SelectorsList from './components/SelectorsList/SelectorsList';
|
||||
import { markTargets, Controls as Player } from 'Player';
|
||||
|
||||
const JUMP_OFFSET = 1000;
|
||||
interface Props {
|
||||
filters: any
|
||||
fetchInsights: (filters) => void
|
||||
urls: []
|
||||
insights: any
|
||||
events: Array<any>
|
||||
urlOptions: Array<any>
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
function PageInsightsPanel({ filters, fetchInsights, events = [], insights, urlOptions, loading = true }: Props) {
|
||||
const [insightsFilters, setInsightsFilters] = useState(filters)
|
||||
|
||||
const onDateChange = (e) => {
|
||||
setInsightsFilters({ ...insightsFilters, startDate: e.startDate, endDate: e.endDate })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
markTargets(insights.toJS());
|
||||
return () => {
|
||||
markTargets(null)
|
||||
}
|
||||
}, [insights])
|
||||
|
||||
useEffect(() => {
|
||||
const url = insightsFilters.url ? insightsFilters.url : urlOptions[0].value
|
||||
fetchInsights({ ...insightsFilters, url })
|
||||
}, [insightsFilters])
|
||||
|
||||
const onPageSelect = (e, { name, value }) => {
|
||||
const event = events.find(item => item.url === value)
|
||||
Player.jump(event.time + JUMP_OFFSET)
|
||||
setInsightsFilters({ ...insightsFilters, url: value })
|
||||
markTargets([])
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-4 bg-gray-lightest">
|
||||
<div className="my-3 flex -ml-2">
|
||||
<DateRange
|
||||
// rangeValue={filters.rangeValue}
|
||||
startDate={filters.startDate}
|
||||
endDate={filters.endDate}
|
||||
onDateChange={onDateChange}
|
||||
customRangeRight
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center">
|
||||
<div className="mr-2 flex-shrink-0">In Page</div>
|
||||
<Dropdown
|
||||
labeled
|
||||
placeholder="change"
|
||||
selection
|
||||
options={ urlOptions }
|
||||
name="url"
|
||||
defaultValue={urlOptions[0].value}
|
||||
onChange={ onPageSelect }
|
||||
id="change-dropdown"
|
||||
className="customDropdown"
|
||||
style={{ minWidth: '80px', width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<Loader loading={ loading }>
|
||||
<SelectorsList />
|
||||
</Loader>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => {
|
||||
const events = state.getIn([ 'sessions', 'visitedEvents' ])
|
||||
return {
|
||||
filters: state.getIn(['sessions', 'insightFilters']),
|
||||
insights: state.getIn([ 'sessions', 'insights' ]),
|
||||
events: events,
|
||||
urlOptions: events.map(({ url }) => ({ text: url, value: url})),
|
||||
loading: state.getIn([ 'sessions', 'fetchInsightsRequest', 'loading' ]),
|
||||
}
|
||||
}, { fetchInsights })(PageInsightsPanel);
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
.wrapper {
|
||||
padding: 10px;
|
||||
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
background-color: white;
|
||||
margin-bottom: 15px;
|
||||
|
||||
& .top {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
& .index {
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: $tealx;
|
||||
flex-shrink: 0;
|
||||
border: solid thin white;
|
||||
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
& .counts {
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
margin: 20px 0;
|
||||
|
||||
& div:first-child {
|
||||
font-size: 18px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #f9ffff;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import React, { useState } from 'react'
|
||||
import stl from './SelectorCard.css'
|
||||
import cn from 'classnames';
|
||||
import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen';
|
||||
import { activeTarget } from 'Player';
|
||||
|
||||
interface Props {
|
||||
index?: number,
|
||||
target: MarkedTarget,
|
||||
showContent: boolean
|
||||
}
|
||||
|
||||
export default function SelectorCard({ index = 1, target, showContent } : Props) {
|
||||
return (
|
||||
<div className={cn(stl.wrapper, { [stl.active]: showContent })}>
|
||||
<div className={stl.top} onClick={() => activeTarget(index)}>
|
||||
<div className={stl.index}>{index + 1}</div>
|
||||
<div className="truncate">{target.selector}</div>
|
||||
</div>
|
||||
{ showContent && (
|
||||
<div className={stl.counts}>
|
||||
<div>{target.count} Clicks - {target.percent}%</div>
|
||||
<div className="color-gray-medium">TOTAL CLICKS</div>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SelectorCard'
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import React, { useState } from 'react'
|
||||
import { NoContent } from 'UI'
|
||||
import { connectPlayer } from 'Player/store';
|
||||
import SelectorCard from '../SelectorCard/SelectorCard';
|
||||
import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen';
|
||||
|
||||
interface Props {
|
||||
targets: Array<MarkedTarget>,
|
||||
activeTargetIndex: number
|
||||
}
|
||||
|
||||
function SelectorsList({ targets, activeTargetIndex }: Props) {
|
||||
return (
|
||||
<NoContent
|
||||
title="No data available."
|
||||
size="small"
|
||||
show={ targets && targets.length === 0 }
|
||||
>
|
||||
{ targets && targets.map((target, index) => (
|
||||
<SelectorCard target={target} index={index} showContent={activeTargetIndex === index} />
|
||||
))}
|
||||
</NoContent>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default connectPlayer(state => ({
|
||||
targets: state.markedTargets,
|
||||
activeTargetIndex: state.activeTargetIndex,
|
||||
}))(SelectorsList)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SelectorsList'
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './PageInsightsPanel'
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import { List } from 'immutable';
|
||||
import PageInsightsPanel from './';
|
||||
|
||||
const list = [
|
||||
{
|
||||
"alertId": 2,
|
||||
"projectId": 1,
|
||||
"name": "new alert",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 240,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 1.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1591893324078,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
},
|
||||
],
|
||||
"LastNotification": 1592929583000,
|
||||
"renotifyInterval": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"alertId": 14,
|
||||
"projectId": 1,
|
||||
"name": "alert 19.06",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 30,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 3000.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1592579750935,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
}
|
||||
],
|
||||
"renotifyInterval": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"alertId": 15,
|
||||
"projectId": 1,
|
||||
"name": "notify every 60min",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 30,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 1.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1592848779604,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
},
|
||||
],
|
||||
"LastNotification": 1599135058000,
|
||||
"renotifyInterval": 60
|
||||
}
|
||||
},
|
||||
{
|
||||
"alertId": 21,
|
||||
"projectId": 1,
|
||||
"name": "always notify",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 30,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 1.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1592849011350,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
}
|
||||
],
|
||||
"LastNotification": 1599135058000,
|
||||
"renotifyInterval": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const notifications = List([
|
||||
{ title: 'test', type: 'change', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
{ title: 'test', type: 'threshold', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
{ title: 'test', type: 'threshold', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
{ title: 'test', type: 'threshold', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
])
|
||||
storiesOf('PageInsights', module)
|
||||
.add('Panel', () => (
|
||||
<PageInsightsPanel />
|
||||
))
|
||||
|
|
@ -73,7 +73,7 @@ function getStorageName(type) {
|
|||
skip: state.skip,
|
||||
skipToIssue: state.skipToIssue,
|
||||
speed: state.speed,
|
||||
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode,
|
||||
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
|
||||
inspectorMode: state.inspectorMode,
|
||||
fullscreenDisabled: state.messagesLoading,
|
||||
logCount: state.logListNow.length,
|
||||
|
|
|
|||
79
frontend/app/components/Session_/Player/Overlay.tsx
Normal file
79
frontend/app/components/Session_/Player/Overlay.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import { connectPlayer, markTargets } from 'Player';
|
||||
import { getStatusText } from 'Player/MessageDistributor/managers/AssistManager';
|
||||
import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen';
|
||||
|
||||
import AutoplayTimer from './Overlay/AutoplayTimer';
|
||||
import PlayIconLayer from './Overlay/PlayIconLayer';
|
||||
import LiveStatusText from './Overlay/LiveStatusText';
|
||||
import Loader from './Overlay/Loader';
|
||||
import ElementsMarker from './Overlay/ElementsMarker';
|
||||
|
||||
interface Props {
|
||||
playing: boolean,
|
||||
completed: boolean,
|
||||
inspectorMode: boolean,
|
||||
messagesLoading: boolean,
|
||||
loading: boolean,
|
||||
live: boolean,
|
||||
liveStatusText: string,
|
||||
autoplay: boolean,
|
||||
markedTargets: MarkedTarget[] | null,
|
||||
activeTargetIndex: number,
|
||||
|
||||
nextId: string,
|
||||
togglePlay: () => void,
|
||||
}
|
||||
|
||||
function Overlay({
|
||||
playing,
|
||||
completed,
|
||||
inspectorMode,
|
||||
messagesLoading,
|
||||
loading,
|
||||
live,
|
||||
liveStatusText,
|
||||
autoplay,
|
||||
markedTargets,
|
||||
activeTargetIndex,
|
||||
nextId,
|
||||
togglePlay,
|
||||
}: Props) {
|
||||
|
||||
// useEffect(() =>{
|
||||
// setTimeout(() => markTargets([{ selector: 'div', count:6}]), 5000)
|
||||
// setTimeout(() => markTargets(null), 8000)
|
||||
// },[])
|
||||
|
||||
const showAutoplayTimer = !live && completed && autoplay && nextId
|
||||
const showPlayIconLayer = !live && !markedTargets && !inspectorMode && !loading && !showAutoplayTimer;
|
||||
const showLiveStatusText = live && liveStatusText && !loading;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ showAutoplayTimer && <AutoplayTimer /> }
|
||||
{ showLiveStatusText &&
|
||||
<LiveStatusText text={liveStatusText} />
|
||||
}
|
||||
{ messagesLoading && <Loader/> }
|
||||
{ showPlayIconLayer &&
|
||||
<PlayIconLayer playing={playing} togglePlay={togglePlay} />
|
||||
}
|
||||
{ markedTargets && <ElementsMarker targets={ markedTargets } activeIndex={activeTargetIndex}/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default connectPlayer(state => ({
|
||||
playing: state.playing,
|
||||
messagesLoading: state.messagesLoading,
|
||||
loading: state.messagesLoading || state.cssLoading,
|
||||
completed: state.completed,
|
||||
autoplay: state.autoplay,
|
||||
live: state.live,
|
||||
liveStatusText: getStatusText(state.peerConnectionStatus),
|
||||
markedTargets: state.markedTargets,
|
||||
activeTargetIndex: state.activeTargetIndex,
|
||||
}))(Overlay);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.overlayBg {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Button, Link } from 'UI'
|
||||
import { session as sessionRoute, withSiteId } from 'App/routes'
|
||||
import stl from './AutoplayTimer.css'
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import stl from './AutoplayTimer.css';
|
||||
import clsOv from './overlay.css';
|
||||
|
||||
function AutoplayTimer({ nextId, siteId, history }) {
|
||||
let timer
|
||||
|
|
@ -33,7 +35,7 @@ function AutoplayTimer({ nextId, siteId, history }) {
|
|||
return ''
|
||||
|
||||
return (
|
||||
<div className={stl.overlay}>
|
||||
<div className={ cn(clsOv.overlay, stl.overlayBg) } >
|
||||
<div className="border p-6 shadow-lg bg-white rounded">
|
||||
<div className="py-4">Next recording will be played in {counter}s</div>
|
||||
<div className="flex items-center">
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import Marker from './ElementsMarker/Marker';
|
||||
|
||||
export default function ElementsMarker({ targets, activeIndex }) {
|
||||
return targets && targets.map(t => <Marker target={t} active={activeIndex === t.index}/>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
.marker {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
border: 2px dotted transparent;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
& .index {
|
||||
opacity: 1;
|
||||
transition: 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
& .index {
|
||||
opacity: 0.3;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: $tealx;
|
||||
flex-shrink: 0;
|
||||
border: solid thin white;
|
||||
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
& .tooltip {
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
background-color: $tealx;
|
||||
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2);
|
||||
font-size: 12px;
|
||||
/* position: absolute; */
|
||||
/* bottom: 100%; */
|
||||
/* left: 0; */
|
||||
/* margin-bottom: 20px; */
|
||||
color: white;
|
||||
|
||||
&::before {
|
||||
top: 100%;
|
||||
left: 40%;
|
||||
border-color: $tealx transparent transparent transparent;
|
||||
content: "";
|
||||
display: block;
|
||||
border-style: solid;
|
||||
border-width: 10px 10px 10px 10px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
border: 2px dotted $tealx;
|
||||
|
||||
& .index {
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import type { MarkedTarget } from 'Player/MessageDistributor/StatedScreen/StatedScreen';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
import cn from 'classnames';
|
||||
import stl from './Marker.css';
|
||||
import { activeTarget } from 'Player';
|
||||
|
||||
interface Props {
|
||||
target: MarkedTarget;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export default function Marker({ target, active }: Props) {
|
||||
const style = {
|
||||
top: `${ target.boundingRect.top }px`,
|
||||
left: `${ target.boundingRect.left }px`,
|
||||
width: `${ target.boundingRect.width }px`,
|
||||
height: `${ target.boundingRect.height }px`,
|
||||
}
|
||||
return (
|
||||
<div className={ cn(stl.marker, { [stl.active] : active }) } style={ style } onClick={() => activeTarget(target.index)}>
|
||||
<div className={stl.index}>{target.index + 1}</div>
|
||||
<Tooltip
|
||||
open={active}
|
||||
arrow
|
||||
sticky
|
||||
distance={15}
|
||||
html={(
|
||||
<div>{target.count} Clicks</div>
|
||||
)}
|
||||
trigger="mouseenter"
|
||||
>
|
||||
<div className="absolute inset-0"></div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.text {
|
||||
color: $gray-light;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import stl from './LiveStatusText.css';
|
||||
import ovStl from './overlay.css';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export default function LiveStatusText({ text }: Props) {
|
||||
return <div className={ovStl.overlay}><span className={stl.text}>{text}</span></div>
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Loader } from 'UI';
|
||||
import ovStl from './overlay.css';
|
||||
|
||||
export default function OverlayLoader() {
|
||||
return <div className={ovStl.overlay}><Loader loading /></div>
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
.iconWrapper {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: all .2s; /* Animation */
|
||||
}
|
||||
|
||||
.zoomIcon {
|
||||
opacity: 1;
|
||||
transform: scale(1.8);
|
||||
transition: all .8s;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
import cls from './PlayIconLayer.css';
|
||||
import clsOv from './overlay.css';
|
||||
|
||||
interface Props {
|
||||
togglePlay: () => void,
|
||||
playing: boolean,
|
||||
}
|
||||
|
||||
export default function PlayIconLayer({ playing, togglePlay }: Props) {
|
||||
const [ showPlayOverlayIcon, setShowPlayOverlayIcon ] = useState(false);
|
||||
const togglePlayAnimated = useCallback(() => {
|
||||
setShowPlayOverlayIcon(true);
|
||||
togglePlay();
|
||||
setTimeout(
|
||||
() => setShowPlayOverlayIcon(false),
|
||||
800,
|
||||
);
|
||||
}, []);
|
||||
return (
|
||||
<div className={ clsOv.overlay } onClick={ togglePlayAnimated }>
|
||||
<div
|
||||
className={ cn(cls.iconWrapper, {
|
||||
[ cls.zoomIcon ]: showPlayOverlayIcon
|
||||
}) }
|
||||
>
|
||||
<Icon
|
||||
name={ playing ? "play" : "pause" }
|
||||
color="gray-medium"
|
||||
size={30}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,3 @@
|
|||
.wrapper {
|
||||
width: 30%;
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -13,5 +8,4 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,29 +2,17 @@ import { connect } from 'react-redux';
|
|||
import { findDOMNode } from 'react-dom';
|
||||
import cn from 'classnames';
|
||||
import { Loader, IconButton, EscapeButton } from 'UI';
|
||||
import { hide as hideTargetDefiner, toggleInspectorMode } from 'Duck/components/targetDefiner';
|
||||
import { hide as hideTargetDefiner } from 'Duck/components/targetDefiner';
|
||||
import { fullscreenOff } from 'Duck/components/player';
|
||||
import withOverlay from 'Components/hocs/withOverlay';
|
||||
import { attach as attachPlayer, Controls as PlayerControls, connectPlayer } from 'Player';
|
||||
import Controls from './Controls';
|
||||
import Overlay from './Overlay';
|
||||
import stl from './player.css';
|
||||
import AutoplayTimer from '../AutoplayTimer';
|
||||
import EventsToggleButton from '../../Session/EventsToggleButton';
|
||||
import { getStatusText } from 'Player/MessageDistributor/managers/AssistManager';
|
||||
|
||||
|
||||
const ScreenWrapper = withOverlay()(React.memo(() => <div className={ stl.screenWrapper } />));
|
||||
|
||||
@connectPlayer(state => ({
|
||||
playing: state.playing,
|
||||
loading: state.messagesLoading,
|
||||
disconnected: state.disconnected,
|
||||
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode,
|
||||
removeOverlay: !state.messagesLoading && state.inspectorMode || state.live,
|
||||
completed: state.completed,
|
||||
autoplay: state.autoplay,
|
||||
live: state.live,
|
||||
liveStatusText: getStatusText(state.peerConnectionStatus),
|
||||
}))
|
||||
@connect(state => ({
|
||||
//session: state.getIn([ 'sessions', 'current' ]),
|
||||
|
|
@ -32,15 +20,9 @@ const ScreenWrapper = withOverlay()(React.memo(() => <div className={ stl.screen
|
|||
nextId: state.getIn([ 'sessions', 'nextId' ]),
|
||||
}), {
|
||||
hideTargetDefiner,
|
||||
toggleInspectorMode: () => toggleInspectorMode(false),
|
||||
fullscreenOff,
|
||||
})
|
||||
export default class Player extends React.PureComponent {
|
||||
state = {
|
||||
showPlayOverlayIcon: false,
|
||||
|
||||
startedToPlayAt: Date.now(),
|
||||
};
|
||||
screenWrapper = React.createRef();
|
||||
|
||||
componentDidMount() {
|
||||
|
|
@ -48,69 +30,14 @@ export default class Player extends React.PureComponent {
|
|||
attachPlayer(parentElement);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.targetSelector !== this.props.targetSelector) {
|
||||
PlayerControls.mark(this.props.targetSelector);
|
||||
}
|
||||
if (prevProps.playing !== this.props.playing) {
|
||||
if (this.props.playing) {
|
||||
this.setState({ startedToPlayAt: Date.now() });
|
||||
} else {
|
||||
this.updateWatchingTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.playing) {
|
||||
this.updateWatchingTime();
|
||||
}
|
||||
}
|
||||
|
||||
updateWatchingTime() {
|
||||
const diff = Date.now() - this.state.startedToPlayAt;
|
||||
}
|
||||
|
||||
|
||||
// onTargetClick = (targetPath) => {
|
||||
// const { targetCustomList, location } = this.props;
|
||||
// const targetCustomFromList = targetCustomList !== this.props.targetSelector
|
||||
// .find(({ path }) => path === targetPath);
|
||||
// const target = targetCustomFromList
|
||||
// ? targetCustomFromList.set('location', location)
|
||||
// : { path: targetPath, isCustom: true, location };
|
||||
// this.props.showTargetDefiner(target);
|
||||
// }
|
||||
|
||||
togglePlay = () => {
|
||||
this.setState({ showPlayOverlayIcon: true });
|
||||
PlayerControls.togglePlay();
|
||||
|
||||
setTimeout(
|
||||
() => this.setState({ showPlayOverlayIcon: false }),
|
||||
800,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
showPlayOverlayIcon,
|
||||
} = this.state;
|
||||
const {
|
||||
className,
|
||||
playing,
|
||||
disabled,
|
||||
removeOverlay,
|
||||
bottomBlockIsActive,
|
||||
loading,
|
||||
disconnected,
|
||||
fullscreen,
|
||||
fullscreenOff,
|
||||
completed,
|
||||
autoplay,
|
||||
nextId,
|
||||
live,
|
||||
liveStatusText,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
|
@ -120,40 +47,12 @@ export default class Player extends React.PureComponent {
|
|||
>
|
||||
{ fullscreen &&
|
||||
<EscapeButton onClose={ fullscreenOff } />
|
||||
// <IconButton
|
||||
// size="18"
|
||||
// className="ml-auto mb-5"
|
||||
// style={{ marginTop: '-5px' }}
|
||||
// onClick={ fullscreenOff }
|
||||
// size="small"
|
||||
// icon="close"
|
||||
// label="Esc"
|
||||
// />
|
||||
}
|
||||
{!live && !fullscreen && <EventsToggleButton /> }
|
||||
<div className="relative flex-1">
|
||||
{ (!removeOverlay || live && liveStatusText) &&
|
||||
<div
|
||||
className={ stl.overlay }
|
||||
onClick={ disabled ? null : this.togglePlay }
|
||||
>
|
||||
{ live && liveStatusText
|
||||
? <span className={stl.liveStatusText}>{liveStatusText}</span>
|
||||
: <Loader loading={ loading } />
|
||||
}
|
||||
{ !live &&
|
||||
<div
|
||||
className={ cn(stl.iconWrapper, {
|
||||
[ stl.zoomIcon ]: showPlayOverlayIcon
|
||||
}) }
|
||||
>
|
||||
<div className={ playing ? stl.playIcon : stl.pauseIcon } />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{ completed && autoplay && nextId && <AutoplayTimer /> }
|
||||
<ScreenWrapper
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<Overlay nextId={nextId} togglePlay={PlayerControls.togglePlay} />
|
||||
<div
|
||||
className={ stl.screenWrapper }
|
||||
ref={ this.screenWrapper }
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
@import 'icons.css';
|
||||
|
||||
.playerBody {
|
||||
background: $white;
|
||||
/* border-radius: 3px; */
|
||||
|
|
@ -25,61 +23,8 @@
|
|||
font-weight: 200;
|
||||
color: $gray-medium;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
|
||||
/* &[data-protect] {
|
||||
pointer-events: none;
|
||||
background: $white;
|
||||
opacity: 0.3;
|
||||
}
|
||||
*/
|
||||
& .iconWrapper {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: all .2s; /* Animation */
|
||||
}
|
||||
|
||||
& .zoomIcon {
|
||||
opacity: 1;
|
||||
transform: scale(1.8);
|
||||
transition: all .8s;
|
||||
}
|
||||
|
||||
& .playIcon {
|
||||
@mixin icon play, $gray-medium, 30px;
|
||||
}
|
||||
|
||||
& .pauseIcon {
|
||||
@mixin icon pause, $gray-medium, 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.playerView {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.inspectorMode {
|
||||
z-index: 99991 !important;
|
||||
}
|
||||
|
||||
.liveStatusText {
|
||||
color: $gray-light;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
.wrapper {
|
||||
& .body {
|
||||
display: flex;
|
||||
border-bottom: solid thin $gray-light;
|
||||
padding: 5px;
|
||||
}
|
||||
background-color: white;
|
||||
outline: solid thin #CCC;
|
||||
& .body {
|
||||
display: flex;
|
||||
border-bottom: solid thin $gray-light;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.preSelections {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import SVG from 'UI/SVG';
|
||||
import styles from './icon.css';
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import Watchdog, { getSessionWatchdogTypes } from 'Types/watchdog';
|
|||
import { clean as cleanParams } from 'App/api_client';
|
||||
import withRequestState, { RequestTypes } from './requestStateCreator';
|
||||
import { getRE } from 'App/utils';
|
||||
import { LAST_7_DAYS } from 'Types/app/period';
|
||||
import { getDateRangeFromValue } from 'App/dateRange';
|
||||
|
||||
|
||||
const INIT = 'sessions/INIT';
|
||||
|
|
@ -15,6 +17,7 @@ const FETCH_FAVORITE_LIST = new RequestTypes('sessions/FETCH_FAVORITE_LIST');
|
|||
const FETCH_LIVE_LIST = new RequestTypes('sessions/FETCH_LIVE_LIST');
|
||||
const TOGGLE_FAVORITE = new RequestTypes('sessions/TOGGLE_FAVORITE');
|
||||
const FETCH_ERROR_STACK = new RequestTypes('sessions/FETCH_ERROR_STACK');
|
||||
const FETCH_INSIGHTS = new RequestTypes('sessions/FETCH_INSIGHTS');
|
||||
const SORT = 'sessions/SORT';
|
||||
const REDEFINE_TARGET = 'sessions/REDEFINE_TARGET';
|
||||
const SET_TIMEZONE = 'sessions/SET_TIMEZONE';
|
||||
|
|
@ -24,6 +27,14 @@ const TOGGLE_CHAT_WINDOW = 'sessions/TOGGLE_CHAT_WINDOW';
|
|||
|
||||
const SET_ACTIVE_TAB = 'sessions/SET_ACTIVE_TAB';
|
||||
|
||||
const range = getDateRangeFromValue(LAST_7_DAYS);
|
||||
const defaultDateFilters = {
|
||||
url: '',
|
||||
rangeValue: LAST_7_DAYS,
|
||||
startDate: range.start.unix() * 1000,
|
||||
endDate: range.end.unix() * 1000
|
||||
}
|
||||
|
||||
const initialState = Map({
|
||||
list: List(),
|
||||
sessionIds: [],
|
||||
|
|
@ -39,7 +50,10 @@ const initialState = Map({
|
|||
sourcemapUploaded: true,
|
||||
filteredEvents: null,
|
||||
showChatWindow: false,
|
||||
liveSessions: List()
|
||||
liveSessions: List(),
|
||||
visitedEvents: List(),
|
||||
insights: List(),
|
||||
insightFilters: defaultDateFilters
|
||||
});
|
||||
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
|
|
@ -136,21 +150,32 @@ const reducer = (state = initialState, action = {}) => {
|
|||
const session = Session(action.data);
|
||||
|
||||
const matching = [];
|
||||
|
||||
const visitedEvents = []
|
||||
const tmpMap = {}
|
||||
session.events.forEach(event => {
|
||||
if (event.type === 'LOCATION' && !tmpMap.hasOwnProperty(event.url)) {
|
||||
tmpMap[event.url] = event.url
|
||||
visitedEvents.push(event)
|
||||
}
|
||||
})
|
||||
|
||||
events.forEach(({ key, operator, value }) => {
|
||||
events.forEach(({ key, operator, value }) => {
|
||||
session.events.forEach((e, index) => {
|
||||
if (key === e.type) {
|
||||
const val = (e.type === 'LOCATION' ? e.url : e.value);
|
||||
if (key === e.type) {
|
||||
const val = (e.type === 'LOCATION' ? e.url : e.value);
|
||||
if (operator === 'is' && value === val) {
|
||||
matching.push(index);
|
||||
}
|
||||
if (operator === 'contains' && val.includes(value)) {
|
||||
matching.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return state.set('current', current.merge(session)).set('eventsIndex', matching);
|
||||
return state.set('current', current.merge(session))
|
||||
.set('eventsIndex', matching)
|
||||
.set('visitedEvents', visitedEvents);
|
||||
}
|
||||
case FETCH_FAVORITE_LIST.SUCCESS:
|
||||
return state
|
||||
|
|
@ -202,9 +227,11 @@ const reducer = (state = initialState, action = {}) => {
|
|||
.set('sessionIds', allList.map(({ sessionId }) => sessionId ).toJS())
|
||||
case SET_TIMEZONE:
|
||||
return state.set('timezone', action.timezone)
|
||||
case TOGGLE_CHAT_WINDOW:
|
||||
console.log(action)
|
||||
case TOGGLE_CHAT_WINDOW:
|
||||
return state.set('showChatWindow', action.state)
|
||||
case FETCH_INSIGHTS.SUCCESS:
|
||||
return state.set('insights', List(action.data).sort((a, b) => b.count - a.count));
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
@ -215,6 +242,7 @@ export default withRequestState({
|
|||
fetchFavoriteListRequest: FETCH_FAVORITE_LIST,
|
||||
toggleFavoriteRequest: TOGGLE_FAVORITE,
|
||||
fetchErrorStackList: FETCH_ERROR_STACK,
|
||||
fetchInsightsRequest: FETCH_INSIGHTS,
|
||||
}, reducer);
|
||||
|
||||
function init(session) {
|
||||
|
|
@ -263,6 +291,13 @@ export function fetchFavoriteList() {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchInsights(params) {
|
||||
return {
|
||||
types: FETCH_INSIGHTS.toArray(),
|
||||
call: client => client.post('/heatmaps/url', params),
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchLiveList() {
|
||||
return {
|
||||
types: FETCH_LIVE_LIST.toArray(),
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ const initialState = Map({
|
|||
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case FETCH_LIST.SUCCESS: {
|
||||
console.log(action);
|
||||
case FETCH_LIST.SUCCESS: {
|
||||
return state.set('list', List(action.data).map(i => {
|
||||
const type = i.type === 'navigate' ? i.type : 'location';
|
||||
return {...i, type: type.toUpperCase()}
|
||||
|
|
|
|||
|
|
@ -306,9 +306,7 @@ export default class MessageDistributor extends StatedScreen {
|
|||
this.pagesManager.moveReady(t).then(() => {
|
||||
|
||||
const lastScroll = this.scrollManager.moveToLast(t, index);
|
||||
// @ts-ignore ??can't see double inheritance
|
||||
if (!!lastScroll && this.window) {
|
||||
// @ts-ignore
|
||||
this.window.scrollTo(lastScroll.x, lastScroll.y);
|
||||
}
|
||||
// Moving mouse and setting :hover classes on ready view
|
||||
|
|
@ -479,7 +477,6 @@ export default class MessageDistributor extends StatedScreen {
|
|||
|
||||
// TODO: clean managers?
|
||||
clean() {
|
||||
// @ts-ignore
|
||||
super.clean();
|
||||
//if (this._socket) this._socket.close();
|
||||
update(INITIAL_STATE);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const INITIAL_STATE: State = {
|
|||
export default abstract class BaseScreen {
|
||||
public readonly overlay: HTMLDivElement;
|
||||
private readonly iframe: HTMLIFrameElement;
|
||||
private readonly _screen: HTMLDivElement;
|
||||
protected readonly screen: HTMLDivElement;
|
||||
protected parentElement: HTMLElement | null = null;
|
||||
constructor() {
|
||||
const iframe = document.createElement('iframe');
|
||||
|
|
@ -44,7 +44,7 @@ export default abstract class BaseScreen {
|
|||
screen.className = styles.screen;
|
||||
screen.appendChild(iframe);
|
||||
screen.appendChild(overlay);
|
||||
this._screen = screen;
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
attach(parentElement: HTMLElement) {
|
||||
|
|
@ -52,7 +52,7 @@ export default abstract class BaseScreen {
|
|||
throw new Error("BaseScreen: Trying to attach an attached screen.");
|
||||
}
|
||||
|
||||
parentElement.appendChild(this._screen);
|
||||
parentElement.appendChild(this.screen);
|
||||
|
||||
this.parentElement = parentElement;
|
||||
// parentElement.onresize = this.scale;
|
||||
|
|
@ -115,29 +115,38 @@ export default abstract class BaseScreen {
|
|||
return this.getElementsFromInternalPoint(this.getInternalCoordinates(point));
|
||||
}
|
||||
|
||||
getElementBySelector(selector: string): Element | null {
|
||||
if (!selector) return null;
|
||||
return this.document?.querySelector(selector) || null;
|
||||
}
|
||||
|
||||
display(flag: boolean = true) {
|
||||
this._screen.style.display = flag ? '' : 'none';
|
||||
this.screen.style.display = flag ? '' : 'none';
|
||||
}
|
||||
|
||||
displayFrame(flag: boolean = true) {
|
||||
this.iframe.style.display = flag ? '' : 'none';
|
||||
}
|
||||
|
||||
private s: number = 1;
|
||||
getScale() {
|
||||
return this.s;
|
||||
}
|
||||
|
||||
_scale() {
|
||||
if (!this.parentElement) return;
|
||||
let s = 1;
|
||||
const { height, width } = getState();
|
||||
const { offsetWidth, offsetHeight } = this.parentElement;
|
||||
|
||||
s = Math.min(offsetWidth / width, offsetHeight / height);
|
||||
if (s > 1) {
|
||||
s = 1;
|
||||
this.s = Math.min(offsetWidth / width, offsetHeight / height);
|
||||
if (this.s > 1) {
|
||||
this.s = 1;
|
||||
} else {
|
||||
s = Math.round(s * 1e3) / 1e3;
|
||||
this.s = Math.round(this.s * 1e3) / 1e3;
|
||||
}
|
||||
this._screen.style.transform = `scale(${ s }) translate(-50%, -50%)`;
|
||||
this._screen.style.width = width + 'px';
|
||||
this._screen.style.height = height + 'px';
|
||||
this.screen.style.transform = `scale(${ this.s }) translate(-50%, -50%)`;
|
||||
this.screen.style.width = width + 'px';
|
||||
this.screen.style.height = height + 'px';
|
||||
this.iframe.style.width = width + 'px';
|
||||
this.iframe.style.height = height + 'px';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,33 @@
|
|||
import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen';
|
||||
import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen';
|
||||
import { update, getState } from '../../store';
|
||||
|
||||
|
||||
//export interface targetPosition
|
||||
|
||||
interface BoundingRect {
|
||||
top: number,
|
||||
left: number,
|
||||
width: number,
|
||||
height: number,
|
||||
}
|
||||
|
||||
export interface MarkedTarget {
|
||||
boundingRect: BoundingRect,
|
||||
el: Element,
|
||||
selector: string,
|
||||
count: number,
|
||||
index: number,
|
||||
active?: boolean,
|
||||
percent: number
|
||||
}
|
||||
|
||||
export interface State extends SuperState {
|
||||
messagesLoading: boolean,
|
||||
cssLoading: boolean,
|
||||
disconnected: boolean,
|
||||
userPageLoading: boolean,
|
||||
markedTargets: MarkedTarget[] | null,
|
||||
activeTargetIndex: number
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
|
|
@ -15,40 +36,88 @@ export const INITIAL_STATE: State = {
|
|||
cssLoading: false,
|
||||
disconnected: false,
|
||||
userPageLoading: false,
|
||||
}
|
||||
markedTargets: null,
|
||||
activeTargetIndex: 0
|
||||
};
|
||||
|
||||
export default class StatedScreen extends Screen {
|
||||
constructor() { super(); }
|
||||
|
||||
setMessagesLoading(messagesLoading: boolean) {
|
||||
// @ts-ignore
|
||||
this.display(!messagesLoading);
|
||||
update({ messagesLoading });
|
||||
}
|
||||
|
||||
setCSSLoading(cssLoading: boolean) {
|
||||
// @ts-ignore
|
||||
|
||||
this.displayFrame(!cssLoading);
|
||||
update({ cssLoading });
|
||||
}
|
||||
|
||||
setDisconnected(disconnected: boolean) {
|
||||
if (!getState().live) return; //?
|
||||
// @ts-ignore
|
||||
this.display(!disconnected);
|
||||
update({ disconnected });
|
||||
}
|
||||
|
||||
setUserPageLoading(userPageLoading: boolean) {
|
||||
// @ts-ignore
|
||||
this.display(!userPageLoading);
|
||||
update({ userPageLoading });
|
||||
}
|
||||
|
||||
setSize({ height, width }: { height: number, width: number }) {
|
||||
update({ width, height });
|
||||
// @ts-ignore
|
||||
this.scale();
|
||||
|
||||
const { markedTargets } = getState();
|
||||
if (markedTargets) {
|
||||
update({
|
||||
markedTargets: markedTargets.map(mt => ({
|
||||
...mt,
|
||||
boundingRect: this.calculateRelativeBoundingRect(mt.el),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private calculateRelativeBoundingRect(el: Element): BoundingRect {
|
||||
if (!this.parentElement) return {top:0, left:0, width:0,height:0} //TODO
|
||||
const { top, left, width, height } = el.getBoundingClientRect();
|
||||
const s = this.getScale();
|
||||
const scrinRect = this.screen.getBoundingClientRect();
|
||||
const parentRect = this.parentElement.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
top: top*s + scrinRect.top - parentRect.top,
|
||||
left: left*s + scrinRect.left - parentRect.left,
|
||||
width: width*s,
|
||||
height: height*s,
|
||||
}
|
||||
}
|
||||
|
||||
setActiveTarget(index) {
|
||||
update({ activeTargetIndex: index });
|
||||
}
|
||||
|
||||
setMarkedTargets(selections: { selector: string, count: number }[] | null) {
|
||||
if (selections) {
|
||||
const targets: MarkedTarget[] = [];
|
||||
const totalCount = selections.reduce((a, b) => {
|
||||
return a + b.count
|
||||
}, 0);
|
||||
selections.forEach((s, index) => {
|
||||
const el = this.getElementBySelector(s.selector);
|
||||
if (!el) return;
|
||||
targets.push({
|
||||
...s,
|
||||
el,
|
||||
index,
|
||||
percent: Math.round((s.count * totalCount) / 100),
|
||||
boundingRect: this.calculateRelativeBoundingRect(el),
|
||||
})
|
||||
});
|
||||
update({ markedTargets: targets });
|
||||
} else {
|
||||
update({ markedTargets: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,9 +125,9 @@ export default class AssistManager {
|
|||
this.md.setMessagesLoading(false);
|
||||
}
|
||||
if (status === ConnectionStatus.Connected) {
|
||||
this.md.display(true);
|
||||
// this.md.display(true);
|
||||
} else {
|
||||
this.md.display(false);
|
||||
// this.md.display(false);
|
||||
}
|
||||
update({ peerConnectionStatus: status });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { goTo as listsGoTo } from './lists';
|
||||
import { update, getState } from './store';
|
||||
import MessageDistributor, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './MessageDistributor';
|
||||
import MessageDistributor, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './MessageDistributor/MessageDistributor';
|
||||
|
||||
const fps = 60;
|
||||
const performance = window.performance || { now: Date.now.bind(Date) };
|
||||
|
|
@ -35,7 +35,7 @@ const initialSkipToIssue = !!localStorage.getItem(SKIP_TO_ISSUE_STORAGE_KEY);
|
|||
const initialAutoplay = !!localStorage.getItem(AUTOPLAY_STORAGE_KEY);
|
||||
const initialShowEvents = !!localStorage.getItem(SHOW_EVENTS_STORAGE_KEY);
|
||||
|
||||
export const INITIAL_STATE: SuperState = {
|
||||
export const INITIAL_STATE = {
|
||||
...SUPER_INITIAL_STATE,
|
||||
time: 0,
|
||||
playing: false,
|
||||
|
|
@ -191,6 +191,15 @@ export default class Player extends MessageDistributor {
|
|||
update({ inspectorMode: false });
|
||||
}
|
||||
}
|
||||
|
||||
markTargets(targets: { selector: string, count: number }[] | null) {
|
||||
this.pause();
|
||||
this.setMarkedTargets(targets);
|
||||
}
|
||||
|
||||
activeTarget(index) {
|
||||
this.setActiveTarget(index);
|
||||
}
|
||||
|
||||
toggleSkipToIssue() {
|
||||
const skipToIssue = !getState().skipToIssue;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ export const markElement = initCheck((...args) => instance.marker && instance.ma
|
|||
export const scale = initCheck(() => instance.scale());
|
||||
export const toggleInspectorMode = initCheck((...args) => instance.toggleInspectorMode(...args));
|
||||
export const callPeer = initCheck((...args) => instance.assistManager.call(...args))
|
||||
export const markTargets = initCheck((...args) => instance.markTargets(...args))
|
||||
export const activeTarget = initCheck((...args) => instance.activeTarget(...args))
|
||||
|
||||
export const Controls = {
|
||||
jump,
|
||||
|
|
|
|||
|
|
@ -258,4 +258,19 @@ p {
|
|||
&:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tippy-tooltip.openreplay-theme {
|
||||
background-color: $tealx;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tippy-tooltip.openreplay-theme[data-animatefill] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.tippy-tooltip.openreplay-theme .tippy-backdrop {
|
||||
background-color: $tealx;
|
||||
}
|
||||
3
frontend/app/styles/import.css
vendored
3
frontend/app/styles/import.css
vendored
|
|
@ -4,4 +4,5 @@
|
|||
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
@import "tailwindcss/utilities";
|
||||
@import 'react-tippy/dist/tippy.css';
|
||||
|
|
@ -23,16 +23,18 @@
|
|||
border-color: $teal !important;
|
||||
}
|
||||
.ui.search.customDropdown>input.search,
|
||||
.ui.search.customDropdown>input.search,
|
||||
.ui.search.customDropdown.active>input.search,
|
||||
.ui.search.customDropdown.visible>input.search {
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
.ui.customDropdown>.text,
|
||||
.ui.search.customDropdown>.text {
|
||||
max-width: 90%;
|
||||
overflow: hidden;
|
||||
max-width: 90% !important;
|
||||
overflow: hidden !important;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
.ui.selection.customDropdown>.dropdown.icon {
|
||||
top: 5px !important;
|
||||
|
|
|
|||
32817
frontend/package-lock.json
generated
32817
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -48,6 +48,7 @@
|
|||
"react-redux": "^5.1.2",
|
||||
"react-router": "^4.3.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-tippy": "^1.4.0",
|
||||
"react-toastify": "^5.5.0",
|
||||
"react-virtualized": "^9.22.2",
|
||||
"recharts": "^1.8.5",
|
||||
|
|
|
|||
|
|
@ -84,3 +84,5 @@ Below is the list of dependencies used in OpenReplay software. Licenses may chan
|
|||
| schedule | MIT | Python |
|
||||
| croniter | MIT | Python |
|
||||
| lib/pq | MIT | Go |
|
||||
| peerjs | MIT | JavaScript |
|
||||
| antonmedv/finder | MIT | JavaScript |
|
||||
Loading…
Add table
Reference in a new issue