change(ui): url filter for clickmap
This commit is contained in:
parent
c8cba2aeaa
commit
62f53275be
18 changed files with 275 additions and 73 deletions
|
|
@ -8,7 +8,7 @@ import { toJS } from 'mobx'
|
|||
function ClickMapCard() {
|
||||
const { metricStore } = useStore()
|
||||
|
||||
console.log(toJS(metricStore.instance))
|
||||
// console.log(toJS(metricStore.instance))
|
||||
return (
|
||||
<div>this is a card</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import FilterList from 'Shared/Filters/FilterList';
|
||||
import {
|
||||
import {
|
||||
edit,
|
||||
updateSeries,
|
||||
addSeriesFilterFilter,
|
||||
|
|
@ -21,7 +21,7 @@ interface Props {
|
|||
edit: typeof edit;
|
||||
updateSeries: typeof updateSeries;
|
||||
onRemoveSeries: (seriesIndex: any) => void;
|
||||
canDelete?: boolean;
|
||||
canDelete?: boolean;
|
||||
addSeriesFilterFilter: typeof addSeriesFilterFilter;
|
||||
editSeriesFilterFilter: typeof editSeriesFilterFilter;
|
||||
editSeriesFilter: typeof editSeriesFilter;
|
||||
|
|
@ -43,6 +43,7 @@ function FilterSeries(props: Props) {
|
|||
|
||||
const onUpdateFilter = (filterIndex: any, filter: any) => {
|
||||
series.filter.updateFilter(filterIndex, filter)
|
||||
console.log('hi', filterIndex, filter)
|
||||
observeChanges()
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ function FilterSeries(props: Props) {
|
|||
<div className="mr-auto">
|
||||
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => series.update('name', name) } />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<div onClick={props.onRemoveSeries} className={cn("ml-3", {'disabled': !canDelete})}>
|
||||
<Icon name="trash" size="16" />
|
||||
|
|
@ -103,11 +104,11 @@ function FilterSeries(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default connect(null, {
|
||||
export default connect(null, {
|
||||
edit,
|
||||
updateSeries,
|
||||
addSeriesFilterFilter,
|
||||
editSeriesFilterFilter,
|
||||
editSeriesFilter,
|
||||
removeSeriesFilterFilter,
|
||||
})(observer(FilterSeries));
|
||||
})(observer(FilterSeries));
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import React from 'react';
|
||||
import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions';
|
||||
import { metricOf, issueOptions } from 'App/constants/filterOptions';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Button, Icon, SegmentSelection } from 'UI'
|
||||
import { Button, Icon } from 'UI'
|
||||
import FilterSeries from '../FilterSeries';
|
||||
import { confirm, Tooltip } from 'UI';
|
||||
import { confirm, Tooltip, Input } from 'UI';
|
||||
import Select from 'Shared/Select'
|
||||
import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'
|
||||
import MetricTypeDropdown from './components/MetricTypeDropdown';
|
||||
import MetricSubtypeDropdown from './components/MetricSubtypeDropdown';
|
||||
import { TIMESERIES, TABLE, CLICKMAP } from 'App/constants/card'
|
||||
import { pageUrlOperators } from 'App/constants/filterOptions'
|
||||
import FilterAutoComplete from 'Shared/Filters/FilterAutoComplete';
|
||||
import { clickmapFilter } from 'App/types/filter/newFilter';
|
||||
import { toJS } from 'mobx'
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
|
|
@ -27,7 +32,6 @@ function WidgetForm(props: Props) {
|
|||
|
||||
const { history, match: { params: { siteId, dashboardId } } } = props;
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const dashboards = dashboardStore.dashboards;
|
||||
const isSaving = metricStore.isSaving;
|
||||
const metric: any = metricStore.instance
|
||||
|
||||
|
|
@ -35,7 +39,6 @@ function WidgetForm(props: Props) {
|
|||
const tableOptions = metricOf.filter(i => i.type === 'table');
|
||||
const isTable = metric.metricType === 'table';
|
||||
const isFunnel = metric.metricType === 'funnel';
|
||||
const canAddToDashboard = metric.exists() && dashboards.length > 0;
|
||||
const canAddSeries = metric.series.length < 3;
|
||||
const eventsLength = metric.series[0].filter.filters.filter((i: any) => i.isEvent).length
|
||||
const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1);
|
||||
|
|
@ -60,13 +63,16 @@ function WidgetForm(props: Props) {
|
|||
}
|
||||
|
||||
if (name === 'metricType') {
|
||||
if (value === 'timeseries') {
|
||||
if (value === TIMESERIES) {
|
||||
obj['metricOf'] = timeseriesOptions[0].value;
|
||||
obj['viewType'] = 'lineChart';
|
||||
} else if (value === 'table') {
|
||||
} else if (value === TABLE) {
|
||||
obj['metricOf'] = tableOptions[0].value;
|
||||
obj['viewType'] = 'table';
|
||||
}
|
||||
if (value === CLICKMAP) {
|
||||
obj['viewType'] = 'chart'
|
||||
}
|
||||
}
|
||||
|
||||
metricStore.merge(obj);
|
||||
|
|
@ -99,13 +105,21 @@ function WidgetForm(props: Props) {
|
|||
metricStore.delete(metric).then(props.onDelete);
|
||||
}
|
||||
}
|
||||
|
||||
const updateClickMapURL = (_, item) => {
|
||||
console.log('updating filter', item)
|
||||
const newValues = {
|
||||
value: item
|
||||
}
|
||||
metric.series[0].filter.updateFilter(0, newValues)
|
||||
console.log(toJS(metric.series))
|
||||
}
|
||||
console.log(metric.series, metric.series[0].filter)
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="form-group">
|
||||
<label className="font-medium">Metric Type</label>
|
||||
<div className="flex items-center">
|
||||
<MetricTypeDropdown />
|
||||
<MetricTypeDropdown onSelect={writeOption} />
|
||||
<MetricSubtypeDropdown onSelect={writeOption} />
|
||||
|
||||
{/* {metric.metricType === 'timeseries' && (
|
||||
|
|
@ -160,6 +174,28 @@ function WidgetForm(props: Props) {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
{metric.metricType === CLICKMAP && (
|
||||
<div className='flex items-center'>
|
||||
<div className="mx-3">Where Visited URL</div>
|
||||
<Select name="clickMapUrl"
|
||||
options={pageUrlOperators.reverse()}
|
||||
defaultValue={pageUrlOperators.reverse()[0].value}
|
||||
onChange={ () => null }
|
||||
/>
|
||||
{/* <Input placeholder="Enter URL or path to select"
|
||||
/> */}
|
||||
<FilterAutoComplete
|
||||
value={metric.series[0].filter.filters[0]?.value || ''} // ?
|
||||
endpoint="/events/search"
|
||||
params={{ type: clickmapFilter.key }}
|
||||
headerText={''}
|
||||
placeholder={clickmapFilter.placeholder}
|
||||
onSelect={updateClickMapURL}
|
||||
icon={clickmapFilter.icon}
|
||||
hideOrText
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ interface Options {
|
|||
|
||||
interface Props {
|
||||
query: Record<string, (key: string) => any>;
|
||||
onSelect: (arg: any) => void;
|
||||
}
|
||||
function MetricTypeDropdown(props: Props) {
|
||||
const { metricStore } = useStore();
|
||||
|
|
@ -48,7 +49,7 @@ function MetricTypeDropdown(props: Props) {
|
|||
placeholder="Select Card Type"
|
||||
options={options}
|
||||
value={options.find((i: any) => i.value === metric.metricType) || options[0]}
|
||||
onChange={(selected) => onChange(selected.value.value as string)}
|
||||
onChange={props.onSelect}
|
||||
// onSelect={onSelect}
|
||||
components={{
|
||||
MenuList: ({ children, ...props }: any) => {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import ReadNote from '../Session_/Player/Controls/components/ReadNote';
|
|||
import { fetchList as fetchMembers } from 'Duck/member';
|
||||
import PlayerContent from './PlayerContent';
|
||||
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
|
||||
import { fetchInsights } from 'Duck/sessions';
|
||||
import Period, { LAST_30_DAYS } from 'Types/app/period';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
const TABS = {
|
||||
EVENTS: 'User Steps',
|
||||
|
|
@ -26,8 +29,13 @@ function WebPlayer(props: any) {
|
|||
live,
|
||||
fullscreen,
|
||||
fetchList,
|
||||
isClickmap,
|
||||
customSession,
|
||||
isClickmap = true,
|
||||
fetchInsights,
|
||||
host,
|
||||
visitedEvents,
|
||||
insightsFilters,
|
||||
insights,
|
||||
} = props;
|
||||
const { notesStore } = useStore();
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
|
|
@ -36,10 +44,16 @@ function WebPlayer(props: any) {
|
|||
const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClickmap) {
|
||||
if (isClickmap) {
|
||||
const urlOptions = visitedEvents.map(({ url, host }: any) => ({ label: url, value: url, host }))
|
||||
const url = insightsFilters.url ? insightsFilters.url : host + urlOptions[0].value;
|
||||
// @ts-ignore
|
||||
const { startDate, endDate, rangeValue } = new Period({ rangeName: LAST_30_DAYS })
|
||||
fetchInsights({ ...insightsFilters, url, startDate, endDate, rangeValue })
|
||||
} else {
|
||||
fetchList('issues');
|
||||
}
|
||||
const usedSession = isClickmap ? customSession : session;
|
||||
const usedSession = isClickmap && customSession ? customSession : session;
|
||||
|
||||
const [WebPlayerInst, PlayerStore] = createWebPlayer(usedSession, (state) =>
|
||||
makeAutoObservable(state)
|
||||
|
|
@ -67,6 +81,23 @@ function WebPlayer(props: any) {
|
|||
return () => WebPlayerInst.clean();
|
||||
}, [session.sessionId]);
|
||||
|
||||
const isPlayerReady = contextValue.store?.get().ready
|
||||
|
||||
React.useEffect(() => {
|
||||
contextValue.player && contextValue.player.play()
|
||||
if (isClickmap && isPlayerReady && insights.size > 0) {
|
||||
setTimeout(() => {
|
||||
contextValue.player.jump(500)
|
||||
contextValue.player.pause()
|
||||
contextValue.player.scaleFullPage()
|
||||
setTimeout(() => { contextValue.player.showClickmap(insights) }, 250)
|
||||
}, 500)
|
||||
}
|
||||
return () => {
|
||||
isPlayerReady && contextValue.player.showClickmap(null)
|
||||
}
|
||||
}, [insights, isPlayerReady])
|
||||
|
||||
// LAYOUT (TODO: local layout state - useContext or something..)
|
||||
useEffect(
|
||||
() => () => {
|
||||
|
|
@ -125,6 +156,10 @@ function WebPlayer(props: any) {
|
|||
export default connect(
|
||||
(state: any) => ({
|
||||
session: state.getIn(['sessions', 'current']),
|
||||
insightsFilters: state.getIn(['sessions', 'insightFilters']),
|
||||
host: state.getIn(['sessions', 'host']),
|
||||
insights: state.getIn(['sessions', 'insights']),
|
||||
visitedEvents: state.getIn(['sessions', 'visitedEvents']),
|
||||
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
|
||||
showEvents: state.get('showEvents'),
|
||||
members: state.getIn(['members', 'list']),
|
||||
|
|
@ -134,5 +169,6 @@ export default connect(
|
|||
closeBottomBlock,
|
||||
fetchList,
|
||||
fetchMembers,
|
||||
fetchInsights,
|
||||
}
|
||||
)(withLocationHandlers()(WebPlayer));
|
||||
)(withLocationHandlers()(observer(WebPlayer)));
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ function PageInsightsPanel({ filters, fetchInsights, events = [], insights, urlO
|
|||
}
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
(state: any) => {
|
||||
const events = state.getIn(['sessions', 'visitedEvents']);
|
||||
return {
|
||||
filters: state.getIn(['sessions', 'insightFilters']),
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function Player(props) {
|
|||
activeTab,
|
||||
fullView,
|
||||
isMultiview,
|
||||
isClickmap = true,
|
||||
isClickmap,
|
||||
} = props;
|
||||
const playerContext = React.useContext(PlayerContext);
|
||||
const screenWrapper = React.useRef();
|
||||
|
|
@ -57,8 +57,6 @@ function Player(props) {
|
|||
const parentElement = findDOMNode(screenWrapper.current); //TODO: good architecture
|
||||
playerContext.player.attach(parentElement);
|
||||
playerContext.player.play();
|
||||
|
||||
setInterval(() => playerContext.player.scaleFullPage(), 4000)
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ interface Props {
|
|||
onSelect: (e: any, item: any) => void;
|
||||
value: any;
|
||||
icon?: string;
|
||||
hideOrText?: boolean
|
||||
}
|
||||
|
||||
function FilterAutoComplete(props: Props) {
|
||||
|
|
@ -128,6 +129,7 @@ function FilterAutoComplete(props: Props) {
|
|||
endpoint = '',
|
||||
params = {},
|
||||
value = '',
|
||||
hideOrText = false,
|
||||
} = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [options, setOptions] = useState<any>([]);
|
||||
|
|
@ -240,7 +242,7 @@ function FilterAutoComplete(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{!showOrButton && <div className="ml-3">or</div>}
|
||||
{!showOrButton && !hideOrText && <div className="ml-3">or</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ function FilterItem(props: Props) {
|
|||
};
|
||||
|
||||
const onUpdateSubFilter = (subFilter: any, subFilterIndex: any) => {
|
||||
console.log(subFilter, subFilterIndex)
|
||||
props.onUpdate({
|
||||
...filter,
|
||||
filters: filter.filters.map((i: any, index: any) => {
|
||||
|
|
@ -48,6 +49,7 @@ function FilterItem(props: Props) {
|
|||
});
|
||||
};
|
||||
|
||||
console.log('filterItem', filter)
|
||||
return (
|
||||
<div className="flex items-center hover:bg-active-blue -mx-5 px-5 py-2">
|
||||
<div className="flex items-start w-full">
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ function FilterValue(props: Props) {
|
|||
}
|
||||
return _;
|
||||
});
|
||||
console.log(item ,{ ...filter, value: newValues });
|
||||
props.onUpdate({ ...filter, value: newValues });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export interface CardType {
|
|||
export const LIBRARY = 'library';
|
||||
export const TIMESERIES = 'timeseries';
|
||||
export const TABLE = 'table';
|
||||
export const CLICKMAP = 'clickmap'
|
||||
export const CLICKMAP = 'clickMap'
|
||||
|
||||
export const TYPES: CardType[] = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { FilterKey, IssueType } from 'Types/filter/filterType';
|
||||
// TODO remove text property from options
|
||||
export const options = [
|
||||
{ key: 'on', label: 'on', value: 'on' },
|
||||
{ key: 'on', label: 'on', value: 'on' },
|
||||
{ key: 'notOn', label: 'not on', value: 'notOn' },
|
||||
{ key: 'onAny', label: 'on any', value: 'onAny' },
|
||||
{ key: 'is', label: 'is', value: 'is' },
|
||||
|
|
@ -13,9 +13,9 @@ export const options = [
|
|||
{ key: 'contains', label: 'contains', value: 'contains' },
|
||||
{ key: 'notContains', label: 'not contains', value: 'notContains' },
|
||||
{ key: 'hasAnyValue', label: 'has any value', value: 'hasAnyValue' },
|
||||
{ key: 'hasNoValue', label: 'has no value', value: 'hasNoValue' },
|
||||
{ key: 'hasNoValue', label: 'has no value', value: 'hasNoValue' },
|
||||
{ key: 'isSignedUp', label: 'is signed up', value: 'isSignedUp' },
|
||||
{ key: 'notSignedUp', label: 'not signed up', value: 'notSignedUp' },
|
||||
{ key: 'notSignedUp', label: 'not signed up', value: 'notSignedUp' },
|
||||
{ key: 'before', label: 'before', value: 'before' },
|
||||
{ key: 'after', label: 'after', value: 'after' },
|
||||
{ key: 'inRage', label: 'in rage', value: 'inRage' },
|
||||
|
|
@ -37,6 +37,7 @@ const stringFilterKeysPerformance = ['is', 'inAnyPage', 'isNot', 'contains', 'st
|
|||
const targetFilterKeys = ['on', 'notOn', 'onAny', 'contains', 'startsWith', 'endsWith', 'notContains'];
|
||||
const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp'];
|
||||
const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast'];
|
||||
const pageUrlFilter = ['contains', 'startsWith', 'endsWith']
|
||||
|
||||
const getOperatorsByKeys = (keys) => {
|
||||
return options.filter(option => keys.includes(option.key));
|
||||
|
|
@ -50,6 +51,7 @@ export const booleanOperators = [
|
|||
{ key: 'true', label: 'true', value: 'true' },
|
||||
{ key: 'false', label: 'false', value: 'false' },
|
||||
]
|
||||
export const pageUrlOperators = options.filter(({key}) => pageUrlFilter.includes(key))
|
||||
|
||||
export const customOperators = [
|
||||
{ key: '=', label: '=', value: '=' },
|
||||
|
|
@ -86,6 +88,7 @@ export const metricOf = [
|
|||
{ label: 'Devices', value: FilterKey.USER_DEVICE, type: 'table' },
|
||||
{ label: 'Countries', value: FilterKey.USER_COUNTRY, type: 'table' },
|
||||
{ label: 'URLs', value: FilterKey.LOCATION, type: 'table' },
|
||||
|
||||
]
|
||||
|
||||
export const methodOptions = [
|
||||
|
|
@ -97,7 +100,7 @@ export const methodOptions = [
|
|||
{ label: 'HEAD', value: 'HEAD' },
|
||||
{ label: 'OPTIONS', value: 'OPTIONS' },
|
||||
{ label: 'TRACE', value: 'TRACE' },
|
||||
{ label: 'CONNECT', value: 'CONNECT' },
|
||||
{ label: 'CONNECT', value: 'CONNECT' },
|
||||
]
|
||||
|
||||
export const issueOptions = [
|
||||
|
|
@ -128,4 +131,5 @@ export default {
|
|||
metricOf,
|
||||
issueOptions,
|
||||
methodOptions,
|
||||
pageUrlOperators,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ export default class MessageManager {
|
|||
|
||||
this.waitingForFiles = false
|
||||
this.setMessagesLoading(false)
|
||||
// this.state.update({ filesLoaded: true })
|
||||
}
|
||||
|
||||
private async loadMessages() {
|
||||
|
|
|
|||
|
|
@ -217,10 +217,10 @@ export default class Screen {
|
|||
|
||||
scaleFullPage() {
|
||||
const { height, width } = this.document.body.getBoundingClientRect();
|
||||
this.cursor.toggle(false)
|
||||
const offsetHeight = this.parentElement.getBoundingClientRect().height
|
||||
if (!this.parentElement) return;
|
||||
|
||||
console.log(height, width)
|
||||
this.scaleRatio = 1
|
||||
this.screen.style.transform = `scale(1) translate(-50%, -50%)`;
|
||||
this.screen.style.overflow = 'scroll';
|
||||
|
|
|
|||
|
|
@ -122,6 +122,11 @@ export default class WebPlayer extends Player {
|
|||
this.targetMarker.markTargets(...args)
|
||||
}
|
||||
|
||||
showClickmap = (...args: Parameters<TargetMarker['injectTargets']>) => {
|
||||
this.pause()
|
||||
this.targetMarker.injectTargets(...args)
|
||||
}
|
||||
|
||||
|
||||
// TODO separate message receivers
|
||||
toggleTimetravel = async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type Screen from '../Screen/Screen'
|
||||
import type { Point } from '../Screen/types'
|
||||
import type { Store } from '../../common/types'
|
||||
|
||||
import { clickmapStyles } from './clickmapStyles'
|
||||
|
||||
function getOffset(el: Element, innerWindow: Window) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
|
@ -37,6 +37,8 @@ export interface State {
|
|||
|
||||
export default class TargetMarker {
|
||||
private clickMapOverlay: HTMLDivElement
|
||||
private clickContainers: HTMLDivElement[] = []
|
||||
private smallClicks: HTMLDivElement[] = []
|
||||
static INITIAL_STATE: State = {
|
||||
markedTargets: null,
|
||||
activeTargetIndex: 0
|
||||
|
|
@ -103,41 +105,16 @@ export default class TargetMarker {
|
|||
|
||||
private actualScroll: Point | null = null
|
||||
markTargets(selections: { selector: string, count: number }[] | null) {
|
||||
|
||||
if (selections) {
|
||||
const totalCount = selections.reduce((a, b) => {
|
||||
return a + b.count
|
||||
}, 0);
|
||||
const markedTargets: MarkedTarget[] = [];
|
||||
let index = 0;
|
||||
|
||||
const overlay = document.createElement("div")
|
||||
overlay.style.position = "absolute"
|
||||
overlay.style.top = "0px"
|
||||
overlay.style.left = "0px"
|
||||
overlay.style.width = '100%'
|
||||
overlay.style.height = "100%"
|
||||
overlay.style.background = 'rgba(0,0,0, 0.1)'
|
||||
this.screen.document.body.appendChild(overlay)
|
||||
this.clickMapOverlay = overlay
|
||||
selections.forEach((s) => {
|
||||
const el = this.screen.getElementBySelector(s.selector);
|
||||
if (!el) return;
|
||||
const test = document.createElement("div")
|
||||
const top = el.getBoundingClientRect().top
|
||||
const left = el.getBoundingClientRect().left
|
||||
test.innerHTML = '' + s.count + 'Clicks'
|
||||
Object.assign(test.style, {
|
||||
position: 'absolute',
|
||||
top: top + 'px',
|
||||
left: left + 'px',
|
||||
padding: '10px',
|
||||
borderRadius: '12px',
|
||||
background: 'white',
|
||||
boxShadow: '0px 2px 10px 2px rgba(0,0,0,0.5)',
|
||||
})
|
||||
|
||||
overlay.appendChild(test)
|
||||
markedTargets.push({
|
||||
...s,
|
||||
el,
|
||||
|
|
@ -155,10 +132,84 @@ export default class TargetMarker {
|
|||
this.actualScroll = null
|
||||
}
|
||||
this.store.update({ markedTargets: null });
|
||||
this.clickMapOverlay.remove()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
injectTargets(selections: { selector: string, count: number }[] | null) {
|
||||
if (selections) {
|
||||
const totalCount = selections.reduce((a, b) => {
|
||||
return a + b.count
|
||||
}, 0);
|
||||
|
||||
const overlay = document.createElement("div")
|
||||
Object.assign(overlay.style, clickmapStyles.overlayStyle)
|
||||
|
||||
this.clickMapOverlay = overlay
|
||||
selections.forEach((s, i) => {
|
||||
const el = this.screen.getElementBySelector(s.selector);
|
||||
if (!el) return;
|
||||
|
||||
const bubbleContainer = document.createElement("div")
|
||||
const {top, left, width, height} = el.getBoundingClientRect()
|
||||
|
||||
const totalClicks = document.createElement("div")
|
||||
totalClicks.innerHTML = `${s.count} ${s.count !== 1 ? 'Clicks' : 'Click'}`
|
||||
Object.assign(totalClicks.style, clickmapStyles.totalClicks)
|
||||
|
||||
const percent = document.createElement("div")
|
||||
percent.style.fontSize = "14px"
|
||||
percent.innerHTML = `${Math.round((s.count * 100) / totalCount)}% of the clicks recorded in this page`
|
||||
|
||||
bubbleContainer.appendChild(totalClicks)
|
||||
bubbleContainer.appendChild(percent)
|
||||
const containerId = `clickmap-bubble-${i}`
|
||||
bubbleContainer.id = containerId
|
||||
this.clickContainers.push(bubbleContainer)
|
||||
Object.assign(bubbleContainer.style, clickmapStyles.bubbleContainer({ top, left }))
|
||||
|
||||
const border = document.createElement("div")
|
||||
Object.assign(border.style, clickmapStyles.highlight({ width, height, top, left }))
|
||||
|
||||
const smallClicksBubble = document.createElement("div")
|
||||
smallClicksBubble.innerHTML = '' + s.count
|
||||
const smallClicksId = containerId + '-small'
|
||||
smallClicksBubble.id = smallClicksId
|
||||
this.smallClicks.push(smallClicksBubble)
|
||||
|
||||
border.onclick = () => {
|
||||
this.clickContainers.forEach(container => {
|
||||
if (container.id === containerId) {
|
||||
container.style.visibility = "visible"
|
||||
} else {
|
||||
container.style.visibility = "hidden"
|
||||
}
|
||||
})
|
||||
this.smallClicks.forEach(container => {
|
||||
if (container.id !== smallClicksId) {
|
||||
container.style.visibility = "visible"
|
||||
} else {
|
||||
container.style.visibility = "hidden"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Object.assign(smallClicksBubble.style, clickmapStyles.clicks)
|
||||
|
||||
border.appendChild(smallClicksBubble)
|
||||
overlay.appendChild(bubbleContainer)
|
||||
overlay.appendChild(border)
|
||||
});
|
||||
|
||||
this.screen.document.body.appendChild(overlay)
|
||||
// this.store.update({ markedTargets });
|
||||
} else {
|
||||
this.store.update({ markedTargets: null });
|
||||
this.clickMapOverlay?.remove()
|
||||
this.clickMapOverlay = null
|
||||
this.smallClicks = []
|
||||
this.clickContainers = []
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
58
frontend/app/player/web/addons/clickmapStyles.ts
Normal file
58
frontend/app/player/web/addons/clickmapStyles.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
export const clickmapStyles = {
|
||||
overlayStyle: {
|
||||
position: 'absolute',
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'rgba(0,0,0, 0.15)',
|
||||
zIndex: 9 * 10e3,
|
||||
// pointerEvents: 'none',
|
||||
},
|
||||
totalClicks: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
},
|
||||
bubbleContainer: ({ top, left }: { top: number; left: number }) => ({
|
||||
position: 'absolute',
|
||||
top: top + 'px',
|
||||
left: left + 'px',
|
||||
padding: '10px',
|
||||
borderRadius: '6px',
|
||||
background: 'white',
|
||||
border: '1px solid rgba(0, 0, 0, 0.12)',
|
||||
boxShadow: '0px 2px 10px 2px rgba(0,0,0,0.5)',
|
||||
transform: `translate(-25%, -110%)`,
|
||||
textAlign: 'center',
|
||||
visibility: 'hidden',
|
||||
}),
|
||||
highlight: ({
|
||||
width,
|
||||
height,
|
||||
top,
|
||||
left,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
top: number;
|
||||
left: number;
|
||||
}) => ({
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
border: '2px dotted red',
|
||||
cursor: 'pointer',
|
||||
top: top + 'px',
|
||||
left: left + 'px',
|
||||
position: 'absolute',
|
||||
}),
|
||||
clicks: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
borderRadius: '999px',
|
||||
padding: '6px',
|
||||
background: 'white',
|
||||
lineHeight: '0.5',
|
||||
transform: 'translate(-70%, -70%)',
|
||||
},
|
||||
};
|
||||
|
|
@ -29,7 +29,7 @@ export const filters = [
|
|||
{ key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'State Action', placeholder: 'E.g. 12', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true },
|
||||
{ key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error Message', placeholder: 'E.g. Uncaught SyntaxError', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true },
|
||||
// { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
|
||||
|
||||
|
||||
// FILTERS
|
||||
{ key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' },
|
||||
{ key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' },
|
||||
|
|
@ -42,7 +42,7 @@ export const filters = [
|
|||
// { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' },
|
||||
{ key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', placeholder: 'E.g. Alex, or alex@domain.com, or EMP123', operator: 'is', operatorOptions: filterOptions.stringOperators.concat([{ label: 'is undefined', value: 'isUndefined'}]), icon: 'filters/userid' },
|
||||
{ key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
|
||||
|
||||
|
||||
// PERFORMANCE
|
||||
{ key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
|
|
@ -53,6 +53,12 @@ export const filters = [
|
|||
{ key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', placeholder: 'Select an issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
|
||||
];
|
||||
|
||||
export const clickmapFilter = {
|
||||
key: FilterKey.LOCATION,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.INTERACTIONS,
|
||||
label: '', placeholder: 'Enter URL or path to select', operator: 'is', operatorOptions: filterOptions.pageUrlOperators, icon: 'filters/location', isEvent: true }
|
||||
|
||||
const mapFilters = (list) => {
|
||||
return list.reduce((acc, filter) => {
|
||||
acc[filter.key] = filter;
|
||||
|
|
@ -97,12 +103,12 @@ export const clearMetaFilters = () => {
|
|||
|
||||
/**
|
||||
* Add a new filter to the filter list
|
||||
* @param {*} category
|
||||
* @param {*} key
|
||||
* @param {*} type
|
||||
* @param {*} operator
|
||||
* @param {*} operatorOptions
|
||||
* @param {*} icon
|
||||
* @param {*} category
|
||||
* @param {*} key
|
||||
* @param {*} type
|
||||
* @param {*} operator
|
||||
* @param {*} operatorOptions
|
||||
* @param {*} icon
|
||||
*/
|
||||
export const addElementToFiltersMap = (
|
||||
category = FilterCategory.METADATA,
|
||||
|
|
@ -143,15 +149,15 @@ export default Record({
|
|||
value: [""],
|
||||
source: [""],
|
||||
category: '',
|
||||
|
||||
|
||||
custom: '',
|
||||
// target: Target(),
|
||||
level: '',
|
||||
|
||||
|
||||
hasNoValue: false,
|
||||
isFilter: false,
|
||||
actualValue: '',
|
||||
|
||||
|
||||
hasSource: false,
|
||||
source: [""],
|
||||
sourceType: '',
|
||||
|
|
@ -161,7 +167,7 @@ export default Record({
|
|||
sourceOperatorOptions: [],
|
||||
|
||||
operator: '',
|
||||
operatorOptions: [],
|
||||
operatorOptions: [],
|
||||
operatorDisabled: false,
|
||||
isEvent: false,
|
||||
index: 0,
|
||||
|
|
@ -199,8 +205,8 @@ export default Record({
|
|||
|
||||
/**
|
||||
* Group filters by category
|
||||
* @param {*} filtersMap
|
||||
* @returns
|
||||
* @param {*} filtersMap
|
||||
* @returns
|
||||
*/
|
||||
export const generateFilterOptions = (map) => {
|
||||
const filterSection = {};
|
||||
|
|
@ -229,4 +235,4 @@ export const generateLiveFilterOptions = (map) => {
|
|||
}
|
||||
});
|
||||
return filterSection;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue