feat(ui) - custom metrics
This commit is contained in:
parent
392feabec1
commit
155e7e4331
62 changed files with 1914 additions and 85 deletions
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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' &&
|
||||
|
|
|
|||
|
|
@ -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 }>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 /> }
|
||||
|
|
|
|||
|
|
@ -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 } />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricForm';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterSeries'
|
||||
1
frontend/app/components/shared/CustomMetrics/index.ts
Normal file
1
frontend/app/components/shared/CustomMetrics/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetrics';
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
1
frontend/app/components/shared/EventSearchInput/index.ts
Normal file
1
frontend/app/components/shared/EventSearchInput/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './EventSearchInput';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterAutoComplete';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterItem';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterList';
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterModal';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterSelection';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FilterValue';
|
||||
|
|
@ -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);
|
||||
1
frontend/app/components/shared/SaveFilterButton/index.ts
Normal file
1
frontend/app/components/shared/SaveFilterButton/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SaveFilterButton'
|
||||
|
|
@ -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);
|
||||
1
frontend/app/components/shared/SaveSearchModal/index.ts
Normal file
1
frontend/app/components/shared/SaveSearchModal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SaveSearchModal'
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
@import 'mixins.css';
|
||||
|
||||
.modalHeader {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
@mixin plainButton;
|
||||
}
|
||||
|
||||
.applyButton {
|
||||
@mixin basicButton;
|
||||
}
|
||||
47
frontend/app/components/shared/SavedSearch/SavedSearch.tsx
Normal file
47
frontend/app/components/shared/SavedSearch/SavedSearch.tsx
Normal 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);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SavedSearchDropdown';
|
||||
1
frontend/app/components/shared/SavedSearch/index.ts
Normal file
1
frontend/app/components/shared/SavedSearch/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SavedSearch'
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionSearchField';
|
||||
|
|
@ -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 => (
|
||||
|
|
|
|||
|
|
@ -61,4 +61,9 @@
|
|||
|
||||
.small .item {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.extraSmall .item {
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
105
frontend/app/duck/customMetrics.js
Normal file
105
frontend/app/duck/customMetrics.js
Normal 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),
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -106,4 +106,9 @@
|
|||
opacity: .5;
|
||||
}
|
||||
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
& label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
61
frontend/app/types/customMetric.js
Normal file
61
frontend/app/types/customMetric.js
Normal 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),
|
||||
}),
|
||||
});
|
||||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
};
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
];
|
||||
308
frontend/app/types/filter/newFilter.js
Normal file
308
frontend/app/types/filter/newFilter.js
Normal 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),
|
||||
// ];
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue