feature(ui) - heatmaps
This commit is contained in:
parent
3f1dc20bf3
commit
c27ec12e40
32 changed files with 33348 additions and 126 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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,86 @@
|
|||
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';
|
||||
|
||||
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 + 500)
|
||||
setInsightsFilters({ ...insightsFilters, url: value })
|
||||
markTargets([])
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-3 bg-gray-lightest">
|
||||
<div className="mb-3 flex">
|
||||
<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"
|
||||
/>
|
||||
</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: 20px;
|
||||
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 12%</div>
|
||||
<div>TOTAL CLICKS</div>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SelectorCard'
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
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 (
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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 />
|
||||
))
|
||||
|
|
@ -19,6 +19,7 @@ interface Props {
|
|||
liveStatusText: string,
|
||||
autoplay: boolean,
|
||||
markedTargets: MarkedTarget[] | null,
|
||||
activeTargetIndex: number,
|
||||
|
||||
nextId: string,
|
||||
togglePlay: () => void,
|
||||
|
|
@ -34,15 +35,15 @@ function Overlay({
|
|||
liveStatusText,
|
||||
autoplay,
|
||||
markedTargets,
|
||||
|
||||
activeTargetIndex,
|
||||
nextId,
|
||||
togglePlay,
|
||||
}: Props) {
|
||||
|
||||
useEffect(() =>{
|
||||
setTimeout(() => markTargets([{ selector: 'div', count:6}]), 5000)
|
||||
setTimeout(() => markTargets(null), 8000)
|
||||
},[])
|
||||
// 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;
|
||||
|
|
@ -58,7 +59,7 @@ function Overlay({
|
|||
{ showPlayIconLayer &&
|
||||
<PlayIconLayer playing={playing} togglePlay={togglePlay} />
|
||||
}
|
||||
{ markedTargets && <ElementsMarker targets={ markedTargets } />
|
||||
{ markedTargets && <ElementsMarker targets={ markedTargets } activeIndex={activeTargetIndex}/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
|
@ -73,5 +74,6 @@ export default connectPlayer(state => ({
|
|||
autoplay: state.autoplay,
|
||||
live: state.live,
|
||||
liveStatusText: getStatusText(state.peerConnectionStatus),
|
||||
markedTargets: state.markedTargets
|
||||
markedTargets: state.markedTargets,
|
||||
activeTargetIndex: state.activeTargetIndex,
|
||||
}))(Overlay);
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import Marker from './ElementsMarker/Marker';
|
||||
|
||||
export default function ElementsMarker({ targets }) {
|
||||
return targets.map(t => <Marker target={t} />)
|
||||
export default function ElementsMarker({ targets, activeIndex }) {
|
||||
return targets && targets.map(t => <Marker target={t} active={activeIndex === t.index}/>)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,65 @@
|
|||
.marker {
|
||||
position: absolute;
|
||||
border: 2px dashed;
|
||||
z-index: 9999;
|
||||
z-index: 100;
|
||||
border: 2px dashed 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 dashed $tealx;
|
||||
|
||||
& .index {
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,37 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { Popup } from 'UI';
|
||||
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 }: Props) {
|
||||
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={ stl.marker } style={ style } />
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ export default abstract class BaseScreen {
|
|||
}
|
||||
|
||||
getElementBySelector(selector: string): Element | null {
|
||||
if (!selector) return null;
|
||||
return this.document?.querySelector(selector) || null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ export interface MarkedTarget {
|
|||
el: Element,
|
||||
selector: string,
|
||||
count: number,
|
||||
index: number,
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
export interface State extends SuperState {
|
||||
|
|
@ -23,7 +25,8 @@ export interface State extends SuperState {
|
|||
cssLoading: boolean,
|
||||
disconnected: boolean,
|
||||
userPageLoading: boolean,
|
||||
markedTargets: MarkedTarget[] | null
|
||||
markedTargets: MarkedTarget[] | null,
|
||||
activeTargetIndex: number
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
|
|
@ -33,6 +36,7 @@ export const INITIAL_STATE: State = {
|
|||
disconnected: false,
|
||||
userPageLoading: false,
|
||||
markedTargets: [],
|
||||
activeTargetIndex: 0
|
||||
};
|
||||
|
||||
export default class StatedScreen extends Screen {
|
||||
|
|
@ -89,15 +93,20 @@ export default class StatedScreen extends Screen {
|
|||
}
|
||||
}
|
||||
|
||||
setActiveTarget(index) {
|
||||
update({ activeTargetIndex: index });
|
||||
}
|
||||
|
||||
setMarkedTargets(selections: { selector: string, count: number }[] | null) {
|
||||
if (selections) {
|
||||
const targets: MarkedTarget[] = [];
|
||||
selections.forEach(s => {
|
||||
selections.forEach((s, index) => {
|
||||
const el = this.getElementBySelector(s.selector);
|
||||
if (!el) return;
|
||||
targets.push({
|
||||
...s,
|
||||
el,
|
||||
index,
|
||||
boundingRect: this.calculateRelativeBoundingRect(el),
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,6 +196,10 @@ export default class Player extends MessageDistributor {
|
|||
this.pause();
|
||||
this.setMarkedTargets(targets);
|
||||
}
|
||||
|
||||
activeTarget(index) {
|
||||
this.setActiveTarget(index);
|
||||
}
|
||||
|
||||
toggleSkipToIssue() {
|
||||
const skipToIssue = !getState().skipToIssue;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ 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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue