Merge branch 'dev-assist-filters' into dev

This commit is contained in:
Shekar Siri 2022-06-15 19:08:01 +02:00
commit 6ba773fe6d
35 changed files with 440 additions and 647 deletions

View file

@ -4,12 +4,16 @@ import LiveSessionSearch from 'Shared/LiveSessionSearch';
import cn from 'classnames'
import withPageTitle from 'HOCs/withPageTitle';
import withPermissions from 'HOCs/withPermissions'
// import SessionSearch from '../shared/SessionSearch';
// import MainSearchBar from '../shared/MainSearchBar';
import AssistSearchField from './AssistSearchField';
function Assist() {
return (
<div className="page-margin container-90 flex relative">
<div className="flex-1 flex">
<div className={cn("w-full mx-auto")} style={{ maxWidth: '1300px'}}>
<AssistSearchField />
<LiveSessionSearch />
<div className="my-4" />
<LiveSessionList />

View file

@ -0,0 +1,32 @@
import React from 'react';
import { Button } from 'UI';
import SessionSearchField from 'Shared/SessionSearchField';
// import { fetchFilterSearch } from 'Duck/search';
import { connect } from 'react-redux';
import { edit as editFilter, addFilterByKeyAndValue, clearSearch, fetchFilterSearch } from 'Duck/liveSearch';
// import { clearSearch } from 'Duck/search';
function AssistSearchField(props: any) {
return (
<div className="flex items-center">
<div style={{ width: "60%", marginRight: "10px"}}>
<SessionSearchField
fetchFilterSearch={props.fetchFilterSearch}
addFilterByKeyAndValue={props.addFilterByKeyAndValue}
/>
</div>
<Button
variant="text-primary"
className="ml-auto font-medium"
// disabled={!hasFilters}
onClick={() => props.clearSearch()}
>
Clear Search
</Button>
</div>
);
}
export default connect(null, {
fetchFilterSearch, editFilter, addFilterByKeyAndValue, clearSearch
})(AssistSearchField);

View file

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

View file

@ -10,6 +10,7 @@ import stl from './sortDropdown.module.css';
export default class SortDropdown extends React.PureComponent {
state = { value: null }
sort = ({ value }) => {
value = value.value
this.setState({ value: value })
const [ sort, order ] = value.split('-');
const sign = order === 'desc' ? -1 : 1;
@ -25,7 +26,6 @@ export default class SortDropdown extends React.PureComponent {
<Select
name="sortSessions"
plain
// className={ stl.dropdown }
right
options={ options }
onChange={ this.sort }

View file

@ -1,7 +1,7 @@
import React from 'react'
import { connect } from 'react-redux';
import cn from 'classnames';
import { SideMenuitem, SavedSearchList, Popup } from 'UI'
import { SideMenuitem, Popup } from 'UI'
import stl from './sessionMenu.module.css';
import { clearEvents } from 'Duck/filters';
import { issues_types } from 'Types/session/issue'
@ -10,7 +10,7 @@ import { useModal } from 'App/components/Modal';
import SessionSettings from 'Shared/SessionSettings/SessionSettings'
function SessionsMenu(props) {
const { activeTab, keyMap, wdTypeCount, toggleRehydratePanel, isEnterprise } = props;
const { activeTab, isEnterprise } = props;
const { showModal } = useModal();
const onMenuItemClick = (filter) => {
@ -45,34 +45,25 @@ function SessionsMenu(props) {
{ issues_types.filter(item => item.visible).map(item => (
<SideMenuitem
key={item.key}
// disabled={!keyMap[item.type] && !wdTypeCount[item.type]}
active={activeTab.type === item.type}
title={item.name} iconName={item.icon}
onClick={() => onMenuItemClick(item)}
/>
))}
<div className={stl.divider} />
<div className="my-3">
<SideMenuitem
title={ isEnterprise ? "Vault" : "Bookmarks" }
iconName={ isEnterprise ? "safe" : "star" }
active={activeTab.type === 'bookmark'}
onClick={() => onMenuItemClick({ name: isEnterprise ? 'Vault' : 'Bookmarks', type: 'bookmark', description: isEnterprise ? 'Sessions saved to vault never get\'s deleted from records.' : '' })}
// TODO show the description in header
/>
</div>
<div className={cn(stl.divider, 'mb-4')} />
<SavedSearchList />
<div className={cn(stl.divider, 'my-4')} />
<SideMenuitem
title={ isEnterprise ? "Vault" : "Bookmarks" }
iconName={ isEnterprise ? "safe" : "star" }
active={activeTab.type === 'bookmark'}
onClick={() => onMenuItemClick({ name: isEnterprise ? 'Vault' : 'Bookmarks', type: 'bookmark', description: isEnterprise ? 'Sessions saved to vault never get\'s deleted from records.' : '' })}
/>
</div>
)
}
export default connect(state => ({
activeTab: state.getIn([ 'search', 'activeTab' ]),
keyMap: state.getIn([ 'sessions', 'keyMap' ]),
wdTypeCount: state.getIn([ 'sessions', 'wdTypeCount' ]),
captureRate: state.getIn(['watchdogs', 'captureRate']),
filters: state.getIn([ 'filters', 'appliedFilter' ]),
sessionsLoading: state.getIn([ 'sessions', 'fetchLiveListRequest', 'loading' ]),

View file

@ -51,7 +51,7 @@ class CustomFields extends React.Component {
}
onChangeSelect = ({ value }) => {
const site = this.props.sites.find(s => s.id === value);
const site = this.props.sites.find(s => s.id === value.value);
this.setState({ currentSite: site })
this.props.fetchList(site.id);
}

View file

@ -7,7 +7,7 @@ import {
assist,
client,
errors,
funnels,
// funnels,
dashboard,
withSiteId,
CLIENT_DEFAULT_TAB,
@ -21,18 +21,17 @@ import OnboardingExplore from './OnboardingExplore/OnboardingExplore'
import Announcements from '../Announcements';
import Notifications from '../Alerts/Notifications';
import { init as initSite, fetchList as fetchSiteList } from 'Duck/site';
import Logo from '../../svg/logo-small.svg';
import ErrorGenPanel from 'App/dev/components';
import ErrorsBadge from 'Shared/ErrorsBadge';
import Alerts from '../Alerts/Alerts';
import AnimatedSVG, { ICONS } from '../shared/AnimatedSVG/AnimatedSVG';
import { fetchList as fetchMetadata } from 'Duck/customField';
const DASHBOARD_PATH = dashboard();
const SESSIONS_PATH = sessions();
const ASSIST_PATH = assist();
const ERRORS_PATH = errors();
const FUNNELS_PATH = funnels();
// const FUNNELS_PATH = funnels();
const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
const AUTOREFRESH_INTERVAL = 30 * 1000;
@ -52,33 +51,15 @@ const Header = (props) => {
useEffect(() => {
activeSite = sites.find(s => s.id == siteId);
props.initSite(activeSite);
props.fetchMetadata();
}, [sites])
const showTrackingModal = (
isRoute(SESSIONS_PATH, location.pathname) ||
isRoute(ERRORS_PATH, location.pathname)
) && activeSite && !activeSite.recorded;
useEffect(() => {
if(showTrackingModal) {
interval = setInterval(() => {
fetchSiteList()
}, AUTOREFRESH_INTERVAL);
} else if (interval){
clearInterval(interval);
}
}, [showTrackingModal])
return (
<div className={ cn(styles.header, showTrackingModal ? styles.placeOnTop : '') }>
<div className={ cn(styles.header) }>
<NavLink to={ withSiteId(SESSIONS_PATH, siteId) }>
<div className="relative">
{/* <img src={ Logo } alt="React Logo" /> */}
{/* <object style={{ width: '30px' }} type="image/svg+xml" data={ Logo } /> */}
<div className="p-2">
<AnimatedSVG name={ICONS.LOGO_SMALL} size="30" />
{/* <object style={{ width: '30px' }} type="image/svg+xml" data={ Logo } /> */}
{/* <Logo width={30} /> */}
</div>
<div className="absolute bottom-0" style={{ fontSize: '7px', right: '5px' }}>v{window.env.VERSION}</div>
</div>
@ -100,7 +81,7 @@ const Header = (props) => {
>
{ 'Assist' }
</NavLink>
<NavLink
{/* <NavLink
to={ withSiteId(ERRORS_PATH, siteId) }
className={ styles.nav }
activeClassName={ styles.active }
@ -113,7 +94,7 @@ const Header = (props) => {
activeClassName={ styles.active }
>
{ 'Funnels' }
</NavLink>
</NavLink> */}
<NavLink
to={ withSiteId(DASHBOARD_PATH, siteId) }
className={ styles.nav }
@ -164,5 +145,5 @@ export default withRouter(connect(
showAlerts: state.getIn([ 'dashboard', 'showAlerts' ]),
boardingCompletion: state.getIn([ 'dashboard', 'boardingCompletion' ])
}),
{ onLogoutClick: logout, initSite, fetchSiteList },
{ onLogoutClick: logout, initSite, fetchSiteList, fetchMetadata },
)(Header));

View file

@ -14,15 +14,15 @@ const COUNTRY = 'country';
const LOCATION = 'location';
const platformOptions = [
{ 'key': PLATFORM, text: 'Desktop', value: 1},
{ 'key': PLATFORM, text: 'Tablet', value: 2 },
{ 'key': PLATFORM, text: 'Tablet Landscape', value: 3 },
{ 'key': PLATFORM, text: 'Mobile', value: 4 },
{ 'key': PLATFORM, text: 'Mobile Landscape', value: 5 }
{ 'key': PLATFORM, label: 'Desktop', value: 1},
{ 'key': PLATFORM, label: 'Tablet', value: 2 },
{ 'key': PLATFORM, label: 'Tablet Landscape', value: 3 },
{ 'key': PLATFORM, label: 'Mobile', value: 4 },
{ 'key': PLATFORM, label: 'Mobile Landscape', value: 5 }
];
const countryOptions = Object.keys(countries).map(c => ({key: COUNTRY, text: countries[c], value: c}));
const locationOptions = Object.keys(regionLabels).map(k => ({ key: LOCATION, text: regionLabels[k], value: k}));
const countryOptions = Object.keys(countries).map(c => ({key: COUNTRY, label: countries[c], value: c}));
const locationOptions = Object.keys(regionLabels).map(k => ({ key: LOCATION, label: regionLabels[k], value: k}));
const _filterKeys = [
{ key: 'userId', name: 'User ID', icon: 'user-alt', placeholder: 'Search for User ID' },

View file

@ -20,7 +20,7 @@ interface Props {
params?: any;
headerText?: string;
placeholder?: string;
onSelect: (e, item) => void;
onSelect: (e: any, item: any) => void;
value: any;
icon?: string;
}
@ -44,17 +44,17 @@ function FilterAutoComplete(props: Props) {
const [options, setOptions] = useState<any>([]);
const [query, setQuery] = useState(value);
const requestValues = (q) => {
const requestValues = (q: any) => {
setLoading(true);
return new APIClient()[method?.toLocaleLowerCase()](endpoint, { ...params, q })
.then(response => {
.then((response: any) => {
if (response.ok) {
return response.json();
}
throw new Error(response.statusText);
})
.then(({ data }) => {
.then(({ data }: any) => {
setOptions(data);
})
.finally(() => setLoading(false));
@ -62,7 +62,7 @@ function FilterAutoComplete(props: Props) {
const debouncedRequestValues = React.useCallback(debounce(requestValues, 1000), [params]);
const onInputChange = ({ target: { value } }) => {
const onInputChange = ({ target: { value } }: any) => {
setQuery(value);
if (!showModal) {
setShowModal(true);
@ -85,7 +85,7 @@ function FilterAutoComplete(props: Props) {
}
}
const onItemClick = (e, item) => {
const onItemClick = (e: any, item: any) => {
e.stopPropagation();
e.preventDefault();
@ -132,7 +132,7 @@ function FilterAutoComplete(props: Props) {
) : (
<div>
{
options.map((item, i) => (
options.map((item: any, i: any) => (
<div
key={item.value + '_' + i}
className={ cn(stl.filterItem) }

View file

@ -51,6 +51,7 @@ function FilterModal(props: Props) {
} = props;
const hasSearchQuery = searchQuery && searchQuery.length > 0;
const showSearchList = isMainSearch && searchQuery.length > 0;
console.log('filters', props.filters)
const onFilterSearchClick = (filter) => {
const _filter = filtersMap[filter.type];
@ -123,7 +124,7 @@ function FilterModal(props: Props) {
}
export default connect(state => ({
filters: state.getIn([ 'search', 'filterList' ]),
// filters: state.getIn([ 'search', 'filterListLive' ]),
filterSearchList: state.getIn([ 'search', 'filterSearchList' ]),
metaOptions: state.getIn([ 'customFields', 'list' ]),
fetchingFilterSearchList: state.getIn([ 'search', 'fetchFilterSearch', 'loading' ]),

View file

@ -1,6 +1,5 @@
import React, { useState } from 'react';
import FilterModal from '../FilterModal';
import LiveFilterModal from '../LiveFilterModal';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import { Icon } from 'UI';
import { connect } from 'react-redux';
@ -10,6 +9,8 @@ const ASSIST_ROUTE = assistRoute();
interface Props {
filter?: any; // event/filter
filterList: any;
filterListLive: any;
onFilterClick: (filter) => void;
children?: any;
isLive?: boolean;
@ -43,13 +44,15 @@ function FilterSelection(props: Props) {
</OutsideClickDetectingDiv>
{showModal && (
<div className="absolute left-0 border shadow rounded bg-white z-50">
{ isRoute(ASSIST_ROUTE, window.location.pathname) ? <LiveFilterModal onFilterClick={onFilterClick} /> : <FilterModal onFilterClick={onFilterClick} /> }
<FilterModal onFilterClick={onFilterClick} filters={isRoute(ASSIST_ROUTE, window.location.pathname) ? props.filterListLive : props.filterList } />
</div>
)}
</div>
);
}
export default connect(state => ({
export default connect((state: any) => ({
filters: state.getIn([ 'search', 'filterList' ]),
liveFilters: state.getIn([ 'search', 'filterListLive' ]),
isLive: state.getIn([ 'sessions', 'activeTab' ]).type === 'live',
}), { })(FilterSelection);

View file

@ -5,10 +5,13 @@ import { FilterKey, FilterCategory, FilterType } from 'Types/filter/filterType';
import FilterValueDropdown from '../FilterValueDropdown';
import FilterDuration from '../FilterDuration';
import { debounce } from 'App/utils';
import { assist as assistRoute, isRoute } from "App/routes";
const ASSIST_ROUTE = assistRoute();
interface Props {
filter: any;
onUpdate: (filter) => void;
onUpdate: (filter: any) => void;
}
function FilterValue(props: Props) {
const { filter } = props;
@ -21,13 +24,13 @@ function FilterValue(props: Props) {
props.onUpdate({ ...filter, value: newValue });
}
const onRemoveValue = (valueIndex) => {
const newValue = filter.value.filter((_, index) => index !== valueIndex);
const onRemoveValue = (valueIndex: any) => {
const newValue = filter.value.filter((_: any, index: any) => index !== valueIndex);
props.onUpdate({ ...filter, value: newValue });
}
const onChange = (e, item, valueIndex) => {
const newValues = filter.value.map((_, _index) => {
const onChange = (e: any, item: any, valueIndex: any) => {
const newValues = filter.value.map((_: any, _index: any) => {
if (_index === valueIndex) {
return item.value;
}
@ -38,11 +41,11 @@ function FilterValue(props: Props) {
const debounceOnSelect = React.useCallback(debounce(onChange, 500), [onChange]);
const onDurationChange = (newValues) => {
const onDurationChange = (newValues: any) => {
setDurationValues({ ...durationValues, ...newValues });
}
const handleBlur = (e) => {
const handleBlur = (e: any) => {
if (filter.type === FilterType.DURATION) {
const { maxDuration, minDuration, key } = filter;
if (maxDuration || minDuration) return;
@ -53,16 +56,22 @@ function FilterValue(props: Props) {
}
}
const getParms = (key) => {
const getParms = (key: any) => {
let params = {};
switch (filter.category) {
case FilterCategory.METADATA:
return { type: FilterKey.METADATA, key: key };
params = { type: FilterKey.METADATA, key: key };
default:
return { type: filter.key };
params = { type: filter.key };
}
if (isRoute(ASSIST_ROUTE, window.location.pathname)) {
params = { ...params, live: true };
}
return params;
}
const renderValueFiled = (value, valueIndex) => {
const renderValueFiled = (value: any, valueIndex: any) => {
const showOrButton = valueIndex === lastIndex && filter.type !== FilterType.NUMBER;
switch(filter.type) {
case FilterType.STRING:
@ -85,7 +94,7 @@ function FilterValue(props: Props) {
filter={filter}
options={filter.options}
onChange={({ value }) => onChange(null, { value }, valueIndex)}
/>
/>
)
case FilterType.ISSUE:
case FilterType.MULTIPLE_DROPDOWN:
@ -174,7 +183,7 @@ function FilterValue(props: Props) {
{ filter.type === FilterType.DURATION ? (
renderValueFiled(filter.value, 0)
) : (
filter.value && filter.value.map((value, valueIndex) => (
filter.value && filter.value.map((value: any, valueIndex: any) => (
<div key={valueIndex}>
{renderValueFiled(value, valueIndex)}
</div>

View file

@ -1,17 +1,17 @@
import React, { useEffect } from 'react';
import { fetchLiveList } from 'Duck/sessions';
import { connect } from 'react-redux';
import { NoContent, Loader, LoadMoreButton, Pagination } from 'UI';
import { List, Map } from 'immutable';
import { NoContent, Loader, Pagination } from 'UI';
import { List } from 'immutable';
import SessionItem from 'Shared/SessionItem';
import withPermissions from 'HOCs/withPermissions'
import { KEYS } from 'Types/filter/customFilter';
import { applyFilter, addAttribute } from 'Duck/filters';
import { FilterCategory, FilterKey } from 'App/types/filter/filterType';
import { FilterKey } from 'App/types/filter/filterType';
import { addFilterByKeyAndValue, updateCurrentPage, updateSort } from 'Duck/liveSearch';
import Select from 'Shared/Select';
import SortOrderButton from 'Shared/SortOrderButton';
import { capitalize, sliceListPerPage } from 'App/utils';
import { capitalize } from 'App/utils';
import LiveSessionReloadButton from 'Shared/LiveSessionReloadButton';
const AUTOREFRESH_INTERVAL = .5 * 60 * 1000
@ -23,7 +23,7 @@ interface Props {
fetchLiveList: () => Promise<void>,
applyFilter: () => void,
filters: any,
addAttribute: (obj) => void,
addAttribute: (obj: any) => void,
addFilterByKeyAndValue: (key: FilterKey, value: string) => void,
updateCurrentPage: (page: number) => void,
currentPage: number,
@ -34,16 +34,13 @@ interface Props {
function LiveSessionList(props: Props) {
const { loading, filters, list, currentPage, metaList = [], sort } = props;
var timeoutId;
const hasUserFilter = filters.map(i => i.key).includes(KEYS.USERID);
var timeoutId: any;
const hasUserFilter = filters.map((i: any) => i.key).includes(KEYS.USERID);
const [sessions, setSessions] = React.useState(list);
const sortOptions = metaList.map(i => ({
const sortOptions = metaList.map((i: any) => ({
text: capitalize(i), label: i
})).toJS();
// const displayedCount = Math.min(currentPage * PER_PAGE, sessions.size);
// const addPage = () => props.updateCurrentPage(props.currentPage + 1)
// useEffect(() => {
// if (filters.size === 0) {
// props.addFilterByKeyAndValue(FilterKey.USERID, '');
@ -53,31 +50,31 @@ function LiveSessionList(props: Props) {
useEffect(() => {
if (metaList.size === 0 || !!sort.field) return;
if ( sortOptions[0]) {
if (sortOptions[0]) {
props.updateSort({ field: sortOptions[0].value });
}
}, [metaList]);
useEffect(() => {
const filteredSessions = filters.size > 0 ? props.list.filter(session => {
let hasValidFilter = true;
filters.forEach(filter => {
if (!hasValidFilter) return;
// useEffect(() => {
// const filteredSessions = filters.size > 0 ? props.list.filter(session => {
// let hasValidFilter = true;
// filters.forEach(filter => {
// if (!hasValidFilter) return;
const _values = filter.value.filter(i => i !== '' && i !== null && i !== undefined).map(i => i.toLowerCase());
if (filter.key === FilterKey.USERID) {
const _userId = session.userId ? session.userId.toLowerCase() : '';
hasValidFilter = _values.length > 0 ? (_values.includes(_userId) && hasValidFilter) || _values.some(i => _userId.includes(i)) : hasValidFilter;
}
if (filter.category === FilterCategory.METADATA) {
const _source = session.metadata[filter.key] ? session.metadata[filter.key].toLowerCase() : '';
hasValidFilter = _values.length > 0 ? (_values.includes(_source) && hasValidFilter) || _values.some(i => _source.includes(i)) : hasValidFilter;
}
})
return hasValidFilter;
}) : props.list;
setSessions(filteredSessions);
}, [filters, list]);
// const _values = filter.value.filter(i => i !== '' && i !== null && i !== undefined).map(i => i.toLowerCase());
// if (filter.key === FilterKey.USERID) {
// const _userId = session.userId ? session.userId.toLowerCase() : '';
// hasValidFilter = _values.length > 0 ? (_values.includes(_userId) && hasValidFilter) || _values.some(i => _userId.includes(i)) : hasValidFilter;
// }
// if (filter.category === FilterCategory.METADATA) {
// const _source = session.metadata[filter.key] ? session.metadata[filter.key].toLowerCase() : '';
// hasValidFilter = _values.length > 0 ? (_values.includes(_source) && hasValidFilter) || _values.some(i => _source.includes(i)) : hasValidFilter;
// }
// })
// return hasValidFilter;
// }) : props.list;
// setSessions(filteredSessions);
// }, [filters, list]);
useEffect(() => {
props.fetchLiveList();
@ -95,7 +92,8 @@ function LiveSessionList(props: Props) {
}
}
const onSortChange = (e, { value }) => {
const onSortChange = ({ value }: any) => {
value = value.value
props.updateSort({ field: value });
}
@ -145,9 +143,7 @@ function LiveSessionList(props: Props) {
show={ !loading && sessions && sessions.size === 0}
>
<Loader loading={ loading }>
{sessions && sliceListPerPage(sessions.sortBy(i => i.metadata[sort.field]).update(list => {
return sort.order === 'desc' ? list.reverse() : list;
}), currentPage - 1).map(session => (
{sessions.map(session => (
<SessionItem
key={ session.sessionId }
session={ session }
@ -162,7 +158,7 @@ function LiveSessionList(props: Props) {
<Pagination
page={currentPage}
totalPages={Math.ceil(sessions.size / PER_PAGE)}
onPageChange={(page) => props.updateCurrentPage(page)}
onPageChange={(page: any) => props.updateCurrentPage(page)}
limit={PER_PAGE}
/>
</div>
@ -173,12 +169,12 @@ function LiveSessionList(props: Props) {
}
export default withPermissions(['ASSIST_LIVE'])(connect(
(state) => ({
list: state.getIn(['sessions', 'liveSessions']),
loading: state.getIn([ 'sessions', 'loading' ]),
(state: any) => ({
list: state.getIn(['liveSearch', 'list']),
loading: state.getIn([ 'liveSearch', 'fetchList', 'loading' ]),
filters: state.getIn([ 'liveSearch', 'instance', 'filters' ]),
currentPage: state.getIn(["liveSearch", "currentPage"]),
metaList: state.getIn(['customFields', 'list']).map(i => i.key),
metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key),
sort: state.getIn(['liveSearch', 'sort']),
}),
{

View file

@ -1,19 +1,19 @@
import React from 'react'
import ReloadButton from '../ReloadButton'
import { connect } from 'react-redux'
import { fetchLiveList } from 'Duck/sessions'
import { fetchSessions } from 'Duck/liveSearch'
interface Props {
loading: boolean
fetchLiveList: typeof fetchLiveList
fetchSessions: typeof fetchSessions
}
function LiveSessionReloadButton(props: Props) {
const { loading } = props
return (
<ReloadButton loading={loading} onClick={() => props.fetchLiveList()} className="cursor-pointer" />
<ReloadButton loading={loading} onClick={() => props.fetchSessions()} className="cursor-pointer" />
)
}
export default connect(state => ({
loading: state.getIn([ 'sessions', 'fetchLiveListRequest', 'loading' ]),
}), { fetchLiveList })(LiveSessionReloadButton)
}), { fetchSessions })(LiveSessionReloadButton)

View file

@ -1,19 +1,20 @@
import React from 'react';
import FilterList from 'Shared/Filters/FilterList';
import { connect } from 'react-redux';
import { edit, addFilter, addFilterByKeyAndValue } from 'Duck/liveSearch';
import FilterSelection from 'Shared/Filters/FilterSelection';
import { IconButton } from 'UI';
import SaveFilterButton from 'Shared/SaveFilterButton';
import { connect } from 'react-redux';
import { Button } from 'UI';
import { edit, addFilter } from 'Duck/liveSearch';
import SaveFunnelButton from '../SaveFunnelButton';
interface Props {
list: any,
appliedFilter: any;
edit: typeof edit;
addFilter: typeof addFilter;
addFilterByKeyAndValue: typeof addFilterByKeyAndValue;
saveRequestPayloads: boolean;
}
function LiveSessionSearch(props: Props) {
const { appliedFilter } = props;
const { appliedFilter, saveRequestPayloads = false } = props;
const hasEvents = appliedFilter.filters.filter(i => i.isEvent).size > 0;
const hasFilters = appliedFilter.filters.filter(i => !i.isEvent).size > 0;
@ -41,10 +42,9 @@ function LiveSessionSearch(props: Props) {
return i !== filterIndex;
});
props.edit({ filters: newFilters, });
// if (newFilters.size === 0) {
// props.addFilterByKeyAndValue(FilterKey.USERID, '');
// }
props.edit({
filters: newFilters,
});
}
const onChangeEventsOrder = (e, { name, value }) => {
@ -53,18 +53,17 @@ function LiveSessionSearch(props: Props) {
});
}
return props.list.size > 0 ? (
return (hasEvents || hasFilters) ? (
<div className="border bg-white rounded mt-4">
{ hasEvents || hasFilters && (
<div className="p-5">
<FilterList
filter={appliedFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
/>
</div>
)}
<div className="p-5">
<FilterList
filter={appliedFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
saveRequestPayloads={saveRequestPayloads}
/>
</div>
<div className="border-t px-5 py-1 flex items-center -mx-2">
<div>
@ -72,15 +71,26 @@ function LiveSessionSearch(props: Props) {
filter={undefined}
onFilterClick={onAddFilter}
>
<IconButton primaryText label="ADD FILTER" icon="plus" />
{/* <IconButton primaryText label="ADD STEP" icon="plus" /> */}
<Button
variant="text-primary"
className="mr-2"
// onClick={() => setshowModal(true)}
icon="plus">
ADD STEP
</Button>
</FilterSelection>
</div>
<div className="ml-auto flex items-center">
{/* <SaveFunnelButton /> */}
{/* <SaveFilterButton /> */}
</div>
</div>
</div>
) : <></>;
}
export default connect(state => ({
saveRequestPayloads: state.getIn(['site', 'active', 'saveRequestPayloads']),
appliedFilter: state.getIn([ 'liveSearch', 'instance' ]),
list: state.getIn(['sessions', 'liveSessions']),
}), { edit, addFilter, addFilterByKeyAndValue })(LiveSessionSearch);
}), { edit, addFilter })(LiveSessionSearch);

View file

@ -2,20 +2,31 @@ import React from 'react';
import SessionSearchField from 'Shared/SessionSearchField';
import SavedSearch from 'Shared/SavedSearch';
import { Button } from 'UI';
import { clearSearch } from 'Duck/search';
// import { clearSearch } from 'Duck/search';
import { connect } from 'react-redux';
import { edit as editFilter, addFilterByKeyAndValue, clearSearch, fetchFilterSearch } from 'Duck/search';
interface Props {
clearSearch: () => void;
appliedFilter: any;
optionsReady: boolean;
editFilter: any,
addFilterByKeyAndValue: any,
fetchFilterSearch: any,
}
const MainSearchBar = (props: Props) => {
const { appliedFilter, optionsReady } = props;
const { appliedFilter } = props;
const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0;
return (
<div className="flex items-center">
<div style={{ width: "60%", marginRight: "10px"}}><SessionSearchField /></div>
<div style={{ width: "60%", marginRight: "10px"}}>
<SessionSearchField
editFilter={props.editFilter}
addFilterByKeyAndValue={props.addFilterByKeyAndValue}
clearSearch={props.clearSearch}
fetchFilterSearch={props.fetchFilterSearch}
/>
</div>
<div className="flex items-center" style={{ width: "40%"}}>
<SavedSearch />
<Button
@ -33,4 +44,9 @@ const MainSearchBar = (props: Props) => {
export default connect(state => ({
appliedFilter: state.getIn(['search', 'instance']),
optionsReady: state.getIn(['customFields', 'optionsReady'])
}), { clearSearch })(MainSearchBar);
}), {
clearSearch,
editFilter,
addFilterByKeyAndValue,
fetchFilterSearch
})(MainSearchBar);

View file

@ -118,7 +118,7 @@ function SessionItem(props: RouteComponentProps<Props>) {
</div>
</div>
</div>
<div style={{ width: "20%", height: "38px" }} className="px-2 flex flex-col justify-between">
<div style={{ width: "20%" }} className="px-2 flex flex-col justify-between">
<div>{formatTimeOrDate(startedAt, timezone) }</div>
<div className="flex items-center color-gray-medium">
{!isAssist && (
@ -133,8 +133,8 @@ function SessionItem(props: RouteComponentProps<Props>) {
<div>{ live ? <Counter startTime={startedAt} /> : formattedDuration }</div>
</div>
</div>
<div style={{ width: "30%", height: "38px" }} className="px-2 flex flex-col justify-between">
<CountryFlag country={ userCountry } className="mr-2" label />
<div style={{ width: "30%" }} className="px-2 flex flex-col justify-between">
<div style={{ height: '21px'}}><CountryFlag country={ userCountry } style={{ paddingTop: '4px' }} label /></div>
<div className="color-gray-medium flex items-center">
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ capitalize(userBrowser) } popupProps={{ inverted: true, size: "tiny" }} />

View file

@ -1,44 +1,39 @@
import React, { useState } from 'react';
import { connect } from 'react-redux';
import stl from './SessionSearchField.module.css';
import { Input } from 'UI';
import FilterModal from 'Shared/Filters/FilterModal';
import { fetchFilterSearch } from 'Duck/search';
import { debounce } from 'App/utils';
import { edit as editFilter, addFilterByKeyAndValue } from 'Duck/search';
import { assist as assistRoute, isRoute } from "App/routes";
const ASSIST_ROUTE = assistRoute();
interface Props {
fetchFilterSearch: (query: any) => void;
editFilter: typeof editFilter;
addFilterByKeyAndValue: (key: string, value: string) => void;
filterList: any;
filterListLive: any;
}
function SessionSearchField(props: Props) {
const debounceFetchFilterSearch = React.useCallback(debounce(props.fetchFilterSearch, 1000), []);
const [showModal, setShowModal] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const onSearchChange = (e, { value }) => {
const onSearchChange = ({ target: { value } }: any) => {
setSearchQuery(value)
debounceFetchFilterSearch({ q: value });
}
const onAddFilter = (filter) => {
const onAddFilter = (filter: any) => {
props.addFilterByKeyAndValue(filter.key, filter.value)
}
return (
<div className="relative">
<Input
// inputProps={ { "data-openreplay-label": "Search", "autocomplete": "off" } }
// className={stl.searchField}
icon="search"
onFocus={ () => setShowModal(true) }
onBlur={ () => setTimeout(setShowModal, 200, false) }
onChange={ onSearchChange }
// icon="search"
// iconPosition="left"
placeholder={ 'Search sessions using any captured event (click, input, page, error...)'}
// fluid
id="search"
type="search"
autoComplete="off"
@ -50,6 +45,7 @@ function SessionSearchField(props: Props) {
searchQuery={searchQuery}
isMainSearch={true}
onFilterClick={onAddFilter}
filters={isRoute(ASSIST_ROUTE, window.location.pathname) ? props.filterListLive : props.filterList }
/>
</div>
)}
@ -57,4 +53,7 @@ function SessionSearchField(props: Props) {
);
}
export default connect(null, { fetchFilterSearch, editFilter, addFilterByKeyAndValue })(SessionSearchField);
export default connect((state: any) => ({
filterList: state.getIn([ 'search', 'filterList' ]),
filterListLive: state.getIn([ 'search', 'filterListLive' ]),
}), { })(SessionSearchField);

View file

@ -12,7 +12,7 @@ const CountryFlag = React.memo(({ country, className, style = {}, label = false
return (
<div className="flex items-center" style={style}>
{knownCountry
? <div className={ cn(`flag flag-${ countryFlag }`, className, stl.default) } />
? <div className={ cn(`mr-1 flag flag-${ countryFlag }`, className, stl.default) } />
: (
<div className="flex items-center w-full">
<Icon name="flag-na" size="22" className="" />

View file

@ -1,20 +0,0 @@
import React from 'react';
import { Icon } from 'UI';
import cn from "classnames";
import stl from './listItem.module.css';
const ListItem = ({icon, label, onClick, onRemove }) => {
return (
<div className={ cn(stl.wrapper, 'flex items-center capitalize') } onClick={ onClick }>
<div className="flex items-center mr-auto">
<Icon name={ icon } color="teal" size="16" />
<span className="ml-3">{ label }</span>
</div>
<div className={ cn(stl.actionWrapper, "p-2")} onClick={onRemove}>
<Icon name="trash" color="teal" size="12" />
</div>
</div>
);
};
export default ListItem;

View file

@ -1,157 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import stl from './savedSearchList.module.css';
import cn from 'classnames';
import { Icon, IconButton, Loader, Button } from 'UI';
import { confirm } from 'UI';
import { withRouter } from 'react-router-dom';
import { addFilterByKeyAndValue } from 'Duck/search';
import {
fetchList as fetchFunnelsList,
remove as deleteSearch,
// clearEvents,
init
} from 'Duck/funnels';
import { setActiveFlow, clearEvents } from 'Duck/filters';
import { setActiveTab } from 'Duck/sessions';
import { funnel as funnelRoute, withSiteId } from 'App/routes';
import Event, { TYPES } from 'Types/filter/event';
import FunnelMenuItem from 'Components/Funnels/FunnelMenuItem';
import FunnelSaveModal from 'Components/Funnels/FunnelSaveModal';
import { blink as setBlink } from 'Duck/funnels';
import { FilterKey } from 'Types/filter/filterType';
const DEFAULT_VISIBLE = 3;
@withRouter
class SavedSearchList extends React.Component {
state = { showMore: false, showSaveModal: false }
setFlow = flow => {
this.props.setActiveTab({ name: 'All', type: 'all' });
this.props.setActiveFlow(flow)
if (flow && flow.type === 'flows') {
this.props.clearEvents()
}
}
renameHandler = funnel => {
this.props.init(funnel);
this.setState({ showSaveModal: true })
}
deleteSearch = async (e, funnel) => {
e.preventDefault();
e.stopPropagation();
if (await confirm({
header: 'Delete Funnel',
confirmButton: 'Delete',
confirmation: `Are you sure you want to permanently delete "${funnel.name}"?`
})) {
this.props.deleteSearch(funnel.funnelId).then(function() {
this.props.fetchFunnelsList();
this.setState({ showSaveModal: false })
}.bind(this));
} else {}
}
createHandler = () => {
const { filters } = this.props;
if (filters.size === 0) {
this.props.addFilterByKeyAndValue(FilterKey.LOCATION, '');
this.props.addFilterByKeyAndValue(FilterKey.LOCATION, '');
this.props.addFilterByKeyAndValue(FilterKey.CLICK, '')
} else {
this.props.setBlink()
}
}
onFlowClick = ({ funnelId }) => {
const { siteId, history } = this.props;
history.push(withSiteId(funnelRoute(funnelId), siteId));
}
render() {
const { funnels, activeFlow, activeTab, loading } = this.props;
const { showMore, showSaveModal } = this.state;
const shouldLimit = funnels.size > DEFAULT_VISIBLE;
return (
<div className={ stl.wrapper }>
<FunnelSaveModal
show={showSaveModal}
closeHandler={() => this.setState({ showSaveModal: false })}
/>
<Loader loading={loading} size="small">
<div className={ cn(stl.header, 'mt-3') }>
<div className={ cn(stl.label, 'flex items-center relative') }>
<span className="mr-2">Funnels</span>
{ funnels.size > 0 && (
<IconButton
tooltip="Create Funnel"
circle
size="small"
icon="plus"
outline
onClick={ this.createHandler }
/>
)}
</div>
</div>
{ funnels.size === 0 &&
<div className="flex flex-col">
<div className="color-gray-medium text-justify font-light mb-2">
Funnels makes it easy to uncover the most significant issues that impacted conversions.
</div>
<IconButton className="-ml-2" icon="plus" label="Create Funnel" primaryText onClick={ this.createHandler } />
</div>
}
{ funnels.size > 0 &&
<React.Fragment>
{ funnels.take(showMore ? funnels.size : DEFAULT_VISIBLE).map(filter => (
<div key={filter.key}>
<FunnelMenuItem
title={filter.name}
isPublic={filter.isPublic}
iconName="filter"
active={activeFlow && activeFlow.funnelId === filter.funnelId && activeTab.type !== 'flows'}
onClick={ () => this.onFlowClick(filter)}
deleteHandler={ (e) => this.deleteSearch(e, filter) }
renameHandler={() => this.renameHandler(filter)}
/>
</div>
))}
{ shouldLimit &&
<div
onClick={() => this.setState({ showMore: !showMore})}
className={cn(stl.showMore, 'cursor-pointer py-2 flex items-center')}
>
{/* <Icon name={showMore ? 'arrow-up' : 'arrow-down'} size="16"/> */}
<span className="ml-4 color-teal text-sm">{ showMore ? 'SHOW LESS' : 'SHOW MORE' }</span>
</div>
}
</React.Fragment>
}
</Loader>
</div>
);
}
}
export default connect(state => ({
funnels: state.getIn([ 'funnels', 'list' ]),
loading: state.getIn(['funnels', 'fetchListRequest', 'loading']),
activeFlow: state.getIn([ 'filters', 'activeFlow' ]),
activeTab: state.getIn([ 'sessions', 'activeTab' ]),
siteId: state.getIn([ 'site', 'siteId' ]),
events: state.getIn([ 'filters', 'appliedFilter', 'events' ]),
filters: state.getIn([ 'search', 'instance', 'filters' ]),
}), {
deleteSearch, setActiveTab,
setActiveFlow, clearEvents,
addFilterByKeyAndValue,
init,
fetchFunnelsList,
setBlink
})(SavedSearchList)

View file

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

View file

@ -1,24 +0,0 @@
.wrapper {
padding: 4px 5px;
cursor: pointer;
border: solid thin transparent;
border-radius: 3px;
margin-left: -5px;
&.active,
&:hover {
background-color: $active-blue;
border-color: $active-blue-border;
& .actionWrapper {
opacity: 1;
}
}
& span {
color: $teal
}
& .actionWrapper {
opacity: 0;
}
}

View file

@ -1,20 +0,0 @@
.header {
margin-bottom: 15px;
& .label {
text-transform: uppercase;
color: gray;
letter-spacing: 0.2em;
}
}
.showMore {
&:hover {
color: $teal;
& svg {
fill: $teal;
}
& .actions {
opacity: 1;
}
}
}

View file

@ -30,7 +30,6 @@ export { default as JSONTree } from './JSONTree';
export { default as Tooltip } from './Tooltip';
export { default as CountryFlag } from './CountryFlag';
export { default as RandomElement } from './RandomElement';
export { default as SavedSearchList } from './SavedSearchList';
export { default as SplitButton } from './SplitButton';
export { default as confirm } from './Confirmation';
export { default as SideMenuitem } from './SideMenuitem';

View file

@ -1,21 +1,21 @@
export default [
{ value: 'performance.dom_content_loaded.average', text: 'performance.dom_content_loaded.average', unit: 'ms' },
{ value: 'performance.first_meaningful_paint.average', text: 'performance.first_meaningful_paint.average', unit: 'ms' },
{ value: 'performance.page_load_time.average', text: 'performance.page_load_time.average', unit: 'ms' },
{ value: 'performance.dom_build_time.average', text: 'performance.dom_build_time.average', unit: 'ms' },
{ value: 'performance.speed_index.average', text: 'performance.speed_index.average', unit: 'ms' },
{ value: 'performance.page_response_time.average', text: 'performance.page_response_time.average', unit: 'ms' },
{ value: 'performance.ttfb.average', text: 'performance.ttfb.average', unit: 'ms' },
{ value: 'performance.time_to_render.average', text: 'performance.time_to_render.average', unit: 'ms' },
{ value: 'performance.image_load_time.average', text: 'performance.image_load_time.average', unit: 'ms' },
{ value: 'performance.request_load_time.average', text: 'performance.request_load_time.average', unit: 'ms' },
{ value: 'resources.load_time.average', text: 'resources.load_time.average', unit: 'ms' },
{ value: 'resources.missing.count', text: 'resources.missing.count', unit: '' },
{ value: 'errors.4xx_5xx.count', text: 'errors.4xx_5xx.count', unit: '' },
{ value: 'errors.4xx.count', text: 'errors.4xx.count', unit: '' },
{ value: 'errors.5xx.count', text: 'errors.5xx.count', unit: '' },
{ value: 'errors.javascript.impacted_sessions.count', text: 'errors.javascript.impacted_sessions.count', unit: '' },
{ value: 'performance.crashes.count', text: 'performance.crashes.count', unit: '' },
{ value: 'errors.javascript.count', text: 'errors.javascript.count', unit: '' },
{ value: 'errors.backend.count', text: 'errors.backend.count', unit: '' },
{ value: 'performance.dom_content_loaded.average', label: 'performance.dom_content_loaded.average', unit: 'ms' },
{ value: 'performance.first_meaningful_paint.average', label: 'performance.first_meaningful_paint.average', unit: 'ms' },
{ value: 'performance.page_load_time.average', label: 'performance.page_load_time.average', unit: 'ms' },
{ value: 'performance.dom_build_time.average', label: 'performance.dom_build_time.average', unit: 'ms' },
{ value: 'performance.speed_index.average', label: 'performance.speed_index.average', unit: 'ms' },
{ value: 'performance.page_response_time.average', label: 'performance.page_response_time.average', unit: 'ms' },
{ value: 'performance.ttfb.average', label: 'performance.ttfb.average', unit: 'ms' },
{ value: 'performance.time_to_render.average', label: 'performance.time_to_render.average', unit: 'ms' },
{ value: 'performance.image_load_time.average', label: 'performance.image_load_time.average', unit: 'ms' },
{ value: 'performance.request_load_time.average', label: 'performance.request_load_time.average', unit: 'ms' },
{ value: 'resources.load_time.average', label: 'resources.load_time.average', unit: 'ms' },
{ value: 'resources.missing.count', label: 'resources.missing.count', unit: '' },
{ value: 'errors.4xx_5xx.count', label: 'errors.4xx_5xx.count', unit: '' },
{ value: 'errors.4xx.count', label: 'errors.4xx.count', unit: '' },
{ value: 'errors.5xx.count', label: 'errors.5xx.count', unit: '' },
{ value: 'errors.javascript.impacted_sessions.count', label: 'errors.javascript.impacted_sessions.count', unit: '' },
{ value: 'performance.crashes.count', label: 'performance.crashes.count', unit: '' },
{ value: 'errors.javascript.count', label: 'errors.javascript.count', unit: '' },
{ value: 'errors.backend.count', label: 'errors.backend.count', unit: '' },
];

View file

@ -1,34 +1,34 @@
import { FilterKey, IssueType } from 'Types/filter/filterType';
// TODO remove text property from options
export const options = [
{ key: 'on', text: 'on', label: 'on', value: 'on' },
{ key: 'notOn', text: 'not on', label: 'not on', value: 'notOn' },
{ key: 'onAny', text: 'on any', label: 'on any', value: 'onAny' },
{ key: 'is', text: 'is', label: 'is', value: 'is' },
{ key: 'isAny', text: 'is any', label: 'is any', value: 'isAny' },
{ key: 'inAnyPage', text: 'in any page', label: 'in any page', value: 'isAny' },
{ key: 'isNot', text: 'is not', label: 'is not', value: 'isNot' },
{ key: 'startsWith', text: 'starts with', label: 'starts with', value: 'startsWith' },
{ key: 'endsWith', text: 'ends with', label: 'ends with', value: 'endsWith' },
{ key: 'contains', text: 'contains', label: 'contains', value: 'contains' },
{ key: 'notContains', text: 'not contains', label: 'not contains', value: 'notContains' },
{ key: 'hasAnyValue', text: 'has any value', label: 'has any value', value: 'hasAnyValue' },
{ key: 'hasNoValue', text: 'has no value', label: 'has no value', value: 'hasNoValue' },
{ key: 'isSignedUp', text: 'is signed up', label: 'is signed up', value: 'isSignedUp' },
{ key: 'notSignedUp', text: 'not signed up', label: 'not signed up', value: 'notSignedUp' },
{ key: 'before', text: 'before', label: 'before', value: 'before' },
{ key: 'after', text: 'after', label: 'after', value: 'after' },
{ key: 'inRage', text: 'in rage', label: 'in rage', value: 'inRage' },
{ key: 'notInRage', text: 'not in rage', label: 'not in rage', value: 'notInRage' },
{ key: 'withinLast', text: 'within last', label: 'within last', value: 'withinLast' },
{ key: 'notWithinLast', text: 'not within last', label: 'not within last', value: 'notWithinLast' },
{ key: 'greaterThan', text: 'greater than', label: 'greater than', value: 'greaterThan' },
{ key: 'lessThan', text: 'less than', label: 'less than', value: 'lessThan' },
{ key: 'equal', text: 'equal', label: 'equal', value: 'equal' },
{ key: 'not equal', text: 'not equal', label: 'not equal', value: 'not equal' },
{ key: 'onSelector', text: 'on selector', label: 'on selector', value: 'onSelector' },
{ key: 'onText', text: 'on text', label: 'on text', value: 'onText' },
{ key: 'onComponent', text: 'on component', label: 'on component', value: 'onComponent' },
{ key: 'on', label: 'on', value: 'on' },
{ key: 'notOn', label: 'not on', value: 'notOn' },
{ key: 'onAny', label: 'on any', value: 'onAny' },
{ key: 'is', label: 'is', value: 'is' },
{ key: 'isAny', label: 'is any', value: 'isAny' },
{ key: 'inAnyPage', label: 'in any page', value: 'isAny' },
{ key: 'isNot', label: 'is not', value: 'isNot' },
{ key: 'startsWith', label: 'starts with', value: 'startsWith' },
{ key: 'endsWith', label: 'ends with', value: 'endsWith' },
{ key: 'contains', label: 'contains', value: 'contains' },
{ key: 'notContains', label: 'not contains', value: 'notContains' },
{ key: 'hasAnyValue', label: 'has any value', value: 'hasAnyValue' },
{ key: 'hasNoValue', label: 'has no value', value: 'hasNoValue' },
{ key: 'isSignedUp', label: 'is signed up', value: 'isSignedUp' },
{ key: 'notSignedUp', label: 'not signed up', value: 'notSignedUp' },
{ key: 'before', label: 'before', value: 'before' },
{ key: 'after', label: 'after', value: 'after' },
{ key: 'inRage', label: 'in rage', value: 'inRage' },
{ key: 'notInRage', label: 'not in rage', value: 'notInRage' },
{ key: 'withinLast', label: 'within last', value: 'withinLast' },
{ key: 'notWithinLast', label: 'not within last', value: 'notWithinLast' },
{ key: 'greaterThan', label: 'greater than', value: 'greaterThan' },
{ key: 'lessThan', label: 'less than', value: 'lessThan' },
{ key: 'equal', label: 'equal', value: 'equal' },
{ key: 'not equal', label: 'not equal', value: 'not equal' },
{ key: 'onSelector', label: 'on selector', value: 'onSelector' },
{ key: 'onText', label: 'on text', value: 'onText' },
{ key: 'onComponent', label: 'on component', value: 'onComponent' },
];
const filterKeys = ['is', 'isNot'];
@ -47,21 +47,21 @@ export const stringOperators = options.filter(({key}) => stringFilterKeys.includ
export const stringOperatorsPerformance = options.filter(({key}) => stringFilterKeysPerformance.includes(key));
export const targetOperators = options.filter(({key}) => targetFilterKeys.includes(key));
export const booleanOperators = [
{ key: 'true', text: 'true', label: 'true', value: 'true' },
{ key: 'false', text: 'false', label: 'false', value: 'false' },
{ key: 'true', label: 'true', value: 'true' },
{ key: 'false', label: 'false', value: 'false' },
]
export const customOperators = [
{ key: '=', text: '=', label: '=', value: '=' },
{ key: '<', text: '<', label: '<', value: '<' },
{ key: '>', text: '>', label: '>', value: '>' },
{ key: '<=', text: '<=', label: '<=', value: '<=' },
{ key: '>=', text: '>=', label: '>=', value: '>=' },
{ key: '=', label: '=', value: '=' },
{ key: '<', label: '<', value: '<' },
{ key: '>', label: '>', value: '>' },
{ key: '<=', label: '<=', value: '<=' },
{ key: '>=', label: '>=', value: '>=' },
]
export const metricTypes = [
{ text: 'Timeseries', label: 'Timeseries', value: 'timeseries' },
{ text: 'Table', label: 'Table', value: 'table' },
{ label: 'Timeseries', value: 'timeseries' },
{ label: 'Table', value: 'table' },
{ label: 'Funnel', value: 'funnel' },
// { label: 'Errors', value: 'errors' },
// { label: 'Sessions', value: 'sessions' },
@ -77,42 +77,42 @@ export const tableColumnName = {
}
export const metricOf = [
{ text: 'Session Count', label: 'Session Count', value: 'sessionCount', type: 'timeseries' },
{ text: 'Users', label: 'Users', value: FilterKey.USERID, type: 'table' },
{ text: 'Sessions', label: 'Sessions', value: FilterKey.SESSIONS, type: 'table' },
{ text: 'JS Errors', label: 'JS Errors', value: FilterKey.ERRORS, type: 'table' },
{ text: 'Issues', label: 'Issues', value: FilterKey.ISSUE, type: 'table' },
{ text: 'Browsers', label: 'Browsers', value: FilterKey.USER_BROWSER, type: 'table' },
{ text: 'Devices', label: 'Devices', value: FilterKey.USER_DEVICE, type: 'table' },
{ text: 'Countries', label: 'Countries', value: FilterKey.USER_COUNTRY, type: 'table' },
{ text: 'URLs', label: 'URLs', value: FilterKey.LOCATION, type: 'table' },
{ label: 'Session Count', value: 'sessionCount', type: 'timeseries' },
{ label: 'Users', value: FilterKey.USERID, type: 'table' },
{ label: 'Sessions', value: FilterKey.SESSIONS, type: 'table' },
{ label: 'JS Errors', value: FilterKey.ERRORS, type: 'table' },
{ label: 'Issues', value: FilterKey.ISSUE, type: 'table' },
{ label: 'Browsers', value: FilterKey.USER_BROWSER, type: 'table' },
{ label: 'Devices', value: FilterKey.USER_DEVICE, type: 'table' },
{ label: 'Countries', value: FilterKey.USER_COUNTRY, type: 'table' },
{ label: 'URLs', value: FilterKey.LOCATION, type: 'table' },
]
export const methodOptions = [
{ text: 'GET', label: 'GET', value: 'GET' },
{ text: 'POST', label: 'POST', value: 'POST' },
{ text: 'PUT', label: 'PUT', value: 'PUT' },
{ text: 'DELETE', label: 'DELETE', value: 'DELETE' },
{ text: 'PATCH', label: 'PATCH', value: 'PATCH' },
{ text: 'HEAD', label: 'HEAD', value: 'HEAD' },
{ text: 'OPTIONS', label: 'OPTIONS', value: 'OPTIONS' },
{ text: 'TRACE', label: 'TRACE', value: 'TRACE' },
{ text: 'CONNECT', label: 'CONNECT', value: 'CONNECT' },
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
{ label: 'PATCH', value: 'PATCH' },
{ label: 'HEAD', value: 'HEAD' },
{ label: 'OPTIONS', value: 'OPTIONS' },
{ label: 'TRACE', value: 'TRACE' },
{ label: 'CONNECT', value: 'CONNECT' },
]
export const issueOptions = [
{ text: 'Click Rage', label: 'Click Rage', value: IssueType.CLICK_RAGE },
{ text: 'Dead Click', label: 'Dead Click', value: IssueType.DEAD_CLICK },
{ text: 'Excessive Scrolling', label: 'Excessive Scrolling', value: IssueType.EXCESSIVE_SCROLLING },
{ text: 'Bad Request', label: 'Bad Request', value: IssueType.BAD_REQUEST },
{ text: 'Missing Resource', label: 'Missing Resource', value: IssueType.MISSING_RESOURCE },
{ text: 'Memory', label: 'Memory', value: IssueType.MEMORY },
{ text: 'CPU', label: 'CPU', value: IssueType.CPU },
{ text: 'Slow Resource', label: 'Slow Resource', value: IssueType.SLOW_RESOURCE },
{ text: 'Slow Page Load', label: 'Slow Page Load', value: IssueType.SLOW_PAGE_LOAD },
{ text: 'Crash', label: 'Crash', value: IssueType.CRASH },
{ text: 'Custom', label: 'Custom', value: IssueType.CUSTOM },
{ text: 'Error', label: 'Error', value: IssueType.JS_EXCEPTION },
{ label: 'Click Rage', value: IssueType.CLICK_RAGE },
{ label: 'Dead Click', value: IssueType.DEAD_CLICK },
{ label: 'Excessive Scrolling', value: IssueType.EXCESSIVE_SCROLLING },
{ label: 'Bad Request', value: IssueType.BAD_REQUEST },
{ label: 'Missing Resource', value: IssueType.MISSING_RESOURCE },
{ label: 'Memory', value: IssueType.MEMORY },
{ label: 'CPU', value: IssueType.CPU },
{ label: 'Slow Resource', value: IssueType.SLOW_RESOURCE },
{ label: 'Slow Page Load', value: IssueType.SLOW_PAGE_LOAD },
{ label: 'Crash', value: IssueType.CRASH },
{ label: 'Custom', value: IssueType.CUSTOM },
{ label: 'Error', value: IssueType.JS_EXCEPTION },
]
export default {

View file

@ -1,5 +1,5 @@
export default [
{ value: 'desktop', text: 'Desktop' },
{ value: 'mobile', text: 'Mobile' },
{ value: 'tablet', text: 'Tablet' },
{ value: 'desktop', label: 'Desktop' },
{ value: 'mobile', label: 'Mobile' },
{ value: 'tablet', label: 'Tablet' },
]

View file

@ -1,49 +1,22 @@
export const MINUTES = [
{ value: 5, text: '5 Minutes' },
{ value: 15, text: '15 Minutes' },
{ value: 30, text: '30 Minutes' },
{ value: 60, text: '60 Minutes' },
{ value: 5, label: '5 Minutes' },
{ value: 15, label: '15 Minutes' },
{ value: 30, label: '30 Minutes' },
{ value: 60, label: '60 Minutes' },
];
export const HOURS = [ ...Array(24).keys() ].map(i => ({ value: i, text: `${ i > 9 ? '' : '0' }${ i }:00` }));
export const HOURS = [ ...Array(24).keys() ].map(i => ({ value: i, label: `${ i > 9 ? '' : '0' }${ i }:00` }));
export const DAYS = [
{
value: -2,
text: 'Every',
},
{
value: -1,
text: 'Everyday',
},
{
value: 6,
text: 'Sunday',
},
{
value: 0,
text: 'Monday',
},
{
value: 1,
text: 'Tuesday',
},
{
value: 2,
text: 'Wednesday',
},
{
value: 3,
text: 'Thursday',
},
{
value: 4,
text: 'Friday',
},
{
value: 5,
text: 'Saturday',
},
{ value: -2, label: 'Every', },
{ value: -1, label: 'Everyday', },
{ value: 6, label: 'Sunday', },
{ value: 0, label: 'Monday', },
{ value: 1, label: 'Tuesday', },
{ value: 2, label: 'Wednesday', },
{ value: 3, label: 'Thursday', },
{ value: 4, label: 'Friday', },
{ value: 5, label: 'Saturday', },
];
export const EMAIL = 'email';
@ -51,16 +24,7 @@ export const SLACK = 'slack';
export const WEBHOOK = 'webhook';
export const CHANNEL = [
{
value: EMAIL,
text: 'Email'
},
{
value: SLACK,
text: 'Slack'
},
{
value: WEBHOOK,
text: 'Webhook'
}
{ value: EMAIL, label: 'Email' },
{ value: SLACK, label: 'Slack' },
{ value: WEBHOOK, label: 'Webhook' },
]

View file

@ -1,21 +1,24 @@
import { List, Map } from 'immutable';
import { fetchType, editType } from './funcTools/crud';
import { fetchListType, fetchType, editType } from './funcTools/crud';
import { createRequestReducer } from './funcTools/request';
import { mergeReducers } from './funcTools/tools';
import { mergeReducers, success } from './funcTools/tools';
import Filter from 'Types/filter';
import { fetchList as fetchSessionList } from './sessions';
import { liveFiltersMap } from 'Types/filter/newFilter';
// import { fetchList as fetchSessionList } from './sessions';
import { liveFiltersMap, filtersMap } from 'Types/filter/newFilter';
import { filterMap, checkFilterValue, hasFilterApplied } from './search';
import Session from 'Types/session';
const name = "liveSearch";
const idKey = "searchId";
const FETCH_FILTER_SEARCH = fetchListType(`${name}/FILTER_SEARCH`);
const FETCH = fetchType(name);
const EDIT = editType(name);
const CLEAR_SEARCH = `${name}/CLEAR_SEARCH`;
const APPLY = `${name}/APPLY`;
const UPDATE_CURRENT_PAGE = `${name}/UPDATE_CURRENT_PAGE`;
const UPDATE_SORT = `${name}/UPDATE_SORT`;
const FETCH_SESSION_LIST = fetchListType(`${name}/FETCH_SESSION_LIST`);
const initialState = Map({
list: List(),
@ -36,6 +39,23 @@ function reducer(state = initialState, action = {}) {
return state.set('currentPage', action.page);
case UPDATE_SORT:
return state.mergeIn(['sort'], action.sort);
case FETCH_SESSION_LIST:
const { sessions, total } = action.data;
const list = List(sessions).map(Session);
return state
.set('list', list)
.set('total', total);
case success(FETCH_FILTER_SEARCH):
const groupedList = action.data.reduce((acc, item) => {
const { projectId, type, value } = item;
const key = type;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push({ projectId, value });
return acc;
}, {});
return state.set('filterSearchList', groupedList);
}
return state;
}
@ -44,21 +64,32 @@ export default mergeReducers(
reducer,
createRequestReducer({
fetch: FETCH,
fetchList: FETCH_SESSION_LIST,
}),
);
const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getState) => {
dispatch(actionCreator(...args));
const filter = getState().getIn([ 'search', 'instance']).toData();
const filter = getState().getIn([ 'liveSearch', 'instance']).toData();
filter.filters = filter.filters.map(filterMap);
filter.limit = 10;
filter.page = getState().getIn([ 'liveSearch', 'currentPage']);
return dispatch(fetchSessionList(filter));
};
export const edit = (instance) => ({
type: EDIT,
instance,
});
export const fetchSessionList = (filter) => {
return {
types: FETCH_SESSION_LIST.array,
call: client => client.post('/assist/sessions', filter),
}
}
export const edit = reduceThenFetchResource((instance) => ({
type: EDIT,
instance,
}));
export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({
type: APPLY,
@ -67,7 +98,7 @@ export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({
}));
export const fetchSessions = (filter) => (dispatch, getState) => {
const _filter = filter ? filter : getState().getIn([ 'search', 'instance']);
const _filter = filter ? filter : getState().getIn([ 'liveSearch', 'instance']);
return dispatch(applyFilter(_filter));
};
@ -93,9 +124,12 @@ export const addFilter = (filter) => (dispatch, getState) => {
}
}
export const addFilterByKeyAndValue = (key, value) => (dispatch, getState) => {
let defaultFilter = liveFiltersMap[key];
export const addFilterByKeyAndValue = (key, value, operator = undefined) => (dispatch, getState) => {
let defaultFilter = filtersMap[key];
defaultFilter.value = value;
if (operator) {
defaultFilter.operator = operator;
}
dispatch(addFilter(defaultFilter));
}
@ -111,4 +145,13 @@ export function updateSort(sort) {
type: UPDATE_SORT,
sort,
};
}
export function fetchFilterSearch(params) {
params.live = true
return {
types: FETCH_FILTER_SEARCH.array,
call: client => client.get('/events/search', params),
params,
};
}

View file

@ -47,7 +47,7 @@ const updateInstance = (state, instance) => state.getIn([ "savedSearch", savedSe
const initialState = Map({
filterList: generateFilterOptions(filtersMap),
filterListLive: generateLiveFilterOptions(liveFiltersMap),
filterListLive: generateFilterOptions(liveFiltersMap),
list: List(),
alertMetricId: null,
instance: new Filter({ filters: [] }),
@ -63,7 +63,7 @@ function reducer(state = initialState, action = {}) {
switch (action.type) {
case REFRESH_FILTER_OPTIONS:
return state.set('filterList', generateFilterOptions(filtersMap))
.set('filterListLive', generateLiveFilterOptions(liveFiltersMap));
.set('filterListLive', generateFilterOptions(liveFiltersMap));
case EDIT:
return state.mergeIn(['instance'], action.instance).set('currentPage', 1);
case APPLY:
@ -171,7 +171,6 @@ export const reduceThenFetchResource = actionCreator => (...args) => (dispatch,
}
}
return isRoute(ERRORS_ROUTE, window.location.pathname)
? dispatch(fetchErrorsList(filter))
: dispatch(fetchSessionList(filter));

View file

@ -1,7 +1,7 @@
import { List, Map } from 'immutable';
import Session from 'Types/session';
import ErrorStack from 'Types/session/errorStack';
import Watchdog, { getSessionWatchdogTypes } from 'Types/watchdog';
import Watchdog from 'Types/watchdog';
import { clean as cleanParams } from 'App/api_client';
import withRequestState, { RequestTypes } from './requestStateCreator';
import { getRE } from 'App/utils';
@ -83,55 +83,11 @@ const reducer = (state = initialState, action = {}) => {
const { sessions, total } = action.data;
const list = List(sessions).map(Session);
const { params } = action;
const eventProperties = {
eventCount: 0,
eventTypes: [],
dateFilter: params.rangeValue,
filterKeys: Object.keys(params)
.filter(key => ![ 'custom', 'startDate', 'endDate', 'strict', 'key', 'events', 'rangeValue' ].includes(key)),
returnedCount: list.size,
totalSearchCount: total,
};
if (Array.isArray(params.events)) {
eventProperties.eventCount = params.events.length;
params.events.forEach(({ type }) => {
if (!eventProperties.eventTypes.includes(type)) {
eventProperties.eventTypes.push(type);
}
})
}
const keyMap = {}
list.forEach(s => {
s.issueTypes.forEach(k => {
if(keyMap[k])
keyMap[k] += 1
else
keyMap[k] = 1;
})
})
const wdTypeCount = {}
try{
list.forEach(s => {
getSessionWatchdogTypes(s).forEach(wdtp => {
wdTypeCount[wdtp] = wdTypeCount[wdtp] ? wdTypeCount[wdtp] + 1 : 1;
})
})
} catch(e) {
}
const sessionIds = list.map(({ sessionId }) => sessionId ).toJS();
return state
.set('list', list)
.set('sessionIds', sessionIds)
.set('sessionIds', list.map(({ sessionId }) => sessionId ).toJS())
.set('favoriteList', list.filter(({ favorite }) => favorite))
.set('total', total)
.set('keyMap', keyMap)
.set('wdTypeCount', wdTypeCount);
.set('total', total);
case SET_AUTOPLAY_VALUES: {
const sessionIds = state.get('sessionIds')
const currentSessionId = state.get('current').sessionId

View file

@ -70,7 +70,7 @@ export default class Filter implements IFilter {
this.filters.splice(index, 1)
}
fromJson(json) {
fromJson(json: any) {
this.name = json.name
this.filters = json.filters.map(i => new FilterItem().fromJson(i))
this.eventsOrder = json.eventsOrder

View file

@ -133,7 +133,7 @@ export default class MessageDistributor extends StatedScreen {
const r = new MFileReader(new Uint8Array(), this.sessionStart)
const msgs: Array<Message> = []
loadFiles([this.session.mobsUrl],
loadFiles(this.session.mobsUrl,
b => {
r.append(b)
let next: ReturnType<MFileReader['next']>

View file

@ -3,67 +3,78 @@ import { FilterType, FilterKey, FilterCategory } from './filterType'
import filterOptions, { countries, platformOptions } from 'App/constants';
import { capitalize } from 'App/utils';
const countryOptions = Object.keys(countries).map(i => ({ text: countries[i], value: i }));
const countryOptions = Object.keys(countries).map(i => ({ label: countries[i], value: i }));
const containsFilters = [{ key: 'contains', label: 'contains', text: 'contains', value: 'contains' }]
export const metaFilter = { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata' };
export const filtersMap = {
// EVENTS
[FilterKey.CLICK]: { key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true },
[FilterKey.INPUT]: { key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true },
[FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Path', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true },
[FilterKey.CUSTOM]: { key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true },
// [FilterKey.REQUEST]: { key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true },
[FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, operator: 'is', label: 'Network Request', filters: [
{ key: FilterKey.FETCH_URL, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with URL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_STATUS_CODE, type: FilterType.NUMBER_MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with status code', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', options: filterOptions.methodOptions },
{ key: FilterKey.FETCH_DURATION, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'with duration (ms)', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
], icon: 'filters/fetch', isEvent: true },
[FilterKey.GRAPHQL]: { key: FilterKey.GRAPHQL, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, label: 'GraphQL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/graphql', isEvent: true, filters: [
{ key: FilterKey.GRAPHQL_NAME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with name', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.GRAPHQL_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', options: filterOptions.methodOptions },
{ key: FilterKey.GRAPHQL_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.GRAPHQL_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
]},
[FilterKey.STATEACTION]: { key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'StateAction', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true },
[FilterKey.ERROR]: { key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true },
// [FilterKey.METADATA]: { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
export const filters = [
{ key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true },
{ key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true },
{ key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Path', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true },
{ key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true },
{ key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true },
{ key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, operator: 'is', label: 'Network Request', filters: [
{ key: FilterKey.FETCH_URL, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with URL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_STATUS_CODE, type: FilterType.NUMBER_MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with status code', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', options: filterOptions.methodOptions },
{ key: FilterKey.FETCH_DURATION, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'with duration (ms)', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
], icon: 'filters/fetch', isEvent: true },
{ key: FilterKey.GRAPHQL, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, label: 'GraphQL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/graphql', isEvent: true, filters: [
{ key: FilterKey.GRAPHQL_NAME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with name', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.GRAPHQL_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', options: filterOptions.methodOptions },
{ key: FilterKey.GRAPHQL_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.GRAPHQL_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
]},
{ key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'StateAction', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true },
{ key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true },
{ key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
// FILTERS
{ key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' },
{ key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' },
{ key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' },
{ key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/platform', options: platformOptions },
{ key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'Version ID', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'collection' },
{ key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/arrow-return-right' },
{ key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is']), icon: 'filters/duration' },
{ key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.USER, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
{ key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' },
{ key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', operator: 'is', operatorOptions: filterOptions.stringOperators.concat([{ label: 'is undefined', value: 'isUndefined'}]), icon: 'filters/userid' },
{ key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
// PERFORMANCE
{ key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.FETCH_FAILED, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Failed Request', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, icon: 'filters/fetch-failed', isEvent: true },
{ key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
];
// FILTERS
[FilterKey.USER_OS]: { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' },
[FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' },
[FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' },
[FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/platform', options: platformOptions },
[FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'Version ID', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'collection' },
[FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/arrow-return-right' },
[FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is']), icon: 'filters/duration' },
[FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.USER, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
// [FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' },
[FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', operator: 'is', operatorOptions: filterOptions.stringOperators.concat([{ text: 'is undefined', value: 'isUndefined'}]), icon: 'filters/userid' },
[FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
export const filtersMap = filters.reduce((acc, filter) => {
acc[filter.key] = filter;
return acc;
}, {});
// PERFORMANCE
[FilterKey.DOM_COMPLETE]: { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
[FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
[FilterKey.TTFB]: { key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
[FilterKey.AVG_CPU_LOAD]: { key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
[FilterKey.AVG_MEMORY_USAGE]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
[FilterKey.FETCH_FAILED]: { key: FilterKey.FETCH_FAILED, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Failed Request', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, icon: 'filters/fetch-failed', isEvent: true },
[FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
}
export const liveFiltersMap = filters.reduce((acc, filter) => {
if (
filter.category !== FilterCategory.INTERACTIONS &&
filter.category !== FilterCategory.JAVASCRIPT &&
filter.category !== FilterCategory.PERFORMANCE
) {
acc[filter.key] = filter;
}
return acc
}, {});
export const filterLabelMap = Object.keys(filtersMap).reduce((acc, key) => {
acc[key] = filtersMap[key].label
export const filterLabelMap = filters.reduce((acc, filter) => {
acc[filter.key] = filter.label
return acc
}, {})
export const liveFiltersMap = {
[FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.STRING, category: FilterCategory.USER, label: 'User Id', operator: 'contains', operatorOptions: containsFilters, icon: 'filters/userid', isLive: true },
}
/**
* Add a new filter to the filter list
* @param {*} category