change(ui): url filter for clickmap

This commit is contained in:
sylenien 2022-12-16 11:33:55 +01:00
parent c8cba2aeaa
commit 62f53275be
18 changed files with 275 additions and 73 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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']),

View file

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

View file

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

View file

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

View file

@ -40,6 +40,7 @@ function FilterValue(props: Props) {
}
return _;
});
console.log(item ,{ ...filter, value: newValues });
props.onUpdate({ ...filter, value: newValues });
};

View file

@ -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[] = [
{

View file

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

View file

@ -207,6 +207,7 @@ export default class MessageManager {
this.waitingForFiles = false
this.setMessagesLoading(false)
// this.state.update({ filesLoaded: true })
}
private async loadMessages() {

View file

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

View file

@ -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 () => {

View file

@ -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 = []
}
}
}

View 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%)',
},
};

View file

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