feat(ui) - assist filters wip

This commit is contained in:
Shekar Siri 2022-06-15 14:14:48 +02:00
parent af7f751b42
commit e5963fbeef
26 changed files with 190 additions and 382 deletions

View file

@ -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>

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

@ -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>

View file

@ -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),

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,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);

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,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

@ -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']>