feature(ui) - heatmaps

This commit is contained in:
Shekar Siri 2021-07-31 00:59:44 +05:30
parent 3f1dc20bf3
commit c27ec12e40
32 changed files with 33348 additions and 126 deletions

View file

@ -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>
)

View file

@ -21,7 +21,8 @@ const siteIdRequiredPaths = [
'/sourcemaps',
'/errors',
'/funnels',
'/assist'
'/assist',
'/heatmaps'
];
const noStoringFetchPathStarts = [

View file

@ -109,7 +109,7 @@ const FunnelHeader = (props) => {
endDate={funnelFilters.endDate}
onDateChange={onDateChange}
customRangeRight
/>
/>
</div>
</div>
</div>

View file

@ -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>
);

View 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>
)
}

View file

@ -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>
)
}

View file

@ -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

View file

@ -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>
</>
);
}
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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>
)
}

View file

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

View file

@ -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)

View file

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

View file

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

View file

@ -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 />
))

View file

@ -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);

View file

@ -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}/>)
}

View file

@ -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
}
}

View file

@ -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>
)
}

View file

@ -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(),

View file

@ -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()}

View file

@ -116,6 +116,7 @@ export default abstract class BaseScreen {
}
getElementBySelector(selector: string): Element | null {
if (!selector) return null;
return this.document?.querySelector(selector) || null;
}

View file

@ -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),
})
});

View file

@ -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 });
}

View file

@ -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;

View file

@ -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,

View file

@ -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;
}

View file

@ -4,4 +4,5 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "tailwindcss/utilities";
@import 'react-tippy/dist/tippy.css';

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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",