feat(ui) - custom metrics

This commit is contained in:
Shekar Siri 2022-01-19 14:49:21 +05:30
parent 155e7e4331
commit e80293efa4
26 changed files with 365 additions and 148 deletions

View file

@ -141,21 +141,21 @@ export default class EventFilter extends React.PureComponent {
{ hasFilters &&
<div className={cn("bg-white rounded border-gray-light mt-2 relative", { 'blink-border' : blink })}>
<div className="absolute right-0 top-0 m-3 z-10 flex items-center">
<div className="mr-2">Operator</div>
<SegmentSelection
primary
name="condition"
extraSmall={true}
// className="my-3"
onSelect={ this.changeConditionTab }
value={{ value: appliedFilter.condition }}
list={ [
{ name: 'AND', value: 'and' },
{ name: 'OR', value: 'or' },
{ name: 'THEN', value: 'then' },
]}
/>
</div>
<div className="mr-2">Operator</div>
<SegmentSelection
primary
name="condition"
extraSmall={true}
// className="my-3"
onSelect={ this.changeConditionTab }
value={{ value: appliedFilter.condition }}
list={ [
{ name: 'AND', value: 'and' },
{ name: 'OR', value: 'or' },
{ name: 'THEN', value: 'then' },
]}
/>
</div>
{ events.size > 0 &&
<>

View file

@ -39,6 +39,7 @@ import SideMenuSection from './SideMenu/SideMenuSection';
import styles from './dashboard.css';
import WidgetSection from 'Shared/WidgetSection/WidgetSection';
import OverviewWidgets from './Widgets/OverviewWidgets/OverviewWidgets';
import CustomMetricsWidgets from './Widgets/CustomMetricsWidgets/CustomMetricsWidgets';
import WidgetHolder from './WidgetHolder/WidgetHolder';
import MetricsFilters from 'Shared/MetricsFilters/MetricsFilters';
import { withRouter } from 'react-router';
@ -47,6 +48,7 @@ const OVERVIEW = 'overview';
const PERFORMANCE = 'performance';
const ERRORS_N_CRASHES = 'errors_n_crashes';
const RESOURCES = 'resources';
const CUSTOM_METRICS = 'custom_metrics';
const menuList = [
{
@ -201,6 +203,12 @@ export default class Dashboard extends React.PureComponent {
</div>
</WidgetSection>
<WidgetSection title="Custom Metrics" type="customMetrics" className="mb-4">
<div className={ cn("gap-4", { 'grid grid-cols-2' : !comparing })} ref={this.list[CUSTOM_METRICS]}>
<CustomMetricsWidgets />
</div>
</WidgetSection>
<WidgetSection title="Errors" className="mb-4" type="errors">
<div className={ cn("gap-4", { 'grid grid-cols-2' : !comparing })} ref={this.list[ERRORS_N_CRASHES]}>
{ dashboardAppearance.impactedSessionsByJsErrors && <WidgetHolder Component={SessionsAffectedByJSErrors} /> }

View file

@ -0,0 +1,75 @@
import React from 'react';
import { Loader, NoContent } from 'UI';
import { widgetHOC, Styles } from '../../common';
import { ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
import CustomMetricWidgetHoc from '../../common/CustomMetricWidgetHoc';
const customParams = rangeName => {
const params = { density: 70 }
if (rangeName === LAST_24_HOURS) params.density = 70
if (rangeName === LAST_30_MINUTES) params.density = 70
if (rangeName === YESTERDAY) params.density = 70
if (rangeName === LAST_7_DAYS) params.density = 70
return params
}
interface Period {
rangeName: string;
}
interface Props {
widget: any;
loading?: boolean;
data?: any;
showSync?: boolean;
compare?: boolean;
period?: Period;
}
function CustomMetricWidget(props: Props) {
const { widget, loading = false, data = { chart: []}, showSync, compare, period = { rangeName: ''} } = props;
const colors = compare ? Styles.compareColors : Styles.colors;
const params = customParams(period.rangeName)
const gradientDef = Styles.gradientDef();
return (
<Loader loading={ loading } size="small">
<NoContent
size="small"
show={ data.chart.length === 0 }
>
<ResponsiveContainer height={ 240 } width="100%">
<AreaChart
data={ data.chart }
margin={Styles.chartMargins}
syncId={ showSync ? "impactedSessionsBySlowPages" : undefined }
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density/7} />
<YAxis
{...Styles.yaxis}
label={{ ...Styles.axisLabelLeft, value: "Number of Requests" }}
allowDecimals={false}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Sessions"
type="monotone"
dataKey="count"
stroke={colors[0]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={compare ? 'url(#colorCountCompare)' : 'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</NoContent>
</Loader>
);
}
export default CustomMetricWidgetHoc(CustomMetricWidget);

View file

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

View file

@ -0,0 +1,29 @@
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { fetchList } from 'Duck/customMetrics';
import { list } from 'App/components/BugFinder/CustomFilters/filterModal.css';
import CustomMetricWidget from './CustomMetricWidget';
interface Props {
fetchList: Function;
list: any;
}
function CustomMetricsWidgets(props: Props) {
const { list } = props;
useEffect(() => {
props.fetchList()
}, [])
return (
<>
{list.map((item: any) => (
<CustomMetricWidget widget={item} />
))}
</>
);
}
export default connect(state => ({
list: state.getIn(['customMetrics', 'list']),
}), { fetchList })(CustomMetricsWidgets);

View file

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

View file

@ -0,0 +1,6 @@
.wrapper {
background-color: white;
/* border: solid thin $gray-medium; */
border-radius: 3px;
padding: 10px;
}

View file

@ -0,0 +1,25 @@
import React from 'react';
import stl from './CustomMetricWidgetHoc.css';
import { Icon } from 'UI';
interface Props {
}
const CustomMetricWidgetHoc = ({ ...rest }: Props) => BaseComponent => {
console.log('CustomMetricWidgetHoc', rest);
return (
<div className={stl.wrapper}>
<div className="flex items-center mb-10 p-2">
<div className="font-medium">Widget Name</div>
<div className="ml-auto">
<div className="cursor-pointer">
<Icon name="bell-plus" size="16" />
</div>
</div>
</div>
{/* <BaseComponent {...rest} /> */}
</div>
);
}
export default CustomMetricWidgetHoc;

View file

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

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Form, SegmentSelection, Button } from 'UI';
import { Form, SegmentSelection, Button, IconButton } from 'UI';
import FilterSeries from '../FilterSeries';
import { connect } from 'react-redux';
import { edit as editMetric, save } from 'Duck/customMetrics';
@ -68,13 +68,13 @@ function CustomMetricForm(props: Props) {
<div className="form-group">
<label className="font-medium">Metric Type</label>
<div className="flex items-center">
<span>Timeseries</span>
<span className="mx-2">of</span>
<div style={{ width: "250px"}}>
<span className="bg-white p-1 px-2 border rounded">Timeseries</span>
<span className="mx-2 color-gray-medium">of</span>
<div>
<SegmentSelection
primary
name="condition"
extraSmall={true}
small={true}
// className="my-3"
onSelect={ changeConditionTab }
value={{ value: metric.type }}
@ -88,7 +88,7 @@ function CustomMetricForm(props: Props) {
</div>
<div className="form-group">
<label className="font-medium">Sereis</label>
<label className="font-medium">Series</label>
{metric.series && metric.series.size > 0 && metric.series.map((series: any, index: number) => (
<div className="mb-2">
<FilterSeries
@ -100,7 +100,9 @@ function CustomMetricForm(props: Props) {
))}
</div>
<Button onClick={addSeries}>Add Series</Button>
<div className="flex justify-end">
<IconButton onClick={addSeries} primaryText label="SERIES" icon="plus" />
</div>
</div>
<div className="absolute w-full bottom-0 px-5 py-2 bg-white">

View file

@ -1,38 +1,29 @@
import React from 'react';
import React, { useState } from 'react';
import { IconButton, SlideModal } from 'UI'
import CustomMetricForm from './CustomMetricForm';
interface Props {}
function CustomMetrics(props: Props) {
const [showModal, setShowModal] = useState(true);
return (
<div>
<IconButton outline icon="plus" label="CREATE METRIC" />
<div className="self-start">
<IconButton outline icon="plus" label="CREATE METRIC" onClick={() => setShowModal(true)} />
<SlideModal
title={
<div className="flex items-center">
<span className="mr-3">{ 'Custom Metric' }</span>
{/* <IconButton
circle
size="small"
icon="plus"
outline
id="add-button"
// onClick={ () => toggleForm({}, true) }
/> */}
</div>
}
isDisplayed={ true }
// onClose={ () => {
// toggleForm({}, false);
// setShowAlerts(false);
// } }
isDisplayed={ showModal }
onClose={ () => setShowModal(false)}
// size="medium"
content={
<div className="bg-gray-light-shade">
content={ showModal && (
<div style={{ backgroundColor: '#f6f6f6'}}>
<CustomMetricForm />
</div>
}
)}
/>
</div>
);

View file

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import FilterList from 'Shared/Filters/FilterList';
import { edit, updateSeries } from 'Duck/customMetrics';
import { connect } from 'react-redux';
import { IconButton, Button, Icon } from 'UI';
import { IconButton, Button, Icon, SegmentSelection } from 'UI';
import FilterSelection from '../../Filters/FilterSelection';
interface Props {
@ -14,6 +14,7 @@ interface Props {
}
function FilterSeries(props: Props) {
const [expanded, setExpanded] = useState(false)
const { series, seriesIndex } = props;
const onAddFilter = (filter) => {
@ -46,6 +47,16 @@ function FilterSeries(props: Props) {
});
}
const onChangeEventsOrder = (e, { name, value }) => {
props.updateSeries(seriesIndex, {
...series.toData(),
filter: {
...series.filter,
eventsOrder: value,
}
});
}
const onRemoveFilter = (filterIndex) => {
const newFilters = series.filter.filters.filter((_filter, i) => {
return i !== filterIndex;
@ -62,35 +73,43 @@ function FilterSeries(props: Props) {
return (
<div className="border rounded bg-white">
<div className="border-b px-5 h-12 flex items-center">
<span className="mr-auto">{ series.name }</span>
<div className="flex items-center cursor-pointer" onClick={props.onRemoveSeries}>
<Icon name="trash" size="16" />
<div className="border-b px-5 h-12 flex items-center relative">
<div className="font-medium">{ series.name }</div>
<div className="flex items-center cursor-pointer ml-auto" >
<div onClick={props.onRemoveSeries} className="ml-3">
<Icon name="trash" size="16" />
</div>
<div onClick={() => setExpanded(!expanded)} className="ml-3">
<Icon name="chevron-down" size="16" />
</div>
</div>
</div>
<div className="p-5">
{ series.filter.filters.size > 0 ? (
<FilterList
filters={series.filter.filters.toJS()}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
/>
): (
<div>Add user event or filter to build the series.</div>
)}
</div>
<div className="px-5 border-t h-12 flex items-center">
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
>
{/* <Button className="flex items-center">
<Icon name="plus" size="16" />
<span>Add Step</span>
</Button> */}
<IconButton primaryText label="ADD STEP" icon="plus" />
</FilterSelection>
</div>
{ expanded && (
<>
<div className="p-5">
{ series.filter.filters.size > 0 ? (
<FilterList
filters={series.filter.filters.toJS()}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
/>
): (
<div className="color-gray-medium">Add user event or filter to build the series.</div>
)}
</div>
<div className="px-5 border-t h-12 flex items-center">
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
>
<IconButton primaryText label="ADD STEP" icon="plus" />
</FilterSelection>
</div>
</>
)}
</div>
);
}

View file

@ -16,20 +16,36 @@
& .right {
height: 28px;
display: flex;
align-items: center;
padding: 0 5px;
align-items: stretch;
padding: 0;
background-color: $gray-lightest;
border-left: solid thin $gray-light !important;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
cursor: pointer;
& div {
/* background-color: red; */
border-left: solid thin $gray-light !important;
width: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
&:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
&:hover {
background-color: $gray-light;
}
}
}
}
.menu {
border-radius: 0 0 3px 3px;
box-shadow: 0 2px 10px 0 $gray-light;
padding: 20px;
border: solid thin $gray-light !important;
/* box-shadow: 0 2px 10px 0 $gray-light; */
/* padding: 20px; */
background-color: white;
max-height: 350px;
overflow-y: auto;
@ -43,7 +59,7 @@
.filterItem {
display: flex;
align-items: center;
padding: 8px;
padding: 8px 10px;
cursor: pointer;
border-radius: 3px;
transition: all 0.4s;

View file

@ -12,6 +12,7 @@ const hiddenStyle = {
interface Props {
showOrButton?: boolean;
showCloseButton?: boolean;
onRemoveValue?: () => void;
onAddValue?: () => void;
endpoint?: string;
@ -25,6 +26,7 @@ interface Props {
function FilterAutoComplete(props: Props) {
const {
showCloseButton = false,
placeholder = 'Type to search',
method = 'GET',
showOrButton = false,
@ -94,7 +96,7 @@ function FilterAutoComplete(props: Props) {
}
return (
<div className="relative">
<div className="relative flex items-center">
<div className={stl.wrapper}>
<input
name="query"
@ -115,11 +117,13 @@ function FilterAutoComplete(props: Props) {
className={stl.right}
// onClick={showOrButton ? onRemoveValue : onAddValue}
>
{ !showOrButton && <Icon onClick={onRemoveValue} name="close" size="18" /> }
{ showOrButton && <span onClick={onAddValue} className="px-1">or</span>}
{ showCloseButton && <div onClick={onRemoveValue}><Icon name="close" size="18" /></div> }
{ showOrButton && <div onClick={onAddValue} className="color-teal"><span className="px-1">or</span></div> }
</div>
</div>
{ !showOrButton && <div className="ml-3">or</div> }
{/* <textarea style={hiddenStyle} ref={(ref) => this.hiddenInput = ref }></textarea> */}
{ showModal && (options.length > 0 || loading) &&

View file

@ -44,26 +44,29 @@ function FitlerItem(props: Props) {
}
return (
<div className="flex items-center mb-3">
<div className="flex items-center mb-4">
<div className="flex items-start mr-auto">
<div className="mt-1 w-6 h-6 text-xs flex justify-center rounded-full bg-gray-light-shade mr-2">{filterIndex+1}</div>
<FilterSelection filter={filter} onFilterClick={replaceFilter} />
<FilterOperator filter={filter} onChange={onOperatorChange} className="mx-2"/>
<FilterOperator filter={filter} onChange={onOperatorChange} className="mx-2 flex-shrink-0"/>
<div className="grid grid-cols-3 gap-3">
{filter.value && filter.value.map((value, valueIndex) => (
<FilterValue
showCloseButton={filter.value.length > 1}
showOrButton={valueIndex === filter.value.length - 1}
key={valueIndex}
// filter={filter}
// key={valueIndex}
value={value}
key={filter.key}
index={valueIndex}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
onSelect={(e, item) => onSelect(e, valueIndex, item)}
onSelect={(e, item) => onSelect(e, item, valueIndex)}
/>
))}
</div>
</div>
<div className="flex">
<div className="flex self-start mt-2">
<div
className="cursor-pointer"
onClick={props.onRemoveFilter}

View file

@ -1,5 +1,6 @@
import React, { useState} from 'react';
import FilterItem from '../FilterItem';
import { SegmentSelection } from 'UI';
interface Props {
filters: any[]; // event/filter
@ -19,6 +20,27 @@ function FilterList(props: Props) {
return (
<div className="flex flex-col">
<div className="flex items-center mb-2">
<div className="mb-2 text-sm color-gray-medium mr-auto">EVENTS</div>
<div className="flex items-center">
<div className="mr-2 color-gray-medium text-sm">Events Order</div>
<SegmentSelection
primary
name="eventsOrder"
extraSmall={true}
// className="my-3"
// onSelect={onChangeEventsOrder }
onSelect={() => null }
// value={{ value: series.filter.eventsOrder }}
value={{ value: 'and' }}
list={ [
{ name: 'AND', value: 'and' },
{ name: 'OR', value: 'or' },
{ name: 'THEN', value: 'then' },
]}
/>
</div>
</div>
{filters.map((filter, filterIndex) => (
<FilterItem
filterIndex={filterIndex}
@ -27,6 +49,16 @@ function FilterList(props: Props) {
onRemoveFilter={() => onRemoveFilter(filterIndex) }
/>
))}
{/* <div>Filters</div>
{filters.filter(f => !f.isEvent).map((filter, filterIndex) => (
<FilterItem
filterIndex={filter.index}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex) }
/>
))} */}
</div>
);
}

View file

@ -9,14 +9,14 @@ interface Props {
function FilterModal(props: Props) {
const { filters, onFilterClick = () => null } = props;
return (
<div className="border p-3" style={{ width: '560px', height: '400px', overflowY: 'auto'}}>
<div className="border p-3" style={{ width: '490px', height: '400px', overflowY: 'auto'}}>
<div className="grid grid-flow-row-dense grid-cols-2">
{filters && Object.keys(filters).map((key) => (
<div className="p-3">
<div className="uppercase font-medium mb-1">{key}</div>
<div>
{filters[key].map((filter: any) => (
<div className="flex items-center py-2 cursor-pointer" onClick={() => onFilterClick(filter)}>
<div className="flex items-center py-2 cursor-pointer hover:bg-gray-lightest -mx-2 px-2" onClick={() => onFilterClick(filter)}>
<Icon name={filter.icon} size="16"/>
<span className="ml-2">{filter.label}</span>
</div>

View file

@ -10,19 +10,20 @@ interface Props {
}
function FilterSelection(props: Props) {
const { filter, onFilterClick, children } = props;
const [showModal, setShowModal] = useState(false)
const [showModal, setShowModal] = useState(false);
return (
<div>
<div className="relative flex-shrink-0">
<OutsideClickDetectingDiv
className="relative"
onClickOutside={ () => setTimeout(function() {
setShowModal(false)
}, 20)}
}, 50)}
>
{ children ? React.cloneElement(children, { onClick: () => setShowModal(true)}) : (
<div
className="rounded border py-1 px-3 flex items-center cursor-pointer bg-gray-lightest text-ellipsis"
style={{ width: '140px', height: '30px'}}
className="rounded py-1 px-3 flex items-center cursor-pointer bg-gray-lightest text-ellipsis"
style={{ width: '140px', height: '30px', border: 'solid thin rgba(34, 36, 38, 0.15)'}}
onClick={() => setShowModal(true)}
>
<span className="mr-auto truncate">{filter.label}</span>

View file

@ -4,25 +4,29 @@ import FilterAutoComplete from '../FilterAutoComplete';
interface Props {
index: number;
value: any; // event/filter
// type: string;
key: string;
onRemoveValue?: () => void;
onAddValue?: () => void;
showCloseButton: boolean;
showOrButton: boolean;
onSelect: (e, item) => void;
}
function FilterValue(props: Props) {
const { index, value, showOrButton, onRemoveValue , onAddValue } = props;
const { index, value, key, showOrButton, showCloseButton, onRemoveValue , onAddValue } = props;
return (
<FilterAutoComplete
value={value}
showCloseButton={showCloseButton}
showOrButton={showOrButton}
onAddValue={onAddValue}
onRemoveValue={onRemoveValue}
method={'GET'}
endpoint='/events/search'
params={undefined}
params={{ type: key }}
headerText={''}
placeholder={''}
// placeholder={''}
onSelect={props.onSelect}
/>
);

View file

@ -3,16 +3,16 @@
align-items: center;
justify-content: space-around;
border: solid thin $gray-light;
border-radius: 5px;
border-radius: 3px;
overflow: hidden;
& .item {
color: $gray-medium;
font-weight: medium;
padding: 10px;
flex: 1;
/* flex: 1; */
text-align: center;
border-right: solid thin $gray-light;
border-right: solid thin $teal;
cursor: pointer;
background-color: $gray-lightest;
display: flex;
@ -64,6 +64,6 @@
}
.extraSmall .item {
padding: 2px 4px;
padding: 0 4px;
font-size: 12px;
}

View file

@ -48,6 +48,7 @@ function reducer(state = initialState, action = {}) {
case EDIT:
return state.mergeIn([ 'instance' ], CustomMetric(action.instance));
case UPDATE_SERIES:
console.log('update series', action.series);
return state.setIn(['instance', 'series', action.index], FilterSeries(action.series));
case success(SAVE):
return state.set([ 'instance' ], CustomMetric(action.data));
@ -55,11 +56,7 @@ function reducer(state = initialState, action = {}) {
return state.set("instance", ErrorInfo(action.data));
case success(FETCH_LIST):
const { data } = action;
return state
.set("totalCount", data ? data.total : 0)
.set("list", List(data && data.errors).map(CustomMetric)
.filter(e => e.parentErrorId == null)
.map(e => e.update("chart", chartWrapper)));
return state.set("list", List(data.map(CustomMetric)));
}
return state;
}
@ -95,11 +92,9 @@ export function save(instance) {
};
}
export function fetchList(params = {}, clear = false) {
export function fetchList() {
return {
types: array(FETCH_LIST),
call: client => client.post('/errors/search', params),
clear,
params: cleanParams(params),
call: client => client.get(`/${name}s`),
};
}

View file

@ -14,11 +14,9 @@ import { newFiltersList } from 'Types/filter'
import NewFilter, { filtersMap } from 'Types/filter/newFilter';
const filterOptions = {}
// newFiltersList.forEach(filter => {
// filterOptions[filter.category] = filter
// })
Object.keys(filtersMap).forEach(key => {
// const filter = NewFilter(filtersMap[key]);
const filter = filtersMap[key];
if (filterOptions.hasOwnProperty(filter.category)) {
filterOptions[filter.category].push(filter);

View file

@ -107,8 +107,9 @@
}
.form-group {
margin-bottom: 20px;
margin-bottom: 25px;
& label {
display: inline-block;
margin-bottom: 5px;
}
}

View file

@ -17,6 +17,7 @@ export const FilterSeries = Record({
methods: {
toData() {
const js = this.toJS();
delete js.key;
// js.filter = js.filter.toData();
return js;
},
@ -42,6 +43,7 @@ export default Record({
toData() {
const js = this.toJS();
js.series = js.series.map(series => {
series.filter.filters = series.filter.filters.map(filter => {
delete filter.operatorOptions
@ -51,6 +53,8 @@ export default Record({
return series;
});
delete js.key;
return js;
},
},

View file

@ -46,6 +46,7 @@ export default Record({
suspicious: undefined,
consoleLevel: undefined,
strict: false,
eventsOrder: 'and',
}, {
idKey: 'searchId',
methods: {
@ -53,10 +54,12 @@ export default Record({
const js = this.toJS();
js.filters = js.filters.map(filter => {
delete filter.operatorOptions
delete filter._key
return filter;
});
delete js.createdAt;
delete js.key;
return js;
}
},

View file

@ -219,41 +219,41 @@ export const booleanOptions = [
]
export const filtersMap = {
[TYPES.CLICK]: { category: 'interactions', label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click' },
[TYPES.INPUT]: { category: 'interactions', label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.LOCATION]: { category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.CLICK]: { key: TYPES.CLICK, type: 'multiple', category: 'interactions', label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click', isEvent: true },
[TYPES.INPUT]: { key: TYPES.INPUT, type: 'multiple', category: 'interactions', label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true },
[TYPES.LOCATION]: { key: TYPES.LOCATION, type: 'multiple', category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click', isEvent: true },
[TYPES.USER_OS]: { category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USER_BROWSER]: { category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USER_DEVICE]: { category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.PLATFORM]: { category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.REVID]: { category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USER_OS]: { key: TYPES.USER_OS, type: 'multiple', category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USER_BROWSER]: { key: TYPES.USER_BROWSER, type: 'multiple', category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USER_DEVICE]: { key: TYPES.USER_DEVICE, type: 'multiple', category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.PLATFORM]: { key: TYPES.PLATFORM, type: 'multiple', category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.REVID]: { key: TYPES.REVID, type: 'multiple', category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.REFERRER]: { category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.DURATION]: { category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USER_COUNTRY]: { category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.REFERRER]: { key: TYPES.REFERRER, type: 'multiple', category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.DURATION]: { key: TYPES.DURATION, type: 'number', category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USER_COUNTRY]: { key: TYPES.USER_COUNTRY, type: 'multiple', category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.CONSOLE]: { category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.ERROR]: { category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.FETCH]: { category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.GRAPHQL]: { category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.STATEACTION]: { category: 'javascript', label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.CONSOLE]: { key: TYPES.CONSOLE, type: 'multiple', category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.ERROR]: { key: TYPES.ERROR, type: 'multiple', category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.FETCH]: { key: TYPES.FETCH, type: 'multiple', category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.GRAPHQL]: { key: TYPES.GRAPHQL, type: 'multiple', category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.STATEACTION]: { key: TYPES.STATEACTION, type: 'multiple', category: 'javascript', label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USERID]: { category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USERANONYMOUSID]: { category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USERID]: { key: TYPES.USERID, type: 'multiple', category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.USERANONYMOUSID]: { key: TYPES.USERANONYMOUSID, type: 'multiple', category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.DOM_COMPLETE]: { category: 'new', label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.LARGEST_CONTENTFUL_PAINT_TIME]: { category: 'new', label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.TIME_BETWEEN_EVENTS]: { category: 'new', label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.TTFB]: { category: 'new', label: 'TTFB', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.AVG_CPU_LOAD]: { category: 'new', label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.AVG_MEMORY_USAGE]: { category: 'new', label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.SLOW_SESSION]: { category: 'new', label: 'Slow Session', operator: 'true', operatorOptions: [{ key: 'true', text: 'true', value: 'true' }], icon: 'filters/click' },
[TYPES.MISSING_RESOURCE]: { category: 'new', label: 'Missing Resource', operator: 'true', operatorOptions: [{ key: 'inImages', text: 'in images', value: 'true' }], icon: 'filters/click' },
[TYPES.CLICK_RAGE]: { category: 'new', label: 'Click Rage', operator: 'onAnything', operatorOptions: [{ key: 'onAnything', text: 'on anything', value: 'true' }], icon: 'filters/click' },
// [TYPES.URL]: { category: 'interactions', label: 'URL', operator: 'is', operatorOptions: stringFilterOptions },
// [TYPES.CUSTOM]: { category: 'interactions', label: 'Custom', operator: 'is', operatorOptions: stringFilterOptions },
// [TYPES.METADATA]: { category: 'interactions', label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions },
[TYPES.DOM_COMPLETE]: { key: TYPES.DOM_COMPLETE, type: 'multiple', category: 'new', label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.LARGEST_CONTENTFUL_PAINT_TIME]: { key: TYPES.LARGEST_CONTENTFUL_PAINT_TIME, type: 'number', category: 'new', label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.TIME_BETWEEN_EVENTS]: { key: TYPES.TIME_BETWEEN_EVENTS, type: 'number', category: 'new', label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.TTFB]: { key: TYPES.TTFB, type: 'time', category: 'new', label: 'TTFB', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.AVG_CPU_LOAD]: { key: TYPES.AVG_CPU_LOAD, type: 'number', category: 'new', label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.AVG_MEMORY_USAGE]: { key: TYPES.AVG_MEMORY_USAGE, type: 'number', category: 'new', label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
[TYPES.SLOW_SESSION]: { key: TYPES.SLOW_SESSION, type: 'boolean', category: 'new', label: 'Slow Session', operator: 'true', operatorOptions: [{ key: 'true', text: 'true', value: 'true' }], icon: 'filters/click' },
[TYPES.MISSING_RESOURCE]: { key: TYPES.MISSING_RESOURCE, type: 'boolean', category: 'new', label: 'Missing Resource', operator: 'true', operatorOptions: [{ key: 'inImages', text: 'in images', value: 'true' }], icon: 'filters/click' },
[TYPES.CLICK_RAGE]: { key: TYPES.CLICK_RAGE, type: 'boolean', category: 'new', label: 'Click Rage', operator: 'onAnything', operatorOptions: [{ key: 'onAnything', text: 'on anything', value: 'true' }], icon: 'filters/click' },
// [TYPES.URL]: { / [TYPES,TYPES. category: 'interactions', label: 'URL', operator: 'is', operatorOptions: stringFilterOptions },
// [TYPES.CUSTOM]: { / [TYPES,TYPES. category: 'interactions', label: 'Custom', operator: 'is', operatorOptions: stringFilterOptions },
// [TYPES.METADATA]: { / [TYPES,TYPES. category: 'interactions', label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions },
}
export default Record({
@ -263,6 +263,7 @@ export default Record({
icon: '',
type: '',
value: [""],
category: '',
custom: '',
// target: Target(),
@ -274,10 +275,13 @@ export default Record({
operator: 'is',
operatorOptions: [],
isEvent: false,
index: 0,
}, {
keyKey: "_key",
fromJS: ({ ...filter }) => ({
...filter,
key: filter.type,
type: filter.type, // camelCased(filter.type.toLowerCase()),
// key: filter.type === METADATA ? filter.label : filter.key || filter.type, // || camelCased(filter.type.toLowerCase()),
// label: getLabel(filter),
@ -299,10 +303,4 @@ export default Record({
// operators: filterMap[key].operatorOptions,
// value: [""]
// }
// }
// export const newFiltersList = [
// NewFilterType(TYPES.CLICK, 'Click', 'filters/click', true),
// NewFilterType(TYPES.CLICK, 'Input', 'filters/click', true),
// NewFilterType(TYPES.CONSOLE, 'Console', 'filters/click', true),
// ];
// }