From 62f53275bed165e1028745bcf4f4bf520e100751 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 16 Dec 2022 11:33:55 +0100 Subject: [PATCH] change(ui): url filter for clickmap --- .../ClickMapCard/ClickMapCard.tsx | 2 +- .../components/FilterSeries/FilterSeries.tsx | 11 +- .../components/WidgetForm/WidgetForm.tsx | 54 +++++++-- .../MetricTypeDropdown/MetricTypeDropdown.tsx | 3 +- frontend/app/components/Session/WebPlayer.tsx | 44 +++++++- .../PageInsightsPanel/PageInsightsPanel.tsx | 2 +- .../app/components/Session_/Player/Player.js | 4 +- .../FilterAutoComplete/FilterAutoComplete.tsx | 4 +- .../shared/Filters/FilterItem/FilterItem.tsx | 2 + .../Filters/FilterValue/FilterValue.tsx | 1 + frontend/app/constants/card.ts | 2 +- frontend/app/constants/filterOptions.js | 12 +- frontend/app/player/web/MessageManager.ts | 1 + frontend/app/player/web/Screen/Screen.ts | 2 +- frontend/app/player/web/WebPlayer.ts | 5 + .../app/player/web/addons/TargetMarker.ts | 105 +++++++++++++----- .../app/player/web/addons/clickmapStyles.ts | 58 ++++++++++ frontend/app/types/filter/newFilter.js | 36 +++--- 18 files changed, 275 insertions(+), 73 deletions(-) create mode 100644 frontend/app/player/web/addons/clickmapStyles.ts diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx index f18afba81..c3e9f4ba4 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx @@ -8,7 +8,7 @@ import { toJS } from 'mobx' function ClickMapCard() { const { metricStore } = useStore() - console.log(toJS(metricStore.instance)) + // console.log(toJS(metricStore.instance)) return (
this is a card
) diff --git a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx index ccb269cbd..2496d0377 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx +++ b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx @@ -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) {
series.update('name', name) } />
- +
@@ -103,11 +104,11 @@ function FilterSeries(props: Props) { ); } -export default connect(null, { +export default connect(null, { edit, updateSeries, addSeriesFilterFilter, editSeriesFilterFilter, editSeriesFilter, removeSeriesFilterFilter, -})(observer(FilterSeries)); \ No newline at end of file +})(observer(FilterSeries)); diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 93df06c54..85676e8ed 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -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 (
- + {/* {metric.metricType === 'timeseries' && ( @@ -160,6 +174,28 @@ function WidgetForm(props: Props) { )}
+ {metric.metricType === CLICKMAP && ( +
+
Where Visited URL
+ */} + +
+ )}
diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx index 891a21d40..9623df971 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx @@ -17,6 +17,7 @@ interface Options { interface Props { query: Record 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) => { diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index cdbb54ff5..6be605c7f 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -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(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))); diff --git a/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx b/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx index b43f70ed9..ce017c601 100644 --- a/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx +++ b/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx @@ -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']), diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index a247e3e0b..e7e29ad3e 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -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) } }, []); diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index a616d8853..4ad93deb2 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -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([]); @@ -240,7 +242,7 @@ function FilterAutoComplete(props: Props) {
- {!showOrButton &&
or
} + {!showOrButton && !hideOrText &&
or
}
); } diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index 4b27e4a93..6cebb0d94 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -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 (
diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index 7daab8acb..4a13b2c4a 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -40,6 +40,7 @@ function FilterValue(props: Props) { } return _; }); + console.log(item ,{ ...filter, value: newValues }); props.onUpdate({ ...filter, value: newValues }); }; diff --git a/frontend/app/constants/card.ts b/frontend/app/constants/card.ts index 64378e557..78626f8c1 100644 --- a/frontend/app/constants/card.ts +++ b/frontend/app/constants/card.ts @@ -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[] = [ { diff --git a/frontend/app/constants/filterOptions.js b/frontend/app/constants/filterOptions.js index 224a32ae6..442717ff5 100644 --- a/frontend/app/constants/filterOptions.js +++ b/frontend/app/constants/filterOptions.js @@ -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, } diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts index e548f406e..bfb127247 100644 --- a/frontend/app/player/web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -207,6 +207,7 @@ export default class MessageManager { this.waitingForFiles = false this.setMessagesLoading(false) + // this.state.update({ filesLoaded: true }) } private async loadMessages() { diff --git a/frontend/app/player/web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts index 8925e387e..f10a3bd35 100644 --- a/frontend/app/player/web/Screen/Screen.ts +++ b/frontend/app/player/web/Screen/Screen.ts @@ -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'; diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index fc0534e2a..7a7c84cb8 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -122,6 +122,11 @@ export default class WebPlayer extends Player { this.targetMarker.markTargets(...args) } + showClickmap = (...args: Parameters) => { + this.pause() + this.targetMarker.injectTargets(...args) + } + // TODO separate message receivers toggleTimetravel = async () => { diff --git a/frontend/app/player/web/addons/TargetMarker.ts b/frontend/app/player/web/addons/TargetMarker.ts index a21e0b089..028858f6d 100644 --- a/frontend/app/player/web/addons/TargetMarker.ts +++ b/frontend/app/player/web/addons/TargetMarker.ts @@ -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 = [] + } + } } diff --git a/frontend/app/player/web/addons/clickmapStyles.ts b/frontend/app/player/web/addons/clickmapStyles.ts new file mode 100644 index 000000000..086260089 --- /dev/null +++ b/frontend/app/player/web/addons/clickmapStyles.ts @@ -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%)', + }, +}; diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 8abd3f5fc..2e5410f50 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -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; -} \ No newline at end of file +}