feat(ui) - custom metrics

This commit is contained in:
Shekar Siri 2022-01-17 17:06:01 +05:30
parent 392feabec1
commit 155e7e4331
62 changed files with 1914 additions and 85 deletions

View file

@ -16,13 +16,14 @@ const siteIdRequiredPaths = [
'/integration/sources',
'/issue_types',
'/sample_rate',
'/flows',
'/saved_search',
'/rehydrations',
'/sourcemaps',
'/errors',
'/funnels',
'/assist',
'/heatmaps'
'/heatmaps',
'/custom_metrics',
];
const noStoringFetchPathStarts = [

View file

@ -27,9 +27,9 @@ class AttributeItem extends React.PureComponent {
applyFilter = debounce(this.props.applyFilter, 1000)
fetchFilterOptionsDebounce = debounce(this.props.fetchFilterOptions, 500)
onFilterChange = (e, { name, value }) => {
onFilterChange = (name, value, valueIndex) => {
const { index } = this.props;
this.props.editAttribute(index, name, value);
this.props.editAttribute(index, name, value, valueIndex);
this.applyFilter();
}
@ -69,13 +69,14 @@ class AttributeItem extends React.PureComponent {
/>
}
{
!filter.hasNoValue &&
// !filter.hasNoValue &&
<AttributeValueField
filter={ filter }
options={ options }
onChange={ this.onFilterChange }
handleSearchChange={this.handleSearchChange}
loading={loadingFilterOptions}
index={index}
/>
}

View file

@ -7,7 +7,7 @@ import { LinkStyledInput, CircularLoader } from 'UI';
import { KEYS } from 'Types/filter/customFilter';
import Event, { TYPES } from 'Types/filter/event';
import CustomFilter from 'Types/filter/customFilter';
import { setActiveKey, addCustomFilter, removeCustomFilter, applyFilter } from 'Duck/filters';
import { setActiveKey, addCustomFilter, removeCustomFilter, applyFilter, updateValue } from 'Duck/filters';
import DurationFilter from '../DurationFilter/DurationFilter';
import AutoComplete from '../AutoComplete';
@ -24,6 +24,7 @@ const getHeader = (type) => {
addCustomFilter,
removeCustomFilter,
applyFilter,
updateValue,
})
class AttributeValueField extends React.PureComponent {
state = {
@ -134,25 +135,46 @@ class AttributeValueField extends React.PureComponent {
return params;
}
onAddValue = () => {
const { index, filter } = this.props;
this.props.updateValue('filters', index, filter.value.concat(""));
}
onRemoveValue = (valueIndex) => {
const { index, filter } = this.props;
this.props.updateValue('filters', index, filter.value.filter((_, i) => i !== valueIndex));
}
onChange = (name, value, valueIndex) => {
const { index, filter } = this.props;
this.props.updateValue('filters', index, filter.value.map((item, i) => i === valueIndex ? value : item));
}
render() {
const { filter, onChange, onTargetChange } = this.props;
// const { filter, onChange } = this.props;
const { filter } = this.props;
const _showAutoComplete = this.isAutoComplete(filter.type);
const _params = _showAutoComplete ? this.getParams(filter) : {};
let _optionsEndpoint= '/events/search';
let _optionsEndpoint= '/events/search';
return (
<React.Fragment>
{ _showAutoComplete ?
<AutoComplete
{ _showAutoComplete ? filter.value.map((v, i) => (
<AutoComplete
name={ 'value' }
endpoint={ _optionsEndpoint }
value={ filter.value }
value={ v }
index={ i }
params={ _params }
optionMapping={this.optionMapping}
onSelect={ onChange }
onSelect={ (e, { name, value }) => onChange(name, value, i) }
headerText={ <h5 className={ stl.header }>{ getHeader(filter.type) }</h5> }
fullWidth={ (filter.type === TYPES.CONSOLE || filter.type === TYPES.LOCATION || filter.type === TYPES.CUSTOM) && filter.value }
onRemoveValue={() => this.onRemoveValue(i)}
onAddValue={this.onAddValue}
showCloseButton={i !== filter.value.length - 1}
/>
))
: this.renderField()
}
{ filter.type === 'INPUT' &&

View file

@ -1,9 +1,10 @@
import React from 'react';
import APIClient from 'App/api_client';
import cn from 'classnames';
import { Input } from 'UI';
import { Input, Icon } from 'UI';
import { debounce } from 'App/utils';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import EventSearchInput from 'Shared/EventSearchInput';
import stl from './autoComplete.css';
import FilterItem from '../CustomFilters/FilterItem';
@ -78,7 +79,7 @@ class AutoComplete extends React.PureComponent {
})
onInputChange = (e, { name, value }) => {
onInputChange = ({ target: { value } }) => {
changed = true;
this.setState({ query: value, updated: true })
const _value = value.trim();
@ -118,23 +119,53 @@ class AutoComplete extends React.PureComponent {
valueToText = defaultValueToText,
placeholder = 'Type to search...',
headerText = '',
fullWidth = false
fullWidth = false,
onRemoveValue = () => {},
onAddValue = () => {},
showCloseButton = false,
} = this.props;
const options = optionMapping(values, valueToText)
return (
<OutsideClickDetectingDiv
className={ cn("relative", { "flex-1" : fullWidth }) }
className={ cn("relative flex items-center", { "flex-1" : fullWidth }) }
onClickOutside={this.onClickOutside}
>
<Input
{/* <EventSearchInput /> */}
<div className={stl.inputWrapper}>
<input
name="query"
// className={cn(stl.input)}
onFocus={ () => this.setState({ddOpen: true})}
onChange={ this.onInputChange }
onBlur={ this.onBlur }
onFocus={ () => this.setState({ddOpen: true})}
value={ query }
autoFocus={ true }
type="text"
placeholder={ placeholder }
onPaste={(e) => {
const text = e.clipboardData.getData('Text');
this.hiddenInput.value = text;
pasted = true; // to use only the hidden input
} }
/>
<div className={stl.right} onClick={showCloseButton ? onRemoveValue : onAddValue}>
{ showCloseButton ? <Icon name="close" size="18" /> : <span className="px-1">or</span>}
</div>
</div>
{showCloseButton && <div className='ml-2'>or</div>}
{/* <Input
className={ cn(stl.searchInput, { [ stl.fullWidth] : fullWidth }) }
onChange={ this.onInputChange }
onBlur={ this.onBlur }
onFocus={ () => this.setState({ddOpen: true})}
value={ query }
icon="search"
// icon="search"
label={{ basic: true, content: <div>test</div> }}
labelPosition='right'
loading={ loading }
autoFocus={ true }
type="search"
@ -144,7 +175,7 @@ class AutoComplete extends React.PureComponent {
this.hiddenInput.value = text;
pasted = true; // to use only the hidden input
} }
/>
/> */}
<textarea style={hiddenStyle} ref={(ref) => this.hiddenInput = ref }></textarea>
{ ddOpen && options.length > 0 &&
<div className={ stl.menu }>

View file

@ -19,6 +19,13 @@
color: $gray-darkest !important;
font-size: 14px !important;
background-color: rgba(255, 255, 255, 0.8) !important;
& .label {
padding: 0px !important;
display: flex;
align-items: center;
justify-content: center;
}
}
height: 28px !important;
width: 280px;
@ -28,3 +35,30 @@
.fullWidth {
width: 100% !important;
}
.inputWrapper {
border: solid thin $gray-light !important;
border-radius: 3px;
border-radius: 3px;
display: flex;
align-items: center;
& input {
height: 28px;
font-size: 13px !important;
padding: 0 5px !important;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
& .right {
height: 28px;
display: flex;
align-items: center;
padding: 0 5px;
background-color: $gray-lightest;
border-left: solid thin $gray-light !important;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
cursor: pointer;
}
}

View file

@ -24,8 +24,10 @@ import SessionFlowList from './SessionFlowList/SessionFlowList';
import { LAST_7_DAYS } from 'Types/app/period';
import { resetFunnel } from 'Duck/funnels';
import { resetFunnelFilters } from 'Duck/funnelFilters'
import NoSessionsMessage from '../shared/NoSessionsMessage';
import TrackerUpdateMessage from '../shared/TrackerUpdateMessage';
import NoSessionsMessage from 'Shared/NoSessionsMessage';
import TrackerUpdateMessage from 'Shared/TrackerUpdateMessage';
import SessionSearchField from 'Shared/SessionSearchField'
import SavedSearch from 'Shared/SavedSearch'
import LiveSessionList from './LiveSessionList'
const weakEqual = (val1, val2) => {
@ -170,8 +172,12 @@ export default class BugFinder extends React.PureComponent {
data-hidden={ activeTab === 'live' || activeTab === 'favorite' }
className="mb-5"
>
<div className="flex items-center">
<div style={{ width: "70%", marginRight: "10px"}}><SessionSearchField /></div>
<SavedSearch />
</div>
<EventFilter />
</div>
</div>
{ activeFlow && activeFlow.type === 'flows' && <FunnelList /> }
{ activeTab.type !== 'live' && <SessionList onMenuItemClick={this.setActiveTab} /> }
{ activeTab.type === 'live' && <LiveSessionList /> }

View file

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import { Input } from 'semantic-ui-react';
import { DNDContext } from 'Components/hocs/dnd';
import {
addEvent, applyFilter, moveEvent, clearEvents,
addEvent, applyFilter, moveEvent, clearEvents, edit,
addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption
} from 'Duck/filters';
import { fetchList as fetchEventList } from 'Duck/events';
@ -11,7 +11,7 @@ import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import EventEditor from './EventEditor';
import ListHeader from '../ListHeader';
import FilterModal from '../CustomFilters/FilterModal';
import { IconButton } from 'UI';
import { IconButton, SegmentSelection } from 'UI';
import stl from './eventFilter.css';
import Attributes from '../Attributes/Attributes';
import RandomPlaceholder from './RandomPlaceholder';
@ -19,6 +19,7 @@ import CustomFilters from '../CustomFilters';
import ManageFilters from '../ManageFilters';
import { blink as setBlink } from 'Duck/funnels';
import cn from 'classnames';
import SaveFilterButton from 'Shared/SaveFilterButton';
@connect(state => ({
events: state.getIn([ 'filters', 'appliedFilter', 'events' ]),
@ -41,7 +42,8 @@ import cn from 'classnames';
setSearchQuery,
setActiveFlow,
setFilterOption,
setBlink
setBlink,
edit,
})
@DNDContext
export default class EventFilter extends React.PureComponent {
@ -109,6 +111,10 @@ export default class EventFilter extends React.PureComponent {
this.props.setActiveFlow(null)
}
changeConditionTab = (e, { name, value }) => {
this.props.edit({ [ 'condition' ]: value })
};
render() {
const {
events,
@ -124,34 +130,6 @@ export default class EventFilter extends React.PureComponent {
return (
<OutsideClickDetectingDiv className={ stl.wrapper } onClickOutside={ this.closeModal } >
{ showPlacehoder && !hasFilters &&
<div
className={ stl.randomElement }
onClick={ this.onPlaceholderClick }
>
{ !searchQuery &&
<div className={ stl.placeholder }>Search for users, clicks, page visits, requests, errors and more</div>
// <RandomPlaceholder onClick={ this.onPlaceholderItemClick } appliedFilterKeys={ appliedFilterKeys } />
}
</div>
}
<Input
inputProps={ { "data-openreplay-label": "Search", "autocomplete": "off" } }
className={stl.searchField}
ref={ this.inputRef }
onChange={ this.onSearchChange }
onKeyUp={this.onKeyUp}
value={searchQuery}
icon="search"
iconPosition="left"
placeholder={ hasFilters ? 'Search sessions using any captured event (click, input, page, error...)' : ''}
fluid
onFocus={ this.onFocus }
onBlur={ this.onBlur }
id="search"
autocomplete="off"
/>
<FilterModal
close={ this.closeModal }
displayed={ showFilterModal }
@ -161,7 +139,24 @@ 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={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>
{ events.size > 0 &&
<>
<div className="py-1"><ListHeader title="Events" /></div>
@ -189,6 +184,7 @@ export default class EventFilter extends React.PureComponent {
showFilters={ true }
/>
</div>
<SaveFilterButton />
<div className="flex items-center">
<div>
<IconButton plain label="CLEAR STEPS" onClick={ this.clearEvents } />

View file

@ -10,7 +10,7 @@
width: 150px;
color: $gray-darkest;
cursor: pointer;
background-color: rgba(255, 255, 255, 0.8) !important;
background-color: rgba(0, 0, 0, 0.1) !important;
&:hover {
background-color: white;
}

View file

@ -5,6 +5,7 @@ import withPermissions from 'HOCs/withPermissions'
import { setPeriod, setPlatform, fetchMetadataOptions } from 'Duck/dashboard';
import { NoContent } from 'UI';
import { WIDGET_KEYS } from 'Types/dashboard';
import CustomMetrics from 'Shared/CustomMetrics';
import {
MissingResources,
@ -184,6 +185,7 @@ export default class Dashboard extends React.PureComponent {
<div>
<div className={ cn(styles.header, "flex items-center w-full") }>
<MetricsFilters />
<CustomMetrics />
</div>
<div className="">
<NoContent

View file

@ -0,0 +1,118 @@
import React from 'react';
import { Form, SegmentSelection, Button } from 'UI';
import FilterSeries from '../FilterSeries';
import { connect } from 'react-redux';
import { edit as editMetric, save } from 'Duck/customMetrics';
interface Props {
metric: any;
editMetric: (metric) => void;
save: (metric) => void;
loading: boolean;
}
function CustomMetricForm(props: Props) {
const { metric, loading } = props;
const addSeries = () => {
const newSeries = {
name: `Series ${metric.series.size + 1}`,
type: '',
series: [],
filter: {
type: '',
value: '',
},
};
props.editMetric({
...metric,
series: [...metric.series, newSeries],
});
}
const removeSeries = (index) => {
const newSeries = metric.series.filter((_series, i) => {
return i !== index;
});
props.editMetric({
...metric,
series: newSeries,
});
}
const write = ({ target: { value, name } }) => props.editMetric({ ...metric.toData(), [ name ]: value })
const changeConditionTab = (e, { name, value }) => {
props.editMetric({ ...metric.toData(), [ 'type' ]: value })
};
return (
<Form
className="relative"
onSubmit={() => props.save(metric)}
>
<div className="p-5" style={{ height: 'calc(100vh - 60px)', overflowY: 'auto' }}>
<div className="form-group">
<label className="font-medium">Metric Title</label>
<input
autoFocus={ true }
className="text-lg"
name="name"
style={{ fontSize: '18px', padding: '10px', fontWeight: '600'}}
// value={ instance && instance.name }
onChange={ write }
placeholder="Metric Title"
id="name-field"
/>
</div>
<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"}}>
<SegmentSelection
primary
name="condition"
extraSmall={true}
// className="my-3"
onSelect={ changeConditionTab }
value={{ value: metric.type }}
list={ [
{ name: 'Session Count', value: 'session_count' },
{ name: 'Session Percentage', value: 'session_percentage' },
]}
/>
</div>
</div>
</div>
<div className="form-group">
<label className="font-medium">Sereis</label>
{metric.series && metric.series.size > 0 && metric.series.map((series: any, index: number) => (
<div className="mb-2">
<FilterSeries
seriesIndex={index}
series={series}
onRemoveSeries={() => removeSeries(index)}
/>
</div>
))}
</div>
<Button onClick={addSeries}>Add Series</Button>
</div>
<div className="absolute w-full bottom-0 px-5 py-2 bg-white">
<Button loading={loading} primary>
Save
</Button>
</div>
</Form>
);
}
export default connect(state => ({
metric: state.getIn(['customMetrics', 'instance']),
loading: state.getIn(['customMetrics', 'saveRequest', 'loading']),
}), { editMetric, save })(CustomMetricForm);

View file

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

View file

@ -0,0 +1,41 @@
import React from 'react';
import { IconButton, SlideModal } from 'UI'
import CustomMetricForm from './CustomMetricForm';
interface Props {}
function CustomMetrics(props: Props) {
return (
<div>
<IconButton outline icon="plus" label="CREATE METRIC" />
<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);
// } }
// size="medium"
content={
<div className="bg-gray-light-shade">
<CustomMetricForm />
</div>
}
/>
</div>
);
}
export default CustomMetrics;

View file

@ -0,0 +1,98 @@
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 FilterSelection from '../../Filters/FilterSelection';
interface Props {
seriesIndex: number;
series: any;
edit: typeof edit;
updateSeries: typeof updateSeries;
onRemoveSeries: (seriesIndex) => void;
}
function FilterSeries(props: Props) {
const { series, seriesIndex } = props;
const onAddFilter = (filter) => {
filter.value = [""]
const newFilters = series.filter.filters.concat(filter);
props.updateSeries(seriesIndex, {
...series,
filter: {
...series.filter,
filters: newFilters,
}
});
}
const onUpdateFilter = (filterIndex, filter) => {
const newFilters = series.filter.filters.map((_filter, i) => {
if (i === filterIndex) {
return filter;
} else {
return _filter;
}
});
props.updateSeries(seriesIndex, {
...series.toData(),
filter: {
...series.filter,
filters: newFilters,
}
});
}
const onRemoveFilter = (filterIndex) => {
const newFilters = series.filter.filters.filter((_filter, i) => {
return i !== filterIndex;
});
props.updateSeries(seriesIndex, {
...series,
filter: {
...series.filter,
filters: newFilters,
}
});
}
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>
</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>
</div>
);
}
export default connect(null, { edit, updateSeries })(FilterSeries);

View file

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

View file

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

View file

@ -137,12 +137,13 @@ class AttributeValueField extends React.PureComponent {
const { filter, onChange } = this.props;
const _showAutoComplete = this.isAutoComplete(filter.type);
const _params = _showAutoComplete ? this.getParams(filter) : {};
let _optionsEndpoint= '/events/search';
let _optionsEndpoint= '/events/search';
console.log('value', filter.value)
return (
<React.Fragment>
{ _showAutoComplete ?
<AutoComplete
<AutoComplete
name={ 'value' }
endpoint={ _optionsEndpoint }
value={ filter.value }
@ -151,6 +152,7 @@ class AttributeValueField extends React.PureComponent {
onSelect={ onChange }
headerText={ <h5 className={ stl.header }>{ getHeader(filter.type) }</h5> }
fullWidth={ (filter.type === TYPES.CONSOLE || filter.type === TYPES.LOCATION || filter.type === TYPES.CUSTOM) && filter.value }
// onAddOrRemove={}
/>
: this.renderField()
}

View file

@ -77,7 +77,7 @@ class AutoComplete extends React.PureComponent {
noResultsMessage: SOME_ERROR_MSG,
})
onInputChange = (e, { name, value }) => {
onInputChange = ({ target: { value } }) => {
changed = true;
this.setState({ query: value, updated: true })
const _value = value.trim();
@ -118,7 +118,8 @@ class AutoComplete extends React.PureComponent {
valueToText = defaultValueToText,
placeholder = 'Type to search...',
headerText = '',
fullWidth = false
fullWidth = false,
onAddOrRemove = () => null,
} = this.props;
const options = optionMapping(values, valueToText)
@ -128,7 +129,7 @@ class AutoComplete extends React.PureComponent {
className={ cn("relative", { "flex-1" : fullWidth }) }
onClickOutside={this.onClickOutside}
>
<Input
{/* <Input
className={ cn(stl.searchInput, { [ stl.fullWidth] : fullWidth }) }
onChange={ this.onInputChange }
onBlur={ this.onBlur }
@ -144,7 +145,30 @@ class AutoComplete extends React.PureComponent {
this.hiddenInput.value = text;
pasted = true; // to use only the hidden input
} }
/>
/> */}
<div className={stl.inputWrapper}>
<input
name="query"
// className={cn(stl.input)}
onFocus={ () => this.setState({ddOpen: true})}
onChange={ this.onInputChange }
onBlur={ this.onBlur }
onFocus={ () => this.setState({ddOpen: true})}
value={ query }
autoFocus={ true }
type="text"
placeholder={ placeholder }
onPaste={(e) => {
const text = e.clipboardData.getData('Text');
this.hiddenInput.value = text;
pasted = true; // to use only the hidden input
} }
/>
<div className={cn(stl.right, 'cursor-pointer')} onLick={onAddOrRemove}>
{/* <Icon name="close" size="18" /> */}
<span className="px-1">or</span>
</div>
</div>
<textarea style={hiddenStyle} ref={(ref) => this.hiddenInput = ref }></textarea>
{ ddOpen && options.length > 0 &&
<div className={ stl.menu }>

View file

@ -28,3 +28,30 @@
.fullWidth {
width: 100% !important;
}
.inputWrapper {
border: solid thin $gray-light !important;
border-radius: 3px;
border-radius: 3px;
display: flex;
align-items: center;
& input {
height: 28px;
font-size: 13px !important;
padding: 0 5px !important;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
& .right {
height: 28px;
display: flex;
align-items: center;
padding: 0 5px;
background-color: $gray-lightest;
border-left: solid thin $gray-light !important;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
cursor: pointer;
}
}

View file

@ -0,0 +1,17 @@
import React from 'react';
interface Props {
}
function EventSearchInput(props) {
return (
<div>
<input
className="border rounded p-1"
type="text" placeholder="Search for an event"
/>
</div>
);
}
export default EventSearchInput;

View file

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

View file

@ -0,0 +1,62 @@
.wrapper {
border: solid thin $gray-light !important;
border-radius: 3px;
border-radius: 3px;
display: flex;
align-items: center;
& input {
height: 28px;
font-size: 13px !important;
padding: 0 5px !important;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
border: solid thin transparent !important;
}
& .right {
height: 28px;
display: flex;
align-items: center;
padding: 0 5px;
background-color: $gray-lightest;
border-left: solid thin $gray-light !important;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
cursor: pointer;
}
}
.menu {
border-radius: 0 0 3px 3px;
box-shadow: 0 2px 10px 0 $gray-light;
padding: 20px;
background-color: white;
max-height: 350px;
overflow-y: auto;
position: absolute;
top: 28px;
left: 0;
width: 500px;
z-index: 99;
}
.filterItem {
display: flex;
align-items: center;
padding: 8px;
cursor: pointer;
border-radius: 3px;
transition: all 0.4s;
margin-bottom: 5px;
max-width: 100%;
& .label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover {
background-color: $gray-lightest;
transition: all 0.2s;
}
}

View file

@ -0,0 +1,147 @@
import React, { useState, useEffect } from 'react';
import { Icon, Loader } from 'UI';
import APIClient from 'App/api_client';
import { debounce } from 'App/utils';
import stl from './FilterAutoComplete.css';
import cn from 'classnames';
const hiddenStyle = {
whiteSpace: 'pre-wrap',
opacity: 0, position: 'fixed', left: '-3000px'
};
interface Props {
showOrButton?: boolean;
onRemoveValue?: () => void;
onAddValue?: () => void;
endpoint?: string;
method?: string;
params?: any;
headerText?: string;
placeholder?: string;
onSelect: (e, item) => void;
value: any;
}
function FilterAutoComplete(props: Props) {
const {
placeholder = 'Type to search',
method = 'GET',
showOrButton = false,
onRemoveValue = () => null,
onAddValue = () => null,
endpoint = '',
params = {},
headerText = '',
value = '',
} = props;
const [showModal, setShowModal] = useState(true)
const [loading, setLoading] = useState(false)
const [options, setOptions] = useState<any>([]);
const [query, setQuery] = useState(value);
const requestValues = (q) => {
// const { params, method } = props;
setLoading(true);
return new APIClient()[method?.toLowerCase()](endpoint, { ...params, q })
.then(response => response.json())
.then(({ errors, data }) => {
if (errors) {
// this.setError();
} else {
setOptions(data);
// this.setState({
// ddOpen: true,
// values: data,
// loading: false,
// noResultsMessage: NO_RESULTS_MSG,
// });
}
}).finally(() => setLoading(false));
// .catch(this.setError);
}
const debouncedRequestValues = debounce(requestValues, 1000)
const onInputChange = ({ target: { value } }) => {
setQuery(value);
}
useEffect(() => {
if (query === '' || query === ' ') {
return
}
debouncedRequestValues(query)
}, [query])
const onItemClick = (e, item) => {
e.stopPropagation();
e.preventDefault();
// const { onSelect, name } = this.props;
if (query !== item.value) {
setQuery(item.value);
}
// this.setState({ query: item.value, ddOpen: false})
props.onSelect(e, item);
// setTimeout(() => {
// setShowModal(false)
// }, 10)
}
return (
<div className="relative">
<div className={stl.wrapper}>
<input
name="query"
onChange={ onInputChange }
onBlur={ () => setTimeout(() => { setShowModal(false) }, 10) }
onFocus={ () => setShowModal(true)}
value={ query }
autoFocus={ true }
type="text"
placeholder={ placeholder }
// onPaste={(e) => {
// const text = e.clipboardData.getData('Text');
// // this.hiddenInput.value = text;
// // pasted = true; // to use only the hidden input
// } }
/>
<div
className={stl.right}
// onClick={showOrButton ? onRemoveValue : onAddValue}
>
{ !showOrButton && <Icon onClick={onRemoveValue} name="close" size="18" /> }
{ showOrButton && <span onClick={onAddValue} className="px-1">or</span>}
</div>
</div>
{/* <textarea style={hiddenStyle} ref={(ref) => this.hiddenInput = ref }></textarea> */}
{ showModal && (options.length > 0 || loading) &&
<div className={ stl.menu }>
{ headerText && headerText }
<Loader loading={loading} size="small">
{
options.map(item => (
<div
className={ cn(stl.filterItem) }
id="filter-item" onClick={ (e) => onItemClick(e, item) }
>
{ item.icon && <Icon name={ item.icon } size="16" marginRight="8" /> }
<span className={ stl.label }>{ item.value }</span>
</div>
))
}
</Loader>
</div>
}
</div>
);
}
export default FilterAutoComplete;

View file

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

View file

@ -0,0 +1,78 @@
import React from 'react';
import FilterOperator from '../FilterOperator/FilterOperator';
import FilterSelection from '../FilterSelection';
import FilterValue from '../FilterValue';
import { Icon } from 'UI';
interface Props {
filterIndex: number;
filter: any; // event/filter
onUpdate: (filter) => void;
onRemoveFilter: () => void;
}
function FitlerItem(props: Props) {
const { filterIndex, filter, onUpdate } = props;
const replaceFilter = (filter) => {
onUpdate(filter);
};
const onAddValue = () => {
const newValues = filter.value.concat("")
onUpdate({ ...filter, value: newValues })
}
const onRemoveValue = (valueIndex) => {
const newValues = filter.value.filter((_, _index) => _index !== valueIndex)
onUpdate({ ...filter, value: newValues })
}
const onSelect = (e, item, valueIndex) => {
const newValues = filter.value.map((_, _index) => {
if (_index === valueIndex) {
return item.value;
}
return _;
})
onUpdate({ ...filter, value: newValues })
}
console.log('filter', filter);
const onOperatorChange = (e, { name, value }) => {
onUpdate({ ...filter, operator: value })
}
return (
<div className="flex items-center mb-3">
<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"/>
<div className="grid grid-cols-3 gap-3">
{filter.value && filter.value.map((value, valueIndex) => (
<FilterValue
showOrButton={valueIndex === filter.value.length - 1}
key={valueIndex}
value={value}
index={valueIndex}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
onSelect={(e, item) => onSelect(e, valueIndex, item)}
/>
))}
</div>
</div>
<div className="flex">
<div
className="cursor-pointer"
onClick={props.onRemoveFilter}
>
<Icon name="close" size="18" />
</div>
</div>
</div>
);
}
export default FitlerItem;

View file

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

View file

@ -0,0 +1,34 @@
import React, { useState} from 'react';
import FilterItem from '../FilterItem';
interface Props {
filters: any[]; // event/filter
onUpdateFilter: (filterIndex, filter) => void;
onRemoveFilter: (filterIndex) => void;
}
function FilterList(props: Props) {
const { filters } = props;
const onRemoveFilter = (filterIndex) => {
const newFilters = filters.filter((_filter, i) => {
return i !== filterIndex;
});
props.onRemoveFilter(filterIndex);
}
return (
<div className="flex flex-col">
{filters.map((filter, filterIndex) => (
<FilterItem
filterIndex={filterIndex}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex) }
/>
))}
</div>
);
}
export default FilterList;

View file

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

View file

@ -0,0 +1,34 @@
import React from 'react';
import { Icon } from 'UI';
import { connect } from 'react-redux';
interface Props {
filters: any,
onFilterClick?: (filter) => void
}
function FilterModal(props: Props) {
const { filters, onFilterClick = () => null } = props;
return (
<div className="border p-3" style={{ width: '560px', 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)}>
<Icon name={filter.icon} size="16"/>
<span className="ml-2">{filter.label}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
}
export default connect(state => ({
filters: state.getIn([ 'filters', 'filterList' ])
}))(FilterModal);

View file

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

View file

@ -0,0 +1,19 @@
.operatorDropdown {
font-weight: 400;
height: 30px;
min-width: 60px;
display: flex !important;
align-items: center;
justify-content: space-between;
padding: 0 8px !important;
font-size: 13px;
/* background-color: rgba(255, 255, 255, 0.8) !important; */
background-color: $gray-lightest !important;
border: solid thin rgba(34, 36, 38, 0.15) !important;
border-radius: 4px !important;
color: $gray-darkest !important;
font-size: 14px !important;
&.ui.basic.button {
box-shadow: 0 0 0 1px rgba(62, 170, 175,36,38,.35) inset, 0 0 0 0 rgba(62, 170, 175,.15) inset !important;
}
}

View file

@ -0,0 +1,29 @@
import React from 'react';
import cn from 'classnames';
import { Dropdown, Icon } from 'UI';
import stl from './FilterOperator.css';
interface Props {
filter: any; // event/filter
// options: any[];
// value: string;
onChange: (e, { name, value }) => void;
className?: string;
}
function FilterOperator(props: Props) {
const { filter, onChange, className = '' } = props;
const options = []
return (
<Dropdown
className={ cn(stl.operatorDropdown, className) }
options={ filter.operatorOptions }
name="operator"
value={ filter.operator }
onChange={ onChange }
icon={ <Icon className="ml-5" name="chevron-down" size="12" /> }
/>
);
}
export default FilterOperator;

View file

@ -0,0 +1,42 @@
import React, { useState } from 'react';
import FilterModal from '../FilterModal';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import { Icon } from 'UI';
interface Props {
filter: any; // event/filter
onFilterClick: (filter) => void;
children?: any;
}
function FilterSelection(props: Props) {
const { filter, onFilterClick, children } = props;
const [showModal, setShowModal] = useState(false)
return (
<div>
<OutsideClickDetectingDiv
className="relative"
onClickOutside={ () => setTimeout(function() {
setShowModal(false)
}, 20)}
>
{ 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'}}
onClick={() => setShowModal(true)}
>
<span className="mr-auto truncate">{filter.label}</span>
<Icon name="chevron-down" size="14" />
</div>
) }
</OutsideClickDetectingDiv>
{showModal && (
<div className="absolute left-0 top-20 border shadow rounded bg-white z-50">
<FilterModal onFilterClick={onFilterClick} />
</div>
)}
</div>
);
}
export default FilterSelection;

View file

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

View file

@ -0,0 +1,31 @@
import React from 'react';
import FilterAutoComplete from '../FilterAutoComplete';
interface Props {
index: number;
value: any; // event/filter
onRemoveValue?: () => void;
onAddValue?: () => void;
showOrButton: boolean;
onSelect: (e, item) => void;
}
function FilterValue(props: Props) {
const { index, value, showOrButton, onRemoveValue , onAddValue } = props;
return (
<FilterAutoComplete
value={value}
showOrButton={showOrButton}
onAddValue={onAddValue}
onRemoveValue={onRemoveValue}
method={'GET'}
endpoint='/events/search'
params={undefined}
headerText={''}
placeholder={''}
onSelect={props.onSelect}
/>
);
}
export default FilterValue;

View file

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

View file

@ -0,0 +1,26 @@
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { save } from 'Duck/filters';
import { Button } from 'UI';
import SaveSearchModal from 'Shared/SaveSearchModal'
interface Props {
filter: any;
}
function SaveFilterButton(props) {
const [showModal, setshowModal] = useState(false)
return (
<div>
<Button onClick={() => setshowModal(true)}>SAVE FILTER</Button>
<SaveSearchModal
show={showModal}
closeHandler={() => setshowModal(false)}
/>
</div>
);
}
export default connect(state => ({
filter: state.getIn([ 'filters', 'appliedFilter' ]),
}), { save })(SaveFilterButton);

View file

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

View file

@ -0,0 +1,78 @@
import React from 'react';
import { connect } from 'react-redux';
import { edit, save } from 'Duck/filters';
import { Button, Modal, Form, Icon, Checkbox } from 'UI';
import stl from './SaveSearchModal.css';
interface Props {
filter: any;
loading: boolean;
edit: (filter: any) => void;
save: (filter: any) => Promise<void>;
show: boolean;
closeHandler: () => void;
}
function SaveSearchModal(props: Props) {
const { filter, loading, show, closeHandler } = props;
const onNameChange = ({ target: { value } }) => {
props.edit({ name: value });
};
const onSave = () => {
const { filter, closeHandler } = props;
if (filter.name.trim() === '') return;
props.save(filter).then(function() {
// this.props.fetchFunnelsList();
closeHandler();
});
}
console.log('filter', filter);
return (
<Modal size="tiny" open={ show }>
<Modal.Header className={ stl.modalHeader }>
<div>{ 'Save Search' }</div>
<Icon
role="button"
tabIndex="-1"
color="gray-dark"
size="18"
name="close"
onClick={ closeHandler }
/>
</Modal.Header>
<Modal.Content>
<Form onSubmit={onSave}>
<Form.Field>
<label>{'Title:'}</label>
<input
autoFocus={ true }
// className={ stl.name }
name="name"
value={ filter.name }
onChange={ onNameChange }
placeholder="Title"
/>
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions className="">
<Button
primary
onClick={ onSave }
loading={ loading }
>
{ filter.exists() ? 'Modify' : 'Save' }
</Button>
<Button className={ stl.cancelButton } marginRight onClick={ closeHandler }>{ 'Cancel' }</Button>
</Modal.Actions>
</Modal>
);
}
export default connect(state => ({
filter: state.getIn(['filters', 'instance']),
loading: state.getIn([ 'filters', 'saveRequest', 'loading' ]) ||
state.getIn([ 'filters', 'updateRequest', 'loading' ]),
}), { edit, save })(SaveSearchModal);

View file

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

View file

@ -0,0 +1,15 @@
@import 'mixins.css';
.modalHeader {
display: flex !important;
align-items: center;
justify-content: space-between;
}
.cancelButton {
@mixin plainButton;
}
.applyButton {
@mixin basicButton;
}

View file

@ -0,0 +1,47 @@
import React, { useState, useEffect } from 'react';
import { Button, Icon } from 'UI';
import SavedSearchDropdown from './components/SavedSearchDropdown';
import { connect } from 'react-redux';
import { fetchList as fetchListSavedSearch } from 'Duck/filters';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
interface Props {
fetchListSavedSearch: () => void;
list: any;
}
function SavedSearch(props) {
const [showMenu, setShowMenu] = useState(false)
useEffect(() => {
props.fetchListSavedSearch()
}, [])
return (
<OutsideClickDetectingDiv
// className={ cn("relative", { "flex-1" : fullWidth }) }
onClickOutside={() => setShowMenu(false)}
>
<div className="relative">
<Button prime outline size="small"
className="flex items-center"
onClick={() => setShowMenu(true)}
>
<span className="mr-2">Search Saved</span>
<Icon name="ellipsis-v" color="teal" size="14" />
</Button>
{ showMenu && (
<div
className="absolute right-0 bg-white border rounded z-50"
style={{ top: '33px', width: '200px' }}
>
<SavedSearchDropdown list={props.list}/>
</div>
)}
</div>
</OutsideClickDetectingDiv>
);
}
export default connect(state => ({
list: state.getIn([ 'filters', 'list' ]),
}), { fetchListSavedSearch })(SavedSearch);

View file

@ -0,0 +1,7 @@
.wrapper {
position: relative;
display: inline-block;
z-index: 999;
display: flex;
flex-direction: column;
}

View file

@ -0,0 +1,24 @@
import React from 'react';
import stl from './SavedSearchDropdown.css';
interface Props {
list: Array<any>
}
function Row ({ name }) {
return (
<div className="p-2 cursor-pointer hover:bg-gray-lightest">{name}</div>
)
}
function SavedSearchDropdown(props: Props) {
return (
<div className={stl.wrapper}>
{props.list.map(item => (
<Row key={item.searchId} name={item.name} />
))}
</div>
);
}
export default SavedSearchDropdown;

View file

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

View file

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

View file

@ -0,0 +1,10 @@
.searchField {
box-shadow: none !important;
& input {
box-shadow: none !important;
border-radius: 3 !important;
border: solid thin $gray-light !important;
height: 34px !important;
font-size: 16px;
}
}

View file

@ -0,0 +1,69 @@
import React, { useRef, useState } from 'react';
import { connect } from 'react-redux';
import stl from './SessionSearchField.css';
import { Input } from 'UI';
import FilterModal from 'Shared/EventFilter/FilterModal';
import { fetchList as fetchEventList } from 'Duck/events';
import { debounce } from 'App/utils';
import {
addEvent, applyFilter, moveEvent, clearEvents,
addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption
} from 'Duck/filters';
interface Props {
setSearchQuery: (query: string) => void;
fetchEventList: (query: any) => void;
searchQuery: string
}
function SessionSearchField(props: Props) {
const debounceFetchEventList = debounce(props.fetchEventList, 1000)
const [showModal, setShowModal] = useState(false)
const onSearchChange = (e, { value }) => {
// props.setSearchQuery(value)
debounceFetchEventList({ q: value });
}
return (
<div className="relative">
<Input
inputProps={ { "data-openreplay-label": "Search", "autocomplete": "off" } }
className={stl.searchField}
onFocus={ () => setShowModal(true) }
onBlur={ () => setTimeout(setShowModal, 100, false) }
// ref={ this.inputRef }
onChange={ onSearchChange }
// onKeyUp={this.onKeyUp}
// value={props.searchQuery}
icon="search"
iconPosition="left"
placeholder={ 'Search sessions using any captured event (click, input, page, error...)'}
fluid
id="search"
type="search"
autocomplete="off"
/>
<FilterModal
close={ () => setShowModal(false) }
displayed={ showModal }
// displayed={ true }
// loading={ loading }
// searchedEvents={ searchedEvents }
searchQuery={ props.searchQuery }
/>
</div>
);
}
export default connect(state => ({
events: state.getIn([ 'filters', 'appliedFilter', 'events' ]),
appliedFilter: state.getIn([ 'filters', 'appliedFilter' ]),
searchQuery: state.getIn([ 'filters', 'searchQuery' ]),
appliedFilterKeys: state.getIn([ 'filters', 'appliedFilter', 'filters' ])
.map(({type}) => type).toJS(),
searchedEvents: state.getIn([ 'events', 'list' ]),
loading: state.getIn([ 'events', 'loading' ]),
strict: state.getIn([ 'filters', 'appliedFilter', 'strict' ]),
blink: state.getIn([ 'funnels', 'blink' ]),
}), { setSearchQuery, fetchEventList })(SessionSearchField);

View file

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

View file

@ -9,12 +9,13 @@ class SegmentSelection extends React.Component {
}
render() {
const { className, list, primary = false, size = "normal" } = this.props;
const { className, list, small = false, extraSmall = false, primary = false, size = "normal" } = this.props;
return (
<div className={ cn(styles.wrapper, {
[styles.primary] : primary,
[styles.small] : size === 'small'
[styles.small] : size === 'small' || small,
[styles.extraSmall] : extraSmall,
}, className) }
>
{ list.map(item => (

View file

@ -61,4 +61,9 @@
.small .item {
padding: 4px 8px;
}
.extraSmall .item {
padding: 2px 4px;
font-size: 12px;
}

View file

@ -0,0 +1,105 @@
import { List, Map } from 'immutable';
import { clean as cleanParams } from 'App/api_client';
import ErrorInfo, { RESOLVED, UNRESOLVED, IGNORED } from 'Types/errorInfo';
import CustomMetric, { FilterSeries } from 'Types/customMetric'
import { createFetch, fetchListType, fetchType, saveType, editType, createEdit } from './funcTools/crud';
// import { createEdit, createInit } from './funcTools/crud';
import { createRequestReducer, ROOT_KEY } from './funcTools/request';
import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools';
import Filter from 'Types/filter';
import NewFilter from 'Types/filter/newFilter';
import Event from 'Types/filter/event';
// import CustomFilter from 'Types/filter/customFilter';
const name = "custom_metric";
const idKey = "metricId";
const FETCH_LIST = fetchListType(name);
const FETCH = fetchType(name);
const SAVE = saveType(name);
const EDIT = editType(name);
const UPDATE_SERIES = `${name}/UPDATE_SERIES`;
function chartWrapper(chart = []) {
return chart.map(point => ({ ...point, count: Math.max(point.count, 0) }));
}
// const updateItemInList = createListUpdater(idKey);
// const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ]
// ? state.mergeIn([ "instance" ], instance)
// : state;
const initialState = Map({
list: List(),
instance: CustomMetric({
name: 'New',
series: List([
{
name: 'Session Count',
filter: new Filter({ filters: [] }),
},
])
}),
});
// Metric - Series - [] - filters
function reducer(state = initialState, action = {}) {
switch (action.type) {
case EDIT:
return state.mergeIn([ 'instance' ], CustomMetric(action.instance));
case UPDATE_SERIES:
return state.setIn(['instance', 'series', action.index], FilterSeries(action.series));
case success(SAVE):
return state.set([ 'instance' ], CustomMetric(action.data));
case success(FETCH):
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;
}
export default mergeReducers(
reducer,
createRequestReducer({
[ ROOT_KEY ]: FETCH_LIST,
fetch: FETCH,
}),
);
export const edit = createEdit(name);
export const updateSeries = (index, series) => ({
type: UPDATE_SERIES,
index,
series,
});
export function fetch(id) {
return {
id,
types: array(FETCH),
call: c => c.get(`/errors/${id}`),
}
}
export function save(instance) {
return {
types: SAVE.array,
call: client => client.post( `/${ name }s`, instance.toData()),
};
}
export function fetchList(params = {}, clear = false) {
return {
types: array(FETCH_LIST),
call: client => client.post('/errors/search', params),
clear,
params: cleanParams(params),
};
}

View file

@ -7,8 +7,33 @@ import CustomFilter, { KEYS } from 'Types/filter/customFilter';
import withRequestState, { RequestTypes } from './requestStateCreator';
import { fetchList as fetchSessionList } from './sessions';
import { fetchList as fetchErrorsList } from './errors';
import { fetchListType, fetchType, saveType, editType, initType, removeType } from './funcTools/crud/types';
import logger from 'App/logger';
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 = filtersMap[key];
if (filterOptions.hasOwnProperty(filter.category)) {
filterOptions[filter.category].push(filter);
} else {
filterOptions[filter.category] = [filter];
}
})
console.log('filterOptions', filterOptions)
// for (var i = 0; i < newFiltersList.length; i++) {
// filterOptions[newFiltersList[i].category] = newFiltersList.filter(filter => filter.category === newFiltersList[i].category)
// }
const ERRORS_ROUTE = errorsRoute();
const FETCH_LIST = new RequestTypes('filters/FETCH_LIST');
@ -16,6 +41,7 @@ const FETCH_FILTER_OPTIONS = new RequestTypes('filters/FETCH_FILTER_OPTIONS');
const SET_FILTER_OPTIONS = 'filters/SET_FILTER_OPTIONS';
const SAVE = new RequestTypes('filters/SAVE');
const REMOVE = new RequestTypes('filters/REMOVE');
const EDIT = editType('funnel/EDIT');
const SET_SEARCH_QUERY = 'filters/SET_SEARCH_QUERY';
const SET_ACTIVE = 'filters/SET_ACTIVE';
@ -35,7 +61,11 @@ const EDIT_ATTRIBUTE = 'filters/EDIT_ATTRIBUTE';
const REMOVE_ATTRIBUTE = 'filters/REMOVE_ATTRIBUTE';
const SET_ACTIVE_FLOW = 'filters/SET_ACTIVE_FLOW';
const UPDATE_VALUE = 'filters/UPDATE_VALUE';
const initialState = Map({
filterList: filterOptions,
instance: Filter(),
activeFilter: null,
list: List(),
appliedFilter: Filter(),
@ -71,6 +101,8 @@ const updateList = (state, instance) => state.update('list', (list) => {
const reducer = (state = initialState, action = {}) => {
let optionsMap = null;
switch (action.type) {
case EDIT:
return state.mergeIn([ 'appliedFilter' ], action.instance);
case FETCH_FILTER_OPTIONS.SUCCESS:
optionsMap = state.getIn(['filterOptions', action.key]).map(i => i.value).toJS();
return state.mergeIn(['filterOptions', action.key], Set(action.data.filter(i => !optionsMap.includes(i.value))));
@ -177,6 +209,8 @@ const reducer = (state = initialState, action = {}) => {
return state.removeIn([ 'appliedFilter', 'filters', action.index ]);
case SET_SEARCH_QUERY:
return state.set('searchQuery', action.query);
case UPDATE_VALUE:
return state.setIn([ 'appliedFilter', action.filterType, action.index, 'value' ], action.value);
default:
return state;
}
@ -234,7 +268,7 @@ export function removeAttribute(index) {
export function fetchList(range) {
return {
types: FETCH_LIST.toArray(),
call: client => client.get(`/flows${range ? '?range_value=' + range : ''}`),
call: client => client.get(`/saved_search`),
};
}
@ -257,7 +291,7 @@ export function setFilterOption(key, filterOption) {
export function save(instance) {
return {
types: SAVE.toArray(),
call: client => client.post('/filters', instance.toData()),
call: client => client.post('/saved_search', instance.toData()),
instance,
};
}
@ -367,4 +401,21 @@ export function setSearchQuery(query) {
type: SET_SEARCH_QUERY,
query
}
}
export const edit = instance => {
return {
type: EDIT,
instance,
}
};
// filterType: 'events' or 'filters'
export const updateValue = (filterType, index, value) => {
return {
type: UPDATE_VALUE,
filterType,
index,
value
}
}

View file

@ -117,9 +117,8 @@ const reducer = (state = initialState, action = {}) => {
.set('issueTypesMap', tmpMap);
case FETCH_INSIGHTS_SUCCESS:
let stages = [];
if (action.isRefresh) {
if (action.isRefresh) {
const activeStages = state.get('activeStages');
console.log('test', activeStages);
const oldInsights = state.get('insights');
const lastStage = action.data.stages[action.data.stages.length - 1]
const lastStageIndex = activeStages.toJS()[1];

View file

@ -34,6 +34,7 @@ import errors from './errors';
import funnels from './funnels';
import config from './config';
import roles from './roles';
import customMetrics from './customMetrics';
export default combineReducers({
jwt,
@ -68,6 +69,7 @@ export default combineReducers({
funnels,
config,
roles,
customMetrics,
...integrations,
...sources,
});

View file

@ -106,4 +106,9 @@
opacity: .5;
}
.form-group {
margin-bottom: 20px;
& label {
margin-bottom: 5px;
}
}

View file

@ -0,0 +1,61 @@
import Record from 'Types/Record';
import { List } from 'immutable';
// import { DateTime } from 'luxon';
// import { validateEmail, validateName } from 'App/validate';
import Filter from 'Types/filter';
import NewFilter from 'Types/filter';
// import { Event } from 'Types/filter';
// import CustomFilter from 'Types/filter/customFilter';
export const FilterSeries = Record({
seriesId: undefined,
index: undefined,
name: 'Filter Series',
filter: new Filter(),
}, {
idKey: 'seriesId',
methods: {
toData() {
const js = this.toJS();
// js.filter = js.filter.toData();
return js;
},
},
fromJS: ({ filter, ...rest }) => ({
...rest,
filter: new Filter(filter),
}),
});
export default Record({
metricId: undefined,
name: 'Series',
type: 'session_count',
series: List(),
isPublic: false,
}, {
idKey: 'metricId',
methods: {
validate() {
return validateName(this.name, { diacritics: true });
},
toData() {
const js = this.toJS();
js.series = js.series.map(series => {
series.filter.filters = series.filter.filters.map(filter => {
delete filter.operatorOptions
delete filter.icon
return filter;
});
return series;
});
return js;
},
},
fromJS: ({ series, ...rest }) => ({
...rest,
series: List(series).map(FilterSeries),
}),
});

View file

@ -30,6 +30,20 @@ const REVID = 'REVID';
const USERANONYMOUSID = 'USERANONYMOUSID';
const USERID = 'USERID';
const ISSUE = 'ISSUE';
const EVENTS_COUNT = 'EVENTS_COUNT';
const UTM_SOURCE = 'UTM_SOURCE';
const UTM_MEDIUM = 'UTM_MEDIUM';
const UTM_CAMPAIGN = 'UTM_CAMPAIGN';
const DOM_COMPLETE = 'DOM_COMPLETE';
const LARGEST_CONTENTFUL_PAINT_TIME = 'LARGEST_CONTENTFUL_PAINT_TIME';
const TIME_BETWEEN_EVENTS = 'TIME_BETWEEN_EVENTS';
const TTFB = 'TTFB';
const AVG_CPU_LOAD = 'AVG_CPU_LOAD';
const AVG_MEMORY_USAGE = 'AVG_MEMORY_USAGE';
export const KEYS = {
ERROR,
MISSING_RESOURCE,
@ -56,7 +70,19 @@ export const KEYS = {
STATEACTION,
REVID,
USERANONYMOUSID,
USERID
USERID,
ISSUE,
EVENTS_COUNT,
UTM_SOURCE,
UTM_MEDIUM,
UTM_CAMPAIGN,
DOM_COMPLETE,
LARGEST_CONTENTFUL_PAINT_TIME,
TIME_BETWEEN_EVENTS,
TTFB,
AVG_CPU_LOAD,
AVG_MEMORY_USAGE,
};
const getOperatorDefault = (type) => {
@ -85,7 +111,7 @@ export default Record({
label: '',
icon: '',
type: '',
value: '',
value: [""],
custom: '',
target: Target(),
level: '',

View file

@ -17,6 +17,13 @@ const USERANONYMOUSID = 'USERANONYMOUSID';
const USERID = 'USERID';
const ERROR = 'ERROR';
const DOM_COMPLETE = 'DOM_COMPLETE';
const LARGEST_CONTENTFUL_PAINT_TIME = 'LARGEST_CONTENTFUL_PAINT_TIME';
const TIME_BETWEEN_EVENTS = 'TIME_BETWEEN_EVENTS';
const TTFB = 'TTFB';
const AVG_CPU_LOAD = 'AVG_CPU_LOAD';
const AVG_MEMORY_USAGE = 'AVG_MEMORY_USAGE';
export const TYPES = {
CONSOLE,
GRAPHQL,
@ -31,7 +38,14 @@ export const TYPES = {
METADATA,
USERANONYMOUSID,
USERID,
ERROR
ERROR,
DOM_COMPLETE,
LARGEST_CONTENTFUL_PAINT_TIME,
TIME_BETWEEN_EVENTS,
TTFB,
AVG_CPU_LOAD,
AVG_MEMORY_USAGE,
};
function getLabelText(type, source) {
@ -49,11 +63,19 @@ function getLabelText(type, source) {
if (type === TYPES.USERID) return 'User Id';
if (type === TYPES.USERANONYMOUSID) return 'User Anonymous Id';
if (type === TYPES.REVID) return 'Rev ID';
if (type === TYPES.DOM_COMPLETE) return 'DOM Complete';
if (type === TYPES.LARGEST_CONTENTFUL_PAINT_TIME) return 'Largest Contentful Paint Time';
if (type === TYPES.TIME_BETWEEN_EVENTS) return 'Time Between Events';
if (type === TYPES.TTFB) return 'TTFB';
if (type === TYPES.AVG_CPU_LOAD) return 'Avg CPU Load';
if (type === TYPES.AVG_MEMORY_USAGE) return 'Avg Memory Usage';
if (type === TYPES.CUSTOM) {
if (!source) return 'Custom';
return source;
}
return '?';
return type;
}
export default Record({
@ -63,7 +85,7 @@ export default Record({
operator: 'on',
type: '',
searchType: '',
value: '',
value: [],
custom: '',
inputValue: '',
target: Target(),
@ -73,16 +95,16 @@ export default Record({
}, {
keyKey: "_key",
fromJS: ({ target, type = "" , ...event }) => {
const viewType = type.split('_')[0];
// const viewType = type.split('_')[0];
return {
...event,
type: viewType,
type: type,
searchType: event.searchType || type,
label: getLabelText(viewType, event.source),
label: getLabelText(type, event.source),
target: Target(target),
operator: event.operator || defaultOperator(event),
// value: target ? target.label : event.value,
icon: event.icon || getEventIcon({...event, type: viewType })
icon: event.icon || getEventIcon({...event, type: type })
};
}
})

View file

@ -9,7 +9,8 @@ import {
getDateRangeFromValue
} from 'App/dateRange';
import Event from './event';
import CustomFilter from './customFilter';
// import CustomFilter from './customFilter';
import NewFilter from './newFilter';
const rangeValue = DATE_RANGE_VALUES.LAST_7_DAYS;
const range = getDateRangeFromValue(rangeValue);
@ -17,8 +18,8 @@ const startDate = range.start.unix() * 1000;
const endDate = range.end.unix() * 1000;
export default Record({
name: undefined,
id: undefined,
name: '',
searchId: undefined,
referrer: undefined,
userBrowser: undefined,
userOs: undefined,
@ -33,6 +34,7 @@ export default Record({
rangeValue,
startDate,
endDate,
condition: 'then',
sort: undefined,
order: undefined,
@ -45,6 +47,19 @@ export default Record({
consoleLevel: undefined,
strict: false,
}, {
idKey: 'searchId',
methods: {
toData() {
const js = this.toJS();
js.filters = js.filters.map(filter => {
delete filter.operatorOptions
return filter;
});
delete js.createdAt;
return js;
}
},
fromJS({ filters, events, custom, ...filter }) {
let startDate;
let endDate;
@ -59,7 +74,7 @@ export default Record({
startDate,
endDate,
events: List(events).map(Event),
filters: List(filters).map(CustomFilter),
filters: List(filters).map(NewFilter),
custom: Map(custom),
}
}
@ -73,6 +88,12 @@ export const defaultFilters = [
type: 'default',
keys: [
{ label: 'Click', key: KEYS.CLICK, type: KEYS.CLICK, filterKey: KEYS.CLICK, icon: 'filters/click', isFilter: false },
{ label: 'DOM Complete', key: KEYS.DOM_COMPLETE, type: KEYS.DOM_COMPLETE, filterKey: KEYS.DOM_COMPLETE, icon: 'filters/click', isFilter: false },
{ label: 'Largest Contentful Paint Time', key: KEYS.LARGEST_CONTENTFUL_PAINT_TIME, type: KEYS.LARGEST_CONTENTFUL_PAINT_TIME, filterKey: KEYS.LARGEST_CONTENTFUL_PAINT_TIME, icon: 'filters/click', isFilter: false },
{ label: 'Time Between Events', key: KEYS.TIME_BETWEEN_EVENTS, type: KEYS.TIME_BETWEEN_EVENTS, filterKey: KEYS.TIME_BETWEEN_EVENTS, icon: 'filters/click', isFilter: false },
{ label: 'Avg CPU Load', key: KEYS.AVG_CPU_LOAD, type: KEYS.AVG_CPU_LOAD, filterKey: KEYS.AVG_CPU_LOAD, icon: 'filters/click', isFilter: false },
{ label: 'Memory Usage', key: KEYS.AVG_MEMORY_USAGE, type: KEYS.AVG_MEMORY_USAGE, filterKey: KEYS.AVG_MEMORY_USAGE, icon: 'filters/click', isFilter: false },
{ label: 'Input', key: KEYS.INPUT, type: KEYS.INPUT, filterKey: KEYS.INPUT, icon: 'event/input', isFilter: false },
{ label: 'Page', key: KEYS.LOCATION, type: KEYS.LOCATION, filterKey: KEYS.LOCATION, icon: 'event/link', isFilter: false },
// { label: 'View', key: KEYS.VIEW, type: KEYS.VIEW, filterKey: KEYS.VIEW, icon: 'event/view', isFilter: false }
@ -103,6 +124,11 @@ export const defaultFilters = [
type: 'default',
keys: [
{ label: 'Errors', key: KEYS.ERROR, type: KEYS.ERROR, filterKey: KEYS.ERROR, icon: 'exclamation-circle', isFilter: false },
{ label: 'Issues', key: KEYS.ISSUES, type: KEYS.ISSUES, filterKey: KEYS.ISSUES, icon: 'exclamation-circle', isFilter: true },
{ label: 'UTM Source', key: KEYS.UTM_SOURCE, type: KEYS.UTM_SOURCE, filterKey: KEYS.UTM_SOURCE, icon: 'exclamation-circle', isFilter: true },
{ label: 'UTM Medium', key: KEYS.UTM_MEDIUM, type: KEYS.UTM_MEDIUM, filterKey: KEYS.UTM_MEDIUM, icon: 'exclamation-circle', isFilter: true },
{ label: 'UTM Campaign', key: KEYS.UTM_CAMPAIGN, type: KEYS.UTM_CAMPAIGN, filterKey: KEYS.UTM_CAMPAIGN, icon: 'exclamation-circle', isFilter: true },
{ label: 'Fetch Requests', key: KEYS.FETCH, type: KEYS.FETCH, filterKey: KEYS.FETCH, icon: 'fetch', isFilter: false },
{ label: 'GraphQL Queries', key: KEYS.GRAPHQL, type: KEYS.GRAPHQL, filterKey: KEYS.GRAPHQL, icon: 'vendors/graphql', isFilter: false },
{ label: 'Store Actions', key: KEYS.STATEACTION, type: KEYS.STATEACTION, filterKey: KEYS.STATEACTION, icon: 'store', isFilter: false },
@ -127,6 +153,13 @@ export const getEventIcon = (filter) => {
if (type === KEYS.USERBROWSER) return 'window';
if (type === KEYS.PLATFORM) return 'window';
if (type === TYPES.DOM_COMPLETE) return 'filters/click';
if (type === TYPES.LARGEST_CONTENTFUL_PAINT_TIME) return 'filters/click';
if (type === TYPES.TIME_BETWEEN_EVENTS) return 'filters/click';
if (type === TYPES.TTFB) return 'filters/click';
if (type === TYPES.AVG_CPU_LOAD) return 'filters/click';
if (type === TYPES.AVG_MEMORY_USAGE) return 'filters/click';
if (type === TYPES.CLICK) return 'filters/click';
if (type === TYPES.LOCATION) return 'map-marker-alt';
if (type === TYPES.VIEW) return 'event/view';

View file

@ -6,7 +6,7 @@ export * from './filter';
const filterKeys = ['is', 'isNot'];
const stringFilterKeys = ['is', 'isNot', 'contains'];
const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith'];
const targetFilterKeys = ['on', 'notOn'];
const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp'];
const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast'];
@ -209,6 +209,12 @@ export const operatorOptions = (filter) => {
case TYPES.REVID:
case 'metadata':
case KEYS.ERROR:
case TYPES.DOM_COMPLETE:
case TYPES.LARGEST_CONTENTFUL_PAINT_TIME:
case TYPES.TIME_BETWEEN_EVENTS:
case TYPES.TTFB:
case TYPES.AVG_CPU_LOAD:
case TYPES.AVG_MEMORY_USAGE:
return stringFilterOptions;
case TYPES.INPUT:
@ -230,4 +236,22 @@ export const operatorOptions = (filter) => {
case KEYS.CLICK_RAGE:
return [{ key: 'onAnything', text: 'on anything', value: 'true' }]
}
}
}
const NewFilterType = (key, category, label, icon, isEvent = false) => {
return {
key: key,
category: category,
label: label,
icon: icon,
isEvent: isEvent,
operators: operatorOptions({ key }),
value: [""]
}
}
export const newFiltersList = [
NewFilterType(TYPES.CLICK, 'Gear', 'Click', 'filters/click', true),
NewFilterType(TYPES.CLICK, 'Gear', 'Input', 'filters/click', true),
NewFilterType(TYPES.CONSOLE, 'Other', 'Console', 'filters/click', true),
];

View file

@ -0,0 +1,308 @@
import Record from 'Types/Record';
const CLICK = 'CLICK';
const INPUT = 'INPUT';
const LOCATION = 'LOCATION';
const VIEW = 'VIEW_IOS';
const CONSOLE = 'ERROR';
const METADATA = 'METADATA';
const CUSTOM = 'CUSTOM';
const URL = 'URL';
const CLICK_RAGE = 'CLICKRAGE';
const USER_BROWSER = 'USERBROWSER';
const USER_OS = 'USEROS';
const USER_COUNTRY = 'USERCOUNTRY';
const USER_DEVICE = 'USERDEVICE';
const PLATFORM = 'PLATFORM';
const DURATION = 'DURATION';
const REFERRER = 'REFERRER';
const ERROR = 'ERROR';
const MISSING_RESOURCE = 'MISSINGRESOURCE';
const SLOW_SESSION = 'SLOWSESSION';
const JOURNEY = 'JOUNRNEY';
const FETCH = 'REQUEST';
const GRAPHQL = 'GRAPHQL';
const STATEACTION = 'STATEACTION';
const REVID = 'REVID';
const USERANONYMOUSID = 'USERANONYMOUSID';
const USERID = 'USERID';
const ISSUE = 'ISSUE';
const EVENTS_COUNT = 'EVENTS_COUNT';
const UTM_SOURCE = 'UTM_SOURCE';
const UTM_MEDIUM = 'UTM_MEDIUM';
const UTM_CAMPAIGN = 'UTM_CAMPAIGN';
const DOM_COMPLETE = 'DOM_COMPLETE';
const LARGEST_CONTENTFUL_PAINT_TIME = 'LARGEST_CONTENTFUL_PAINT_TIME';
const TIME_BETWEEN_EVENTS = 'TIME_BETWEEN_EVENTS';
const TTFB = 'TTFB';
const AVG_CPU_LOAD = 'AVG_CPU_LOAD';
const AVG_MEMORY_USAGE = 'AVG_MEMORY_USAGE';
export const TYPES = {
ERROR,
MISSING_RESOURCE,
SLOW_SESSION,
CLICK_RAGE,
CLICK,
INPUT,
LOCATION,
VIEW,
CONSOLE,
METADATA,
CUSTOM,
URL,
USER_BROWSER,
USER_OS,
USER_DEVICE,
PLATFORM,
DURATION,
REFERRER,
USER_COUNTRY,
JOURNEY,
FETCH,
GRAPHQL,
STATEACTION,
REVID,
USERANONYMOUSID,
USERID,
ISSUE,
EVENTS_COUNT,
UTM_SOURCE,
UTM_MEDIUM,
UTM_CAMPAIGN,
DOM_COMPLETE,
LARGEST_CONTENTFUL_PAINT_TIME,
TIME_BETWEEN_EVENTS,
TTFB,
AVG_CPU_LOAD,
AVG_MEMORY_USAGE,
};
const filterKeys = ['is', 'isNot'];
const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith'];
const targetFilterKeys = ['on', 'notOn'];
const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp'];
const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast'];
const options = [
{
key: 'is',
text: 'is',
value: 'is'
}, {
key: 'isNot',
text: 'is not',
value: 'isNot'
}, {
key: 'startsWith',
text: 'starts with',
value: 'startsWith'
}, {
key: 'endsWith',
text: 'ends with',
value: 'endsWith'
}, {
key: 'contains',
text: 'contains',
value: 'contains'
}, {
key: 'doesNotContain',
text: 'does not contain',
value: 'doesNotContain'
}, {
key: 'hasAnyValue',
text: 'has any value',
value: 'hasAnyValue'
}, {
key: 'hasNoValue',
text: 'has no value',
value: 'hasNoValue'
},
{
key: 'isSignedUp',
text: 'is signed up',
value: 'isSignedUp'
}, {
key: 'notSignedUp',
text: 'not signed up',
value: 'notSignedUp'
},
{
key: 'before',
text: 'before',
value: 'before'
}, {
key: 'after',
text: 'after',
value: 'after'
}, {
key: 'on',
text: 'on',
value: 'on'
}, {
key: 'notOn',
text: 'not on',
value: 'notOn'
}, {
key: 'inRage',
text: 'in rage',
value: 'inRage'
}, {
key: 'notInRage',
text: 'not in rage',
value: 'notInRage'
}, {
key: 'withinLast',
text: 'within last',
value: 'withinLast'
}, {
key: 'notWithinLast',
text: 'not within last',
value: 'notWithinLast'
},
{
key: 'greaterThan',
text: 'greater than',
value: 'greaterThan'
}, {
key: 'lessThan',
text: 'less than',
value: 'lessThan'
}, {
key: 'equal',
text: 'equal',
value: 'equal'
}, {
key: 'not equal',
text: 'not equal',
value: 'not equal'
},
{
key: 'onSelector',
text: 'on selector',
value: 'onSelector'
}, {
key: 'onText',
text: 'on text',
value: 'onText'
}, {
key: 'onComponent',
text: 'on component',
value: 'onComponent'
},
{
key: 'onAnything',
text: 'on anything',
value: 'onAnything'
}
];
export const filterOptions = options.filter(({key}) => filterKeys.includes(key));
export const stringFilterOptions = options.filter(({key}) => stringFilterKeys.includes(key));
export const targetFilterOptions = options.filter(({key}) => targetFilterKeys.includes(key));
export const booleanOptions = [
{ key: 'true', text: 'true', value: 'true' },
{ key: 'false', text: 'false', value: 'false' },
]
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.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.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.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.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.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 },
}
export default Record({
timestamp: 0,
key: '',
label: '',
icon: '',
type: '',
value: [""],
custom: '',
// target: Target(),
level: '',
source: null,
hasNoValue: false,
isFilter: false,
actualValue: '',
operator: 'is',
operatorOptions: [],
}, {
keyKey: "_key",
fromJS: ({ ...filter }) => ({
...filter,
type: filter.type, // camelCased(filter.type.toLowerCase()),
// key: filter.type === METADATA ? filter.label : filter.key || filter.type, // || camelCased(filter.type.toLowerCase()),
// label: getLabel(filter),
// target: Target(target),
// operator: getOperatorDefault(filter.type),
// value: target ? target.label : filter.value,
// value: typeof value === 'string' ? [value] : value,
// icon: filter.type ? getfilterIcon(filter.type) : 'filters/metadata'
}),
})
// const NewFilterType = (key, category, icon, isEvent = false) => {
// return {
// key: key,
// category: category,
// label: filterMap[key].label,
// icon: icon,
// isEvent: isEvent,
// 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),
// ];

View file

@ -5,13 +5,13 @@ require('dotenv').config()
const oss = {
name: 'oss',
PRODUCTION: true,
PRODUCTION: false,
SENTRY_ENABLED: false,
SENTRY_URL: "",
CAPTCHA_ENABLED: process.env.CAPTCHA_ENABLED === 'true',
CAPTCHA_SITE_KEY: process.env.CAPTCHA_SITE_KEY,
ORIGIN: () => 'window.location.origin',
API_EDP: () => 'window.location.origin + "/api"',
API_EDP: "https://dol.openreplay.com/api",
ASSETS_HOST: () => 'window.location.origin + "/assets"',
VERSION: '1.3.6',
SOURCEMAP: true,