feat(ui) - assist filters wip
This commit is contained in:
parent
af7f751b42
commit
e5963fbeef
26 changed files with 190 additions and 382 deletions
|
|
@ -4,13 +4,20 @@ 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'}}>
|
||||
|
||||
{/* <MainSearchBar /> */}
|
||||
<AssistSearchField />
|
||||
<LiveSessionSearch />
|
||||
{/* <SessionSearch /> */}
|
||||
<div className="my-4" />
|
||||
<LiveSessionList />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './AssistSearchField'
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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' ]),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ 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} /> }
|
||||
{/* { isRoute(ASSIST_ROUTE, window.location.pathname) ? <LiveFilterModal onFilterClick={onFilterClick} /> : <FilterModal onFilterClick={onFilterClick} /> } */}
|
||||
<FilterModal onFilterClick={onFilterClick} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -174,8 +174,8 @@ function LiveSessionList(props: Props) {
|
|||
|
||||
export default withPermissions(['ASSIST_LIVE'])(connect(
|
||||
(state) => ({
|
||||
list: state.getIn(['sessions', 'liveSessions']),
|
||||
loading: state.getIn([ 'sessions', 'loading' ]),
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" }} />
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
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 { fetchFilterSearch } from 'Duck/search';
|
||||
import { debounce } from 'App/utils';
|
||||
import { edit as editFilter, addFilterByKeyAndValue } from 'Duck/search';
|
||||
|
||||
interface Props {
|
||||
fetchFilterSearch: (query: any) => void;
|
||||
editFilter: typeof editFilter;
|
||||
// editFilter: typeof editFilter;
|
||||
addFilterByKeyAndValue: (key: string, value: string) => void;
|
||||
}
|
||||
function SessionSearchField(props: Props) {
|
||||
|
|
@ -17,28 +16,23 @@ function SessionSearchField(props: Props) {
|
|||
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"
|
||||
|
|
@ -57,4 +51,4 @@ function SessionSearchField(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default connect(null, { fetchFilterSearch, editFilter, addFilterByKeyAndValue })(SessionSearchField);
|
||||
export default connect(null, { })(SessionSearchField);
|
||||
|
|
@ -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="" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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)
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SavedSearchList'
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -171,7 +171,6 @@ export const reduceThenFetchResource = actionCreator => (...args) => (dispatch,
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return isRoute(ERRORS_ROUTE, window.location.pathname)
|
||||
? dispatch(fetchErrorsList(filter))
|
||||
: dispatch(fetchSessionList(filter));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue