change(ui): duck/search wip

This commit is contained in:
Shekar Siri 2024-09-19 18:29:43 +05:30
parent b9590f702e
commit 64a3eb7e89
33 changed files with 1104 additions and 612 deletions

View file

@ -4,9 +4,8 @@ import { ConnectedProps, connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { toast } from 'react-toastify';
import { withStore } from 'App/mstore';
import { useStore, withStore } from 'App/mstore';
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
import { clearSearch } from 'Duck/search';
import { edit, fetchList, remove, save, update } from 'Duck/site';
import { setSiteId } from 'Duck/site';
import { pushNewSite } from 'Duck/user';
@ -35,7 +34,6 @@ const NewSiteForm = ({
pushNewSite,
fetchList,
setSiteId,
clearSearch,
clearSearchLive,
location: { pathname },
onClose,
@ -44,6 +42,7 @@ const NewSiteForm = ({
canDelete,
}: Props) => {
const [existsError, setExistsError] = useState(false);
const { searchStore } = useStore();
useEffect(() => {
if (pathname.includes('onboarding')) {
@ -70,7 +69,7 @@ const NewSiteForm = ({
save(site).then((response: any) => {
if (!response || !response.errors || response.errors.size === 0) {
onClose(null);
clearSearch();
searchStore.clearSearch();
clearSearchLive();
mstore.initClient();
toast.success('Project added successfully');
@ -201,7 +200,6 @@ const connector = connect(mapStateToProps, {
pushNewSite,
fetchList,
setSiteId,
clearSearch,
clearSearchLive,
});

View file

@ -12,22 +12,20 @@ import { sessions as sessionsRoute } from 'App/routes';
import Divider from 'Components/Errors/ui/Divider';
import ErrorName from 'Components/Errors/ui/ErrorName';
import Label from 'Components/Errors/ui/Label';
import { addFilterByKeyAndValue } from 'Duck/search';
import { Button, ErrorDetails, Icon, Loader } from 'UI';
import SessionBar from './SessionBar';
function MainSection(props) {
const { errorStore } = useStore();
const { errorStore, searchStore } = useStore();
const error = errorStore.instance;
const trace = errorStore.instanceTrace;
const sourcemapUploaded = errorStore.sourcemapUploaded;
const loading = errorStore.isLoading;
const addFilterByKeyAndValue = props.addFilterByKeyAndValue;
const className = props.className;
const findSessions = () => {
addFilterByKeyAndValue(FilterKey.ERROR, error.message);
searchStore.addFilterByKeyAndValue(FilterKey.ERROR, error.message);
props.history.push(sessionsRoute());
};
return (
@ -103,7 +101,8 @@ function MainSection(props) {
<div className="flex items-center rounded overflow-hidden bg-gray-lightest">
<div className="bg-gray-light-shade py-1 px-2 text-disabled-text">
{Object.entries(tag)[0][0]}
</div>{' '}
</div>
{' '}
<div className="py-1 px-2 text-gray-dark">
{Object.entries(tag)[0][1]}
</div>
@ -130,5 +129,5 @@ function MainSection(props) {
}
export default withRouter(
connect(null, { addFilterByKeyAndValue })(observer(MainSection))
connect(null)(observer(MainSection))
);

View file

@ -30,11 +30,11 @@ function Overview({ match: { params } }: IProps) {
<Switch>
<Route exact strict
path={[withSiteId(sessions(), siteId), withSiteId(notes(), siteId), withSiteId(bookmarks(), siteId)]}>
<div className='mb-5 w-full mx-auto' style={{ maxWidth: '1360px' }}>
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
<NoSessionsMessage siteId={siteId} />
<MainSearchBar />
<SessionSearch />
<div className='my-4' />
<div className="my-4" />
<SessionsTabOverview />
</div>
</Route>

View file

@ -2,12 +2,12 @@ import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { setAutoplayValues } from 'Duck/sessions';
import { withSiteId, session as sessionRoute } from 'App/routes';
import AutoplayToggle from "Shared/AutoplayToggle/AutoplayToggle";
import AutoplayToggle from 'Shared/AutoplayToggle/AutoplayToggle';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import cn from 'classnames';
import { fetchAutoplaySessions } from 'Duck/search';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Popover } from 'antd'
import { Button, Popover } from 'antd';
import { useStore } from 'App/mstore';
const PER_PAGE = 10;
@ -21,8 +21,8 @@ interface Props extends RouteComponentProps {
setAutoplayValues: () => void;
latestRequestTime: any;
sessionIds: any;
fetchAutoplaySessions: (page: number) => Promise<void>;
}
function QueueControls(props: Props) {
const {
siteId,
@ -34,10 +34,12 @@ function QueueControls(props: Props) {
latestRequestTime,
match: {
// @ts-ignore
params: { sessionId },
},
params: { sessionId }
}
} = props;
const { searchStore } = useStore();
const disabled = sessionIds.length === 0;
useEffect(() => {
@ -48,7 +50,7 @@ function QueueControls(props: Props) {
// check for the last page and load the next
if (currentPage !== totalPages && index === sessionIds.length - 1) {
props.fetchAutoplaySessions(currentPage + 1).then(props.setAutoplayValues);
searchStore.fetchAutoplaySessions(currentPage + 1).then(props.setAutoplayValues);
}
}
}, []);
@ -67,7 +69,7 @@ function QueueControls(props: Props) {
onClick={prevHandler}
className={cn('p-1 group rounded-full', {
'pointer-events-none opacity-50': !previousId,
'cursor-pointer': !!previousId,
'cursor-pointer': !!previousId
})}
>
<Popover
@ -85,7 +87,7 @@ function QueueControls(props: Props) {
onClick={nextHandler}
className={cn('p-1 group ml-1 rounded-full', {
'pointer-events-none opacity-50': !nextId,
'cursor-pointer': !!nextId,
'cursor-pointer': !!nextId
})}
>
<Popover
@ -110,7 +112,7 @@ export default connect(
currentPage: state.getIn(['search', 'currentPage']) || 1,
total: state.getIn(['sessions', 'total']) || 0,
sessionIds: state.getIn(['sessions', 'sessionIds']) || [],
latestRequestTime: state.getIn(['search', 'latestRequestTime']),
latestRequestTime: state.getIn(['search', 'latestRequestTime'])
}),
{ setAutoplayValues, fetchAutoplaySessions }
{ setAutoplayValues }
)(withRouter(QueueControls));

View file

@ -36,9 +36,10 @@ function FilterList(props: Props) {
actions = []
} = props;
const filters = List(filter.filters);
const hasEvents = filters.filter((i: any) => i.isEvent).size > 0;
const hasFilters = filters.filter((i: any) => !i.isEvent).size > 0;
const filters = filter.filters;
console.log('filters', filters)
const hasEvents = filters.filter((i: any) => i.isEvent).length > 0;
const hasFilters = filters.filter((i: any) => !i.isEvent).length > 0;
let rowIndex = 0;
const cannotDeleteFilter = hasEvents && !supportsEmpty;

View file

@ -3,46 +3,47 @@ import { connect } from 'react-redux';
import stl from './LiveSessionSearchField.module.css';
import { Input } from 'UI';
import LiveFilterModal from 'Shared/Filters/LiveFilterModal';
import { fetchFilterSearch } from 'Duck/search';
import { debounce } from 'App/utils';
import { edit as editFilter, addFilterByKeyAndValue } from 'Duck/liveSearch';
import { useStore } from 'App/mstore';
interface Props {
fetchFilterSearch: (query: any) => void;
editFilter: typeof editFilter;
addFilterByKeyAndValue: (key: string, value: string) => void;
}
function LiveSessionSearchField(props: Props) {
const debounceFetchFilterSearch = debounce(props.fetchFilterSearch, 1000)
const [showModal, setShowModal] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const { searchStore } = useStore();
const debounceFetchFilterSearch = debounce(searchStore.fetchFilterSearch, 1000);
const [showModal, setShowModal] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const onSearchChange = (e, { value }) => {
setSearchQuery(value)
setSearchQuery(value);
debounceFetchFilterSearch({ q: value });
}
};
const onAddFilter = (filter) => {
props.addFilterByKeyAndValue(filter.key, filter.value)
}
props.addFilterByKeyAndValue(filter.key, filter.value);
};
return (
<div className="relative">
<Input
// inputProps={ { "data-openreplay-label": "Search", "autocomplete": "off" } }
className={stl.searchField}
onFocus={ () => setShowModal(true) }
onBlur={ () => setTimeout(setShowModal, 200, false) }
onChange={ onSearchChange }
onFocus={() => setShowModal(true)}
onBlur={() => setTimeout(setShowModal, 200, false)}
onChange={onSearchChange}
icon="search"
placeholder={ 'Find live sessions by user or metadata.'}
placeholder={'Find live sessions by user or metadata.'}
fluid
id="search"
type="search"
autoComplete="off"
/>
{ showModal && (
{showModal && (
<div className="absolute left-0 shadow-sm rounded-lg bg-white z-50">
<LiveFilterModal
searchQuery={searchQuery}
@ -55,4 +56,4 @@ function LiveSessionSearchField(props: Props) {
);
}
export default connect(null, { fetchFilterSearch, editFilter, addFilterByKeyAndValue })(LiveSessionSearchField);
export default connect(null, { editFilter, addFilterByKeyAndValue })(LiveSessionSearchField);

View file

@ -2,24 +2,24 @@ import React from 'react';
import SessionSearchField from 'Shared/SessionSearchField';
import AiSessionSearchField from 'Shared/SessionSearchField/AiSessionSearchField';
import SavedSearch from 'Shared/SavedSearch';
// import { Button } from 'UI';
import { Button } from 'antd';
import { connect } from 'react-redux';
import { clearSearch } from 'Duck/search';
import TagList from './components/TagList';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
interface Props {
clearSearch: () => void;
appliedFilter: any;
savedSearch: any;
site: any;
}
const MainSearchBar = (props: Props) => {
const { appliedFilter, site } = props;
const currSite = React.useRef(site)
const { site } = props;
const { searchStore } = useStore();
const appliedFilter = searchStore.instance;
const savedSearch = searchStore.savedSearch;
const currSite = React.useRef(site);
const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0;
const hasSavedSearch = props.savedSearch && props.savedSearch.exists();
const hasSavedSearch = savedSearch && savedSearch.exists();
const hasSearch = hasFilters || hasSavedSearch;
// @ts-ignore
@ -28,11 +28,11 @@ const MainSearchBar = (props: Props) => {
React.useEffect(() => {
if (site !== currSite.current && currSite.current !== undefined) {
console.debug('clearing filters due to project change')
props.clearSearch();
currSite.current = site
console.debug('clearing filters due to project change');
searchStore.clearSearch();
currSite.current = site;
}
}, [site])
}, [site]);
return (
<div className="flex items-center flex-wrap">
<div style={{ flex: 3, marginRight: '10px' }}>
@ -44,10 +44,10 @@ const MainSearchBar = (props: Props) => {
<Button
// variant={hasSearch ? 'text-primary' : 'text'}
// className="ml-auto font-medium"
type='link'
type="link"
disabled={!hasSearch}
onClick={() => props.clearSearch()}
className='ml-auto font-medium'
onClick={() => searchStore.clearSearch()}
className="ml-auto font-medium"
>
Clear Search
</Button>
@ -58,11 +58,6 @@ const MainSearchBar = (props: Props) => {
export default connect(
(state: any) => ({
appliedFilter: state.getIn(['search', 'instance']),
savedSearch: state.getIn(['search', 'savedSearch']),
site: state.getIn(['site', 'siteId']),
}),
{
clearSearch,
}
)(MainSearchBar);
site: state.getIn(['site', 'siteId'])
})
)(observer(MainSearchBar));

View file

@ -1,7 +1,5 @@
import { Tag } from 'App/services/TagWatchService';
import { useModal } from 'Components/Modal';
import { refreshFilterOptions, addFilterByKeyAndValue } from 'Duck/search';
import { connect } from 'react-redux';
import React from 'react';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
@ -11,11 +9,8 @@ import { Icon, confirm } from 'UI';
import { Button, Typography } from 'antd';
import { toast } from 'react-toastify';
function TagList(props: {
refreshFilterOptions: typeof refreshFilterOptions;
addFilterByKeyAndValue: typeof addFilterByKeyAndValue;
}) {
const { refreshFilterOptions, addFilterByKeyAndValue } = props;
function TagList() {
const { searchStore } = useStore();
const { tagWatchStore } = useStore();
const { showModal, hideModal } = useModal();
@ -27,29 +22,29 @@ function TagList(props: {
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({ label: tag.name, value: tag.tagId.toString() }))
);
refreshFilterOptions();
searchStore.refreshFilterOptions();
}
});
}
}, []);
const addTag = (tagId: number) => {
addFilterByKeyAndValue(FilterKey.TAGGED_ELEMENT, tagId.toString());
searchStore.addFilterByKeyAndValue(FilterKey.TAGGED_ELEMENT, tagId.toString());
hideModal();
};
const openModal = () => {
showModal(<TagListModal onTagClick={addTag} />, {
right: true,
width: 400,
width: 400
});
};
return (
<Button
// variant={'outline'}
type='primary'
<Button
// variant={'outline'}
type="primary"
ghost
className='gap-1'
disabled={!tagWatchStore.tags.length}
className="gap-1"
disabled={!tagWatchStore.tags.length}
onClick={openModal}>
<span>Tags</span>
<span className={'font-medium ml-1'}>{tagWatchStore.tags.length}</span>
@ -71,7 +66,7 @@ const TagListModal = observer(({ onTagClick }: { onTagClick: (tagId: number) =>
await confirm({
header: 'Remove Tag',
confirmButton: 'Remove',
confirmation: 'Are you sure you want to remove this tag?',
confirmation: 'Are you sure you want to remove this tag?'
})
) {
void tagWatchStore.deleteTag(id);
@ -129,7 +124,7 @@ const TagRow = (props: {
setName(tag.name);
},
triggerType: [],
maxLength: 90,
maxLength: 90
}}
>
{tag.name}
@ -157,6 +152,4 @@ const TagRow = (props: {
);
};
export default connect(() => ({}), { refreshFilterOptions, addFilterByKeyAndValue })(
observer(TagList)
);
export default observer(TagList);

View file

@ -1,12 +1,11 @@
import React from 'react';
import { SideMenuitem } from 'UI';
import { connect } from 'react-redux';
import { setActiveTab } from 'Duck/search';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { sessions, fflags, withSiteId, notes, bookmarks } from 'App/routes';
import { useStore } from 'App/mstore';
interface Props {
setActiveTab: (tab: any) => void;
activeTab: string;
isEnterprise: boolean;
}
@ -21,12 +20,13 @@ const TabToUrlMap = {
function OverviewMenu(props: Props & RouteComponentProps) {
// @ts-ignore
const { activeTab, isEnterprise, history, match: { params: { siteId } }, location } = props;
const { searchStore } = useStore();
React.useEffect(() => {
const currentLocation = location.pathname;
const tab = Object.keys(TabToUrlMap).find((tab: keyof typeof TabToUrlMap) => currentLocation.includes(TabToUrlMap[tab]));
if (tab && tab !== activeTab) {
props.setActiveTab({ type: tab });
searchStore.setActiveTab({ type: tab });
}
}, [location.pathname]);
@ -39,7 +39,7 @@ function OverviewMenu(props: Props & RouteComponentProps) {
title='Sessions'
iconName='play-circle-bold'
onClick={() => {
props.setActiveTab({ type: 'all' });
searchStore.setActiveTab({ type: 'all' });
!location.pathname.includes(sessions()) && history.push(withSiteId(sessions(), siteId));
}}
/>
@ -63,7 +63,7 @@ function OverviewMenu(props: Props & RouteComponentProps) {
title='Notes'
iconName='stickies'
onClick={() => {
props.setActiveTab({ type: 'notes' });
searchStore.setActiveTab({ type: 'notes' });
!location.pathname.includes(notes()) && history.push(withSiteId(notes(), siteId));
}}
/>
@ -75,7 +75,7 @@ function OverviewMenu(props: Props & RouteComponentProps) {
title='Feature Flags'
iconName='toggles'
onClick={() => {
props.setActiveTab({ type: 'flags' });
searchStore.setActiveTab({ type: 'flags' });
!location.pathname.includes(fflags()) && history.push(withSiteId(fflags(), siteId));
}}
/>
@ -87,4 +87,4 @@ function OverviewMenu(props: Props & RouteComponentProps) {
export default connect((state: any) => ({
activeTab: state.getIn(['search', 'activeTab', 'type']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee'
}), { setActiveTab })(withRouter(OverviewMenu));
}), )(withRouter(OverviewMenu));

View file

@ -1,6 +1,6 @@
import {
CaretDownOutlined,
FolderAddOutlined,
FolderAddOutlined
} from '@ant-design/icons';
import { Button, Divider, Dropdown, Space, Typography } from 'antd';
import cn from 'classnames';
@ -13,7 +13,6 @@ import { hasSiteId, siteChangeAvailable } from 'App/routes';
import NewSiteForm from 'Components/Client/Sites/NewSiteForm';
import { useModal } from 'Components/Modal';
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
import { clearSearch } from 'Duck/search';
import { setSiteId } from 'Duck/site';
import { init as initProject } from 'Duck/site';
import { Icon } from 'UI';
@ -30,7 +29,6 @@ interface Props extends RouteComponentProps {
sites: Site[];
siteId: string;
setSiteId: (siteId: string) => void;
clearSearch: (isSession: boolean) => void;
clearSearchLive: () => void;
initProject: (data: any) => void;
mstore: any;
@ -44,12 +42,13 @@ function ProjectDropdown(props: Props) {
const showCurrent =
hasSiteId(location.pathname) || siteChangeAvailable(location.pathname);
const { showModal, hideModal } = useModal();
const { customFieldStore } = useStore();
const { customFieldStore, searchStore } = useStore();
const handleSiteChange = async (newSiteId: string) => {
props.setSiteId(newSiteId); // Fixed: should set the new siteId, not the existing one
await customFieldStore.fetchList(newSiteId)
props.clearSearch(location.pathname.includes('/sessions'));
await customFieldStore.fetchList(newSiteId);
// searchStore.clearSearch(location.pathname.includes('/sessions'));
searchStore.clearSearch();
props.clearSearchLive();
props.mstore.initClient();
@ -82,7 +81,7 @@ function ProjectDropdown(props: Props) {
{site.host}
</Text>
</div>
),
)
}));
if (isAdmin) {
menuItems.unshift({
@ -99,7 +98,7 @@ function ProjectDropdown(props: Props) {
</div>
<Divider style={{ marginTop: 4, marginBottom: 0 }} />
</>
),
)
});
}
@ -111,8 +110,8 @@ function ProjectDropdown(props: Props) {
defaultSelectedKeys: [siteId],
style: {
maxHeight: 500,
overflowY: 'auto',
},
overflowY: 'auto'
}
}}
placement="bottomLeft"
>
@ -144,14 +143,13 @@ function ProjectDropdown(props: Props) {
const mapStateToProps = (state: any) => ({
sites: state.getIn(['site', 'list']),
siteId: state.getIn(['site', 'siteId']),
account: state.getIn(['user', 'account']),
account: state.getIn(['user', 'account'])
});
export default withRouter(
connect(mapStateToProps, {
setSiteId,
clearSearch,
clearSearchLive,
initProject,
initProject
})(withStore(ProjectDropdown))
);

View file

@ -1,36 +1,33 @@
import React from 'react';
import { connect } from 'react-redux';
import { editSavedSearch as edit, save, remove } from 'Duck/search';
import { Button, Modal, Form, Icon, Checkbox, Input } from 'UI';
import { confirm } from 'UI';
import stl from './SaveSearchModal.module.css';
import cn from 'classnames';
import { toast } from 'react-toastify';
import { useStore } from 'App/mstore';
interface Props {
filter: any;
loading: boolean;
edit: (filter: any) => void;
save: (searchId: any, rename: boolean) => Promise<void>;
show: boolean;
closeHandler: () => void;
savedSearch: any;
remove: (filterId: number) => Promise<void>;
userId: number;
rename: boolean;
}
function SaveSearchModal(props: Props) {
const { savedSearch, loading, show, closeHandler, rename = false } = props;
const { searchStore } = useStore();
const onNameChange = ({ target: { value } }: any) => {
props.edit({ name: value });
searchStore.edit({ name: value });
};
const onSave = () => {
const { closeHandler } = props;
props
.save(savedSearch.exists() ? savedSearch.searchId : null, rename)
searchStore.save(savedSearch.exists() ? savedSearch.searchId : null, rename)
.then(() => {
toast.success(`${savedSearch.exists() ? 'Updated' : 'Saved'} Successfully`);
closeHandler();
@ -48,13 +45,13 @@ function SaveSearchModal(props: Props) {
confirmation: `Are you sure you want to permanently delete this Saved search?`,
})
) {
props.remove(savedSearch.searchId).then(() => {
searchStore.remove(savedSearch.searchId).then(() => {
closeHandler();
});
}
};
const onChangeOption = ({ target: { checked, name } }: any) => props.edit({ [name]: checked });
const onChangeOption = ({ target: { checked, name } }: any) => searchStore.edit({ [name]: checked });
return (
<Modal size="small" open={show} onClose={closeHandler}>
@ -88,7 +85,7 @@ function SaveSearchModal(props: Props) {
/>
<div
className="flex items-center cursor-pointer select-none"
onClick={() => props.edit({ isPublic: !savedSearch.isPublic })}
onClick={() => searchStore.edit({ isPublic: !savedSearch.isPublic })}
>
<Icon name="user-friends" size="16" />
<span className="ml-2"> Team Visible</span>
@ -122,5 +119,4 @@ export default connect(
filter: state.getIn(['search', 'instance']),
loading: state.getIn(['search', 'saveRequest', 'loading']) || state.getIn(['search', 'updateRequest', 'loading']),
}),
{ edit, save, remove }
)(SaveSearchModal);

View file

@ -2,43 +2,43 @@ import React, { useEffect } from 'react';
import { Icon } from 'UI';
import { Button } from 'antd';
import { connect } from 'react-redux';
import { fetchList as fetchListSavedSearch } from 'Duck/search';
import cn from 'classnames';
import stl from './SavedSearch.module.css';
import { useModal } from 'App/components/Modal';
import SavedSearchModal from './components/SavedSearchModal'
import SavedSearchModal from './components/SavedSearchModal';
import { useStore } from 'App/mstore';
interface Props {
fetchListSavedSearch: () => void;
list: any;
savedSearch: any;
fetchedMeta: boolean
}
function SavedSearch(props: Props) {
const { list } = props;
const { savedSearch } = props;
const { showModal } = useModal();
const { searchStore, customFieldStore } = useStore();
const savedSearch = searchStore.savedSearch;
const list = searchStore.list;
const fetchedMeta = customFieldStore.fetchedMetadata;
useEffect(() => {
if (list.size === 0 && props.fetchedMeta) {
props.fetchListSavedSearch()
if (list.size === 0 && fetchedMeta) {
searchStore.fetchList(); // TODO check this call
}
}, [props.fetchedMeta])
}, [fetchedMeta]);
return (
<div className={cn("flex items-center", { [stl.disabled] : list.size === 0})}>
<div className={cn('flex items-center', { [stl.disabled]: list.size === 0 })}>
<Button
// variant="outline"
type='primary'
type="primary"
ghost
onClick={() => showModal(<SavedSearchModal />, { right: true, width: 450 })}
className='flex gap-1'
className="flex gap-1"
>
<span className="mr-1">Saved Search</span>
<span className="font-meidum">{list.size}</span>
<Icon name="ellipsis-v" color="teal" size="14" />
</Button>
{ savedSearch.exists() && (
{savedSearch.exists() && (
<div className="flex items-center ml-2">
<Icon name="search" size="14" />
<span className="color-gray-medium px-1">Viewing:</span>
@ -51,8 +51,4 @@ function SavedSearch(props: Props) {
);
}
export default connect((state: any) => ({
list: state.getIn([ 'search', 'list' ]),
savedSearch: state.getIn([ 'search', 'savedSearch' ]),
fetchedMeta: state.getIn(['customFields', 'fetchedMetadata'])
}), { fetchListSavedSearch })(SavedSearch);
export default connect((state: any) => ({}))(SavedSearch);

View file

@ -3,111 +3,114 @@ import cn from 'classnames';
import { Icon, Input } from 'UI';
import { List } from 'immutable';
import { confirm, Tooltip } from 'UI';
import { applySavedSearch, remove, editSavedSearch } from 'Duck/search';
import { connect } from 'react-redux';
import { useModal } from 'App/components/Modal';
import { SavedSearch } from 'Types/ts/search';
import SaveSearchModal from 'Shared/SaveSearchModal';
import stl from './savedSearchModal.module.css';
import { useStore } from 'App/mstore';
interface ITooltipIcon {
title: string;
name: string;
onClick: (e: MouseEvent<HTMLDivElement>) => void;
title: string;
name: string;
onClick: (e: MouseEvent<HTMLDivElement>) => void;
}
function TooltipIcon(props: ITooltipIcon) {
return (
<div onClick={(e) => props.onClick(e)}>
<Tooltip title={props.title}>
{/* @ts-ignore */}
<Icon size="16" name={props.name} color="main" />
</Tooltip>
</div>
);
return (
<div onClick={(e) => props.onClick(e)}>
<Tooltip title={props.title}>
{/* @ts-ignore */}
<Icon size="16" name={props.name} color="main" />
</Tooltip>
</div>
);
}
interface Props {
list: List<SavedSearch>;
applySavedSearch: (item: SavedSearch) => void;
remove: (itemId: number) => void;
editSavedSearch: (item: SavedSearch) => void;
list: List<SavedSearch>;
applySavedSearch: (item: SavedSearch) => void;
remove: (itemId: number) => void;
editSavedSearch: (item: SavedSearch) => void;
}
function SavedSearchModal(props: Props) {
const { hideModal } = useModal();
const [showModal, setshowModal] = useState(false);
const [filterQuery, setFilterQuery] = useState('');
const { hideModal } = useModal();
const [showModal, setshowModal] = useState(false);
const [filterQuery, setFilterQuery] = useState('');
const { searchStore } = useStore();
const onClick = (item: SavedSearch, e) => {
e.stopPropagation();
props.applySavedSearch(item);
hideModal();
};
const onDelete = async (item: SavedSearch, e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
const confirmation = await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: 'Are you sure you want to permanently delete this search?',
});
if (confirmation) {
props.remove(item.searchId);
}
};
const onEdit = (item: SavedSearch, e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
props.editSavedSearch(item);
setTimeout(() => setshowModal(true), 0);
};
const onClick = (item: SavedSearch, e) => {
e.stopPropagation();
searchStore.applySavedSearch(item);
hideModal();
};
const onDelete = async (item: SavedSearch, e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
const confirmation = await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: 'Are you sure you want to permanently delete this search?'
});
if (confirmation) {
searchStore.remove(item.searchId + '');
}
};
const onEdit = (item: SavedSearch, e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
searchStore.editSavedSearch(item);
setTimeout(() => setshowModal(true), 0);
};
const shownItems = props.list.filter((item) => item.name.toLocaleLowerCase().includes(filterQuery.toLocaleLowerCase()));
const shownItems = props.list.filter((item) => item.name.toLocaleLowerCase().includes(filterQuery.toLocaleLowerCase()));
return (
<div className="bg-white box-shadow h-screen">
<div className="p-6">
<h1 className="text-2xl">
Saved Search <span className="color-gray-medium">{props.list.size}</span>
</h1>
</div>
{props.list.size > 1 && (
<div className="mb-6 w-full px-4">
<Input
icon="search"
onChange={({ target: { value } }: any) => setFilterQuery(value)}
placeholder="Filter by name"
/>
</div>
)}
<div style={{ maxHeight: 'calc(100vh - 106px)', overflowY: 'auto' }}>
{shownItems.map((item) => (
<div
key={item.key}
className={cn('p-4 cursor-pointer border-b flex items-center group hover:bg-active-blue', item.isPublic && 'pb-10')}
onClick={(e) => onClick(item, e)}
>
<Icon name="search" color="gray-medium" size="16" />
<div className="ml-4">
<div className="text-lg">{item.name} </div>
{item.isPublic && (
<div className={cn(stl.iconContainer, 'absolute color-gray-medium flex items-center px-2 mt-2')}>
<Icon name="user-friends" size="11" />
<div className="ml-1 text-sm"> Team </div>
</div>
)}
</div>
<div className="flex items-center ml-auto self-center">
<div className={cn(stl.iconCircle, 'mr-2 invisible group-hover:visible')}>
<TooltipIcon name="pencil" onClick={(e) => onEdit(item, e)} title="Rename" />
</div>
<div className={cn(stl.iconCircle, 'invisible group-hover:visible')}>
<TooltipIcon name="trash" onClick={(e) => onDelete(item, e)} title="Delete" />
</div>
</div>
</div>
))}
</div>
{showModal && <SaveSearchModal show closeHandler={() => setshowModal(false)} rename={true} />}
return (
<div className="bg-white box-shadow h-screen">
<div className="p-6">
<h1 className="text-2xl">
Saved Search <span className="color-gray-medium">{props.list.size}</span>
</h1>
</div>
{props.list.size > 1 && (
<div className="mb-6 w-full px-4">
<Input
icon="search"
onChange={({ target: { value } }: any) => setFilterQuery(value)}
placeholder="Filter by name"
/>
</div>
);
)}
<div style={{ maxHeight: 'calc(100vh - 106px)', overflowY: 'auto' }}>
{shownItems.map((item) => (
<div
key={item.key}
className={cn('p-4 cursor-pointer border-b flex items-center group hover:bg-active-blue', item.isPublic && 'pb-10')}
onClick={(e) => onClick(item, e)}
>
<Icon name="search" color="gray-medium" size="16" />
<div className="ml-4">
<div className="text-lg">{item.name} </div>
{item.isPublic && (
<div className={cn(stl.iconContainer, 'absolute color-gray-medium flex items-center px-2 mt-2')}>
<Icon name="user-friends" size="11" />
<div className="ml-1 text-sm"> Team</div>
</div>
)}
</div>
<div className="flex items-center ml-auto self-center">
<div className={cn(stl.iconCircle, 'mr-2 invisible group-hover:visible')}>
<TooltipIcon name="pencil" onClick={(e) => onEdit(item, e)} title="Rename" />
</div>
<div className={cn(stl.iconCircle, 'invisible group-hover:visible')}>
<TooltipIcon name="trash" onClick={(e) => onDelete(item, e)} title="Delete" />
</div>
</div>
</div>
))}
</div>
{showModal && <SaveSearchModal show closeHandler={() => setshowModal(false)} rename={true} />}
</div>
);
}
export default connect((state: any) => ({ list: state.getIn(['search', 'list']) }), { applySavedSearch, remove, editSavedSearch })(SavedSearchModal);
export default connect((state: any) => ({ list: state.getIn(['search', 'list']) }))(SavedSearchModal);

View file

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import AnimatedSVG, { ICONS } from "Shared/AnimatedSVG/AnimatedSVG";
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import FilterList from 'Shared/Filters/FilterList';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SaveFilterButton from 'Shared/SaveFilterButton';
@ -7,35 +7,31 @@ import { connect } from 'react-redux';
import { FilterKey } from 'Types/filter/filterType';
import { addOptionsToFilter } from 'Types/filter/newFilter';
import { Button, Loader } from 'UI';
import { edit, addFilter, fetchSessions, updateFilter } from 'Duck/search';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import { debounce } from 'App/utils';
import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler';
import { refreshFilterOptions } from 'Duck/search';
let debounceFetch: any = () => {};
let debounceFetch: any = () => {
};
interface Props {
appliedFilter: any;
edit: typeof edit;
addFilter: typeof addFilter;
saveRequestPayloads: boolean;
metaLoading?: boolean;
fetchSessions: typeof fetchSessions;
updateFilter: typeof updateFilter;
refreshFilterOptions: typeof refreshFilterOptions;
}
function SessionSearch(props: Props) {
const { tagWatchStore, aiFiltersStore } = useStore();
const { appliedFilter, saveRequestPayloads = false, metaLoading = false } = props;
const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).size > 0;
const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).size > 0;
const { tagWatchStore, aiFiltersStore, searchStore, customFieldStore } = useStore();
const appliedFilter = searchStore.instance;
const metaLoading = customFieldStore.isLoading;
const { saveRequestPayloads = false } = props;
const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).length > 0;
const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).length > 0;
console.log('appliedFilter', appliedFilter)
useSessionSearchQueryHandler({
appliedFilter,
applyFilter: props.updateFilter,
applyFilter: searchStore.updateFilter,
loading: metaLoading,
onBeforeLoad: async () => {
const tags = await tagWatchStore.getTags();
@ -44,20 +40,20 @@ function SessionSearch(props: Props) {
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({
label: tag.name,
value: tag.tagId.toString(),
value: tag.tagId.toString()
}))
);
props.refreshFilterOptions();
searchStore.refreshFilterOptions();
}
},
}
});
useEffect(() => {
debounceFetch = debounce(() => props.fetchSessions(), 500);
debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
}, []);
const onAddFilter = (filter: any) => {
props.addFilter(filter);
searchStore.addFilter(filter);
};
const onUpdateFilter = (filterIndex: any, filter: any) => {
@ -69,38 +65,38 @@ function SessionSearch(props: Props) {
}
});
props.updateFilter({
searchStore.updateFilter({
...appliedFilter,
filters: newFilters,
filters: newFilters
});
debounceFetch();
};
const onFilterMove = (newFilters: any) => {
props.updateFilter({
searchStore.updateFilter({
...appliedFilter,
filters: newFilters,
filters: newFilters
});
debounceFetch();
}
};
const onRemoveFilter = (filterIndex: any) => {
const newFilters = appliedFilter.filters.filter((_filter: any, i: any) => {
return i !== filterIndex;
});
props.updateFilter({
filters: newFilters,
searchStore.updateFilter({
filters: newFilters
});
debounceFetch();
};
const onChangeEventsOrder = (e: any, { value }: any) => {
props.updateFilter({
eventsOrder: value,
searchStore.updateFilter({
eventsOrder: value
});
debounceFetch();
@ -154,9 +150,6 @@ function SessionSearch(props: Props) {
export default connect(
(state: any) => ({
saveRequestPayloads: state.getIn(['site', 'instance', 'saveRequestPayloads']),
appliedFilter: state.getIn(['search', 'instance']),
metaLoading: state.getIn(['customFields', 'fetchRequestActive', 'loading']),
}),
{ edit, addFilter, fetchSessions, updateFilter, refreshFilterOptions }
saveRequestPayloads: state.getIn(['site', 'instance', 'saveRequestPayloads'])
})
)(observer(SessionSearch));

View file

@ -9,14 +9,8 @@ import { assist as assistRoute, isRoute } from 'App/routes';
import { debounce } from 'App/utils';
import {
addFilterByKeyAndValue as liveAddFilterByKeyAndValue,
fetchFilterSearch as liveFetchFilterSearch,
fetchFilterSearch as liveFetchFilterSearch
} from 'Duck/liveSearch';
import {
addFilterByKeyAndValue,
clearSearch,
edit,
fetchFilterSearch,
} from 'Duck/search';
import { Icon, Input } from 'UI';
import FilterModal from 'Shared/Filters/FilterModal';
@ -26,23 +20,20 @@ import OutsideClickDetectingDiv from '../OutsideClickDetectingDiv';
const ASSIST_ROUTE = assistRoute();
interface Props {
fetchFilterSearch: (query: any) => void;
addFilterByKeyAndValue: (key: string, value: string) => void;
liveAddFilterByKeyAndValue: (key: string, value: string) => void;
liveFetchFilterSearch: any;
appliedFilter: any;
edit: typeof edit;
clearSearch: typeof clearSearch;
setFocused?: (focused: boolean) => void;
}
function SessionSearchField(props: Props) {
const { searchStore } = useStore();
const isLive =
isRoute(ASSIST_ROUTE, window.location.pathname) ||
window.location.pathname.includes('multiview');
const debounceFetchFilterSearch = React.useCallback(
debounce(
isLive ? props.liveFetchFilterSearch : props.fetchFilterSearch,
isLive ? props.liveFetchFilterSearch : searchStore.fetchFilterSearch,
1000
),
[]
@ -59,7 +50,7 @@ function SessionSearchField(props: Props) {
const onAddFilter = (filter: any) => {
isLive
? props.liveAddFilterByKeyAndValue(filter.key, filter.value)
: props.addFilterByKeyAndValue(filter.key, filter.value);
: searchStore.addFilterByKeyAndValue(filter.key, filter.value);
};
const onFocus = () => {
@ -102,13 +93,15 @@ function SessionSearchField(props: Props) {
);
}
const AiSearchField = observer(
({ edit, appliedFilter, clearSearch }: Props) => {
const AiSearchField = observer(() => {
const { searchStore } = useStore();
const appliedFilter = searchStore.instance;
const hasFilters =
appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0;
appliedFilter && appliedFilter.filters && appliedFilter.filters.length > 0;
const { aiFiltersStore } = useStore();
const [searchQuery, setSearchQuery] = useState('');
const onSearchChange = ({ target: { value } }: any) => {
setSearchQuery(value);
};
@ -126,13 +119,13 @@ const AiSearchField = observer(
};
const clearAll = () => {
clearSearch();
searchStore.clearSearch();
setSearchQuery('');
};
React.useEffect(() => {
if (aiFiltersStore.filtersSetKey !== 0) {
edit(aiFiltersStore.filters);
searchStore.edit(aiFiltersStore.filters);
}
}, [aiFiltersStore.filters, aiFiltersStore.filtersSetKey]);
@ -186,8 +179,8 @@ function AiSessionSearchField(props: Props) {
};
const boxStyle = tab === 'ask'
? gradientBox
: isFocused ? regularBoxFocused : regularBoxUnfocused;
? gradientBox
: isFocused ? regularBoxFocused : regularBoxUnfocused;
return (
<div className={'bg-white rounded-full shadow-sm'}>
<div
@ -242,9 +235,9 @@ function AiSessionSearchField(props: Props) {
onClick: () => {
changeValue('ask');
closeTour();
},
},
},
}
}
}
]}
/>
</div>
@ -253,10 +246,10 @@ function AiSessionSearchField(props: Props) {
}
export const AskAiSwitchToggle = ({
enabled,
setEnabled,
loading,
}: {
enabled,
setEnabled,
loading
}: {
enabled: boolean;
loading: boolean;
setEnabled: () => void;
@ -279,7 +272,7 @@ export const AskAiSwitchToggle = ({
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
border: 0,
verticalAlign: 'middle',
verticalAlign: 'middle'
}}
>
<div
@ -293,7 +286,7 @@ export const AskAiSwitchToggle = ({
transition: 'all 0.2s ease-in-out',
background: '#fff',
borderRadius: 100,
verticalAlign: 'middle',
verticalAlign: 'middle'
}}
/>
<div
@ -304,7 +297,7 @@ export const AskAiSwitchToggle = ({
height: '100%',
transition: 'all 0.2s ease-in-out',
paddingInline: !enabled ? '30px 0px' : '10px 24px',
width: 88,
width: 88
}}
>
<div style={{ color: 'white', fontSize: 16 }}>Ask AI</div>
@ -324,7 +317,7 @@ export const gradientBox = {
display: 'flex',
gap: '0.25rem',
alignItems: 'center',
width: '100%',
width: '100%'
};
const regularBoxUnfocused = {
@ -334,7 +327,7 @@ const regularBoxUnfocused = {
display: 'flex',
gap: '0.25rem',
alignItems: 'center',
width: '100%',
width: '100%'
};
const regularBoxFocused = {
@ -344,19 +337,13 @@ const regularBoxFocused = {
display: 'flex',
gap: '0.25rem',
alignItems: 'center',
width: '100%',
}
width: '100%'
};
export default connect(
(state: any) => ({
appliedFilter: state.getIn(['search', 'instance']),
}),
(state: any) => ({}),
{
addFilterByKeyAndValue,
fetchFilterSearch,
liveFetchFilterSearch,
liveAddFilterByKeyAndValue,
edit,
clearSearch,
liveAddFilterByKeyAndValue
}
)(observer(AiSessionSearchField));

View file

@ -4,28 +4,27 @@ import { Input } from 'UI';
import FilterModal from 'Shared/Filters/FilterModal';
import { debounce } from 'App/utils';
import { assist as assistRoute, isRoute } from 'App/routes';
import { addFilterByKeyAndValue, fetchFilterSearch } from 'Duck/search';
import {
addFilterByKeyAndValue as liveAddFilterByKeyAndValue,
fetchFilterSearch as liveFetchFilterSearch,
fetchFilterSearch as liveFetchFilterSearch
} from 'Duck/liveSearch';
const ASSIST_ROUTE = assistRoute();
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
interface Props {
fetchFilterSearch: (query: any) => void;
addFilterByKeyAndValue: (key: string, value: string) => void;
liveAddFilterByKeyAndValue: (key: string, value: string) => void;
liveFetchFilterSearch: any;
}
function SessionSearchField(props: Props) {
const { searchStore } = useStore();
const isLive =
isRoute(ASSIST_ROUTE, window.location.pathname) ||
window.location.pathname.includes('multiview');
const debounceFetchFilterSearch = React.useCallback(
debounce(isLive ? props.liveFetchFilterSearch : props.fetchFilterSearch, 1000),
debounce(isLive ? props.liveFetchFilterSearch : searchStore.fetchFilterSearch, 1000),
[]
);
const [showModal, setShowModal] = useState(false);
@ -39,7 +38,7 @@ function SessionSearchField(props: Props) {
const onAddFilter = (filter: any) => {
isLive
? props.liveAddFilterByKeyAndValue(filter.key, filter.value)
: props.addFilterByKeyAndValue(filter.key, filter.value);
: searchStore.addFilterByKeyAndValue(filter.key, filter.value);
};
return (
@ -72,8 +71,6 @@ function SessionSearchField(props: Props) {
}
export default connect(null, {
addFilterByKeyAndValue,
fetchFilterSearch,
liveFetchFilterSearch,
liveAddFilterByKeyAndValue,
liveAddFilterByKeyAndValue
})(observer(SessionSearchField));

View file

@ -1,20 +1,21 @@
import React from 'react';
import { connect } from 'react-redux';
import { updateCurrentPage } from 'Duck/search';
import { numberWithCommas } from 'App/utils'
import { numberWithCommas } from 'App/utils';
import { useStore } from 'App/mstore';
interface Props {
latestSessions: any;
updateCurrentPage: (page: number) => void;
}
function LatestSessionsMessage(props: Props) {
const { latestSessions = [] } = props;
const count = latestSessions.length;
const { searchStore } = useStore();
return count > 0 ? (
<div
className="bg-amber-50 p-1 flex w-full border-b text-center justify-center link"
style={{ backgroundColor: 'rgb(255 251 235)' }}
onClick={() => props.updateCurrentPage(1)}
onClick={() => searchStore.updateCurrentPage(1)}
>
Show {numberWithCommas(count)} New {count > 1 ? 'Sessions' : 'Session'}
</div>
@ -25,7 +26,6 @@ function LatestSessionsMessage(props: Props) {
export default connect(
(state: any) => ({
latestSessions: state.getIn(['search', 'latestList']),
}),
{ updateCurrentPage }
latestSessions: state.getIn(['search', 'latestList'])
})
)(LatestSessionsMessage);

View file

@ -1,38 +1,30 @@
import React, { useMemo } from 'react';
import { applyFilter } from 'Duck/search';
import Period from 'Types/app/period';
import SelectDateRange from 'Shared/SelectDateRange';
import SessionTags from '../SessionTags';
import NoteTags from '../Notes/NoteTags';
import { connect } from 'react-redux';
import SessionSort from '../SessionSort';
import { setActiveTab } from 'Duck/search';
import { Space } from 'antd';
import { useStore } from 'App/mstore';
interface Props {
listCount: number;
filter: any;
activeTab: string;
isEnterprise: boolean;
applyFilter: (filter: any) => void;
setActiveTab: (tab: any) => void;
}
function SessionHeader(props: Props) {
const {
filter: { startDate, endDate, rangeValue },
activeTab,
isEnterprise,
listCount
} = props;
const { searchStore } = useStore();
const activeTab = searchStore.activeTab;
const { startDate, endDate, rangeValue } = searchStore.instance;
const { isEnterprise } = props;
const period = Period({ start: startDate, end: endDate, rangeName: rangeValue });
const title = useMemo(() => {
if (activeTab === 'notes') {
if (activeTab.type === 'notes') {
return 'Notes';
}
if (activeTab === 'bookmark') {
if (activeTab.type === 'bookmark') {
return isEnterprise ? 'Vault' : 'Bookmarks';
}
return 'Sessions';
@ -40,18 +32,18 @@ function SessionHeader(props: Props) {
const onDateChange = (e: any) => {
const dateValues = e.toJSON();
props.applyFilter(dateValues);
searchStore.applyFilter(dateValues);
};
return (
<div className='flex items-center px-4 py-1 justify-between w-full'>
<h2 className='text-2xl capitalize mr-4'>{title}</h2>
{activeTab !== 'notes' ? (
<div className='flex items-center w-full justify-end'>
{activeTab !== 'bookmark' && (
<div className="flex items-center px-4 py-1 justify-between w-full">
<h2 className="text-2xl capitalize mr-4">{title}</h2>
{activeTab.type !== 'notes' ? (
<div className="flex items-center w-full justify-end">
{activeTab.type !== 'bookmark' && (
<>
<SessionTags />
<div className='mr-auto' />
<div className="mr-auto" />
<Space>
<SelectDateRange isAnt period={period} onChange={onDateChange} right={true} />
<SessionSort />
@ -61,8 +53,8 @@ function SessionHeader(props: Props) {
</div>
) : null}
{activeTab === 'notes' && (
<div className='flex items-center justify-end w-full'>
{activeTab.type === 'notes' && (
<div className="flex items-center justify-end w-full">
<NoteTags />
</div>
)}
@ -72,10 +64,7 @@ function SessionHeader(props: Props) {
export default connect(
(state: any) => ({
filter: state.getIn(['search', 'instance']),
listCount: state.getIn(['sessions', 'total']),
activeTab: state.getIn(['search', 'activeTab', 'type']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee'
}),
{ applyFilter, setActiveTab }
})
)(SessionHeader);

View file

@ -1,34 +1,25 @@
import React from 'react';
import { connect } from 'react-redux';
import Period from 'Types/app/period';
import { applyFilter } from 'Duck/search';
import SelectDateRange from 'Shared/SelectDateRange';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
interface Props {
filter: any;
applyFilter: (filter: any) => void;
}
function SessionDateRange(props: Props) {
const {
filter: { startDate, endDate, rangeValue },
} = props;
const period = Period({ start: startDate, end: endDate, rangeName: rangeValue });
const isCustom = period.rangeName === 'CUSTOM_RANGE'
function SessionDateRange() {
const { searchStore } = useStore();
const { startDate, endDate, rangeValue } = searchStore.instance
;
const period: any = Period({ start: startDate, end: endDate, rangeName: rangeValue });
const isCustom = period.rangeName === 'CUSTOM_RANGE';
const onDateChange = (e: any) => {
const dateValues = e.toJSON();
props.applyFilter(dateValues);
searchStore.applyFilter(dateValues);
};
return (
<div className="flex items-center">
<span className="mr-1">No sessions {isCustom ? 'between' : 'in the'}</span>
<SelectDateRange period={period} onChange={onDateChange} right={true} useButtonStyle={true}/>
<SelectDateRange period={period} onChange={onDateChange} right={true} useButtonStyle={true} />
</div>
);
}
export default connect(
(state: any) => ({
filter: state.getIn(['search', 'instance']),
}),
{ applyFilter }
)(SessionDateRange);
export default observer(SessionDateRange);

View file

@ -2,22 +2,16 @@ import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { FilterKey } from 'Types/filter/filterType';
import SessionItem from 'Shared/SessionItem';
import { NoContent, Loader, Pagination, Button, Icon } from 'UI';
import { NoContent, Loader, Pagination, Button } from 'UI';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import {
fetchSessions,
addFilterByKeyAndValue,
updateCurrentPage,
setScrollPosition,
checkForLatestSessions
} from 'Duck/search';
import { numberWithCommas } from 'App/utils';
import { toggleFavorite } from 'Duck/sessions';
import SessionDateRange from './SessionDateRange';
import RecordingStatus from 'Shared/SessionsTabOverview/components/RecordingStatus';
import { sessionService } from 'App/services';
import { updateProjectRecordingStatus } from 'Duck/site';
import { useStore } from 'App/mstore';
enum NoContentType {
Bookmarked,
@ -46,14 +40,9 @@ interface Props extends RouteComponentProps {
lastPlayedSessionId: string;
metaList: any;
scrollY: number;
addFilterByKeyAndValue: (key: string, value: any, operator?: string) => void;
updateCurrentPage: (page: number) => void;
setScrollPosition: (scrollPosition: number) => void;
fetchSessions: (filters: any, force: boolean) => void;
updateProjectRecordingStatus: (siteId: string, status: boolean) => void;
activeTab: any;
isEnterprise?: boolean;
checkForLatestSessions: () => void;
toggleFavorite: (sessionId: string) => Promise<void>;
sites: object[];
isLoggedIn: boolean;
@ -62,21 +51,20 @@ interface Props extends RouteComponentProps {
function SessionList(props: Props) {
const [noContentType, setNoContentType] = React.useState<NoContentType>(NoContentType.ToDate);
const { searchStore } = useStore();
const {
loading,
list,
currentPage,
pageSize,
total,
filters,
lastPlayedSessionId,
metaList,
activeTab,
isEnterprise = false,
sites,
isLoggedIn,
siteId
} = props;
const { currentPage, scrollY, activeTab, pageSize } = searchStore;
const { filters } = searchStore.instance;
const _filterKeys = filters.map((i: any) => i.key);
const hasUserFilter =
_filterKeys.includes(FilterKey.USERID) || _filterKeys.includes(FilterKey.USERANONYMOUSID);
@ -140,7 +128,7 @@ function SessionList(props: Props) {
if (statusData.status === 2 && activeSite) { // recording && processed
props.updateProjectRecordingStatus(activeSite.id, true);
props.fetchSessions(null, true);
searchStore.fetchSessions(true);
clearInterval(sessionStatusTimeOut);
}
}, [statusData, activeSite]);
@ -148,7 +136,7 @@ function SessionList(props: Props) {
useEffect(() => {
const id = setInterval(() => {
if (!document.hidden) {
props.checkForLatestSessions();
searchStore.checkForLatestSessions();
}
}, AUTOREFRESH_INTERVAL);
return () => clearInterval(id);
@ -161,12 +149,12 @@ function SessionList(props: Props) {
if (total === 0 && !loading && !hasNoRecordings) {
setTimeout(() => {
props.fetchSessions(null, true);
searchStore.fetchSessions(true);
}, 300);
}
return () => {
props.setScrollPosition(window.scrollY);
searchStore.setScrollPosition(window.scrollY);
};
}, []);
@ -178,7 +166,7 @@ function SessionList(props: Props) {
sessionTimeOut = setTimeout(function() {
if (!document.hidden) {
props.checkForLatestSessions();
searchStore.checkForLatestSessions();
}
}, 5000);
};
@ -192,15 +180,15 @@ function SessionList(props: Props) {
const onUserClick = (userId: any) => {
if (userId) {
props.addFilterByKeyAndValue(FilterKey.USERID, userId);
searchStore.addFilterByKeyAndValue(FilterKey.USERID, userId);
} else {
props.addFilterByKeyAndValue(FilterKey.USERID, '', 'isUndefined');
searchStore.addFilterByKeyAndValue(FilterKey.USERID, '', 'isUndefined');
}
};
const toggleFavorite = (sessionId: string) => {
props.toggleFavorite(sessionId).then(() => {
props.fetchSessions(null, true);
searchStore.fetchSessions(true);
});
};
@ -210,18 +198,18 @@ function SessionList(props: Props) {
<>
<NoContent
title={
<div className='flex items-center justify-center flex-col'>
<span className='py-5'>
<div className="flex items-center justify-center flex-col">
<span className="py-5">
<AnimatedSVG name={NO_CONTENT.icon} size={60} />
</span>
<div className='mt-4' />
<div className='text-center relative text-lg font-medium'>
{NO_CONTENT.message }
<div className="mt-4" />
<div className="text-center relative text-lg font-medium">
{NO_CONTENT.message}
</div>
</div>
}
subtext={
<div className='flex flex-col items-center'>
<div className="flex flex-col items-center">
{(isVault || isBookmark) && (
<div>
{isVault
@ -230,11 +218,11 @@ function SessionList(props: Props) {
</div>
)}
<Button
variant='text-primary'
className='mt-4'
icon='arrow-repeat'
variant="text-primary"
className="mt-4"
icon="arrow-repeat"
iconSize={20}
onClick={() => props.fetchSessions(null, true)}
onClick={() => searchStore.fetchSessions(true)}
>
Refresh
</Button>
@ -243,7 +231,7 @@ function SessionList(props: Props) {
show={!loading && list.length === 0}
>
{list.map((session: any) => (
<div key={session.sessionId} className='border-b'>
<div key={session.sessionId} className="border-b">
<SessionItem
session={session}
hasUserFilter={hasUserFilter}
@ -258,16 +246,16 @@ function SessionList(props: Props) {
</NoContent>
{total > 0 && (
<div className='flex items-center justify-between p-5'>
<div className="flex items-center justify-between p-5">
<div>
Showing <span className='font-medium'>{(currentPage - 1) * pageSize + 1}</span> to{' '}
<span className='font-medium'>{(currentPage - 1) * pageSize + list.length}</span> of{' '}
<span className='font-medium'>{numberWithCommas(total)}</span> sessions.
Showing <span className="font-medium">{(currentPage - 1) * pageSize + 1}</span> to{' '}
<span className="font-medium">{(currentPage - 1) * pageSize + list.length}</span> of{' '}
<span className="font-medium">{numberWithCommas(total)}</span> sessions.
</div>
<Pagination
page={currentPage}
total={total}
onPageChange={(page) => props.updateCurrentPage(page)}
onPageChange={(page) => searchStore.updateCurrentPage(page)}
limit={pageSize}
debounceRequest={1000}
/>
@ -283,26 +271,16 @@ function SessionList(props: Props) {
export default connect(
(state: any) => ({
list: state.getIn(['sessions', 'list']),
filters: state.getIn(['search', 'instance', 'filters']),
lastPlayedSessionId: state.getIn(['sessions', 'lastPlayedSessionId']),
metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key),
loading: state.getIn(['sessions', 'loading']),
currentPage: state.getIn(['search', 'currentPage']) || 1,
total: state.getIn(['sessions', 'total']) || 0,
scrollY: state.getIn(['search', 'scrollY']),
activeTab: state.getIn(['search', 'activeTab']),
pageSize: state.getIn(['search', 'pageSize']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
siteId: state.getIn(['site', 'siteId']),
sites: state.getIn(['site', 'list']),
isLoggedIn: Boolean(state.getIn(['user', 'jwt'])),
isLoggedIn: Boolean(state.getIn(['user', 'jwt']))
}),
{
updateCurrentPage,
addFilterByKeyAndValue,
setScrollPosition,
fetchSessions,
checkForLatestSessions,
toggleFavorite,
updateProjectRecordingStatus
}

View file

@ -2,27 +2,25 @@ import { DownOutlined } from '@ant-design/icons';
import { Dropdown } from 'antd';
import React from 'react';
import { connect } from 'react-redux';
import { applyFilter } from 'Duck/search';
import { sort } from 'Duck/sessions';
import { useStore } from 'App/mstore';
const sortOptionsMap = {
'startTs-desc': 'Newest',
'startTs-asc': 'Oldest',
'eventsCount-asc': 'Events Ascending',
'eventsCount-desc': 'Events Descending',
'eventsCount-desc': 'Events Descending'
};
const sortOptions = Object.entries(sortOptionsMap).map(([value, label]) => ({
// value,
label,
key: value,
key: value
}));
interface Props {
filter: any;
options?: any;
applyFilter: (filter: any) => void;
sort: (sort: string, sign: number) => void;
}
@ -39,7 +37,7 @@ export function SortDropdown<T>({ defaultOption, onSort, sortOptions, current }:
items: sortOptions,
defaultSelectedKeys: defaultOption ? [defaultOption] : undefined,
// @ts-ignore
onClick: onSort,
onClick: onSort
}}
>
<div
@ -51,15 +49,16 @@ export function SortDropdown<T>({ defaultOption, onSort, sortOptions, current }:
<DownOutlined />
</div>
</Dropdown>
)
);
}
function SessionSort(props: Props) {
const { sort, order } = props.filter;
const { searchStore } = useStore();
const { sort, order } = searchStore.instance;
const onSort = ({ key }: { key: string }) => {
const [sort, order] = key.split('-');
const sign = order === 'desc' ? -1 : 1;
props.applyFilter({ order, sort });
searchStore.applyFilter({ order, sort });
props.sort(sort, sign);
};
@ -77,7 +76,7 @@ function SessionSort(props: Props) {
export default connect(
(state: any) => ({
filter: state.getIn(['search', 'instance']),
// filter: state.getIn(['search', 'instance'])
}),
{ sort, applyFilter }
{ sort }
)(SessionSort);

View file

@ -4,10 +4,8 @@ import cn from 'classnames';
import { Angry, CircleAlert, Skull, WifiOff } from 'lucide-react';
import React, { memo } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { setActiveTab } from 'Duck/search';
import { Icon } from 'UI';
import { useStore } from 'App/mstore';
interface Tag {
name: string;
@ -16,28 +14,25 @@ interface Tag {
}
interface StateProps {
activeTab: { type: string };
tags: Tag[];
total: number;
}
interface DispatchProps {
setActiveTab: typeof setActiveTab;
}
type Props = StateProps & DispatchProps;
type Props = StateProps;
const tagIcons = {
[types.ALL]: undefined,
[types.JS_EXCEPTION]: <CircleAlert size={14} />,
[types.BAD_REQUEST]: <WifiOff size={14} />,
[types.CLICK_RAGE]: <Angry size={14} />,
[types.CRASH]: <Skull size={14} />,
} as Record<string, any>
[types.CRASH]: <Skull size={14} />
} as Record<string, any>;
const SessionTags: React.FC<Props> = memo(
({ activeTab, tags, total, setActiveTab }) => {
const disable = activeTab.type === 'all' && total === 0;
({ tags, total }) => {
const { searchStore } = useStore();
const disable = searchStore.activeTab.type === 'all' && total === 0;
const activeTab = searchStore.activeTab;
const options = tags.map((tag, i) => ({
label: (
@ -60,13 +55,13 @@ const SessionTags: React.FC<Props> = memo(
</div>
),
value: tag.type,
disabled: disable && tag.type !== 'all',
disabled: disable && tag.type !== 'all'
}));
const onPick = (tabValue: string) => {
const tab = tags.find((t) => t.type === tabValue);
if (tab) {
setActiveTab(tab);
searchStore.setActiveTab(tab);
}
};
return (
@ -96,7 +91,7 @@ export const TagItem: React.FC<{
'transition group rounded ml-2 px-2 py-1 flex items-center uppercase text-sm hover:bg-active-blue hover:text-teal',
{
'bg-active-blue text-teal': isActive,
disabled: disabled,
disabled: disabled
}
)}
style={{ height: '36px' }}
@ -115,7 +110,6 @@ export const TagItem: React.FC<{
const mapStateToProps = (state: any): StateProps => {
const platform = state.getIn(['site', 'active'])?.platform || '';
const activeTab = state.getIn(['search', 'activeTab']);
const filteredTags = issues_types.filter(
(tag) =>
tag.type !== 'mouse_thrashing' &&
@ -125,15 +119,7 @@ const mapStateToProps = (state: any): StateProps => {
);
const total = state.getIn(['sessions', 'total']) || 0;
return { activeTab, tags: filteredTags, total };
return { tags: filteredTags, total };
};
const mapDispatchToProps = (dispatch: any): DispatchProps =>
bindActionCreators(
{
setActiveTab,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(SessionTags);
export default connect(mapStateToProps)(SessionTags);

View file

@ -14,10 +14,9 @@ import {
fflags,
notes,
sessions,
withSiteId,
withSiteId
} from 'App/routes';
import { MODULES } from 'Components/Client/Modules';
import { setActiveTab } from 'Duck/search';
import { Icon } from 'UI';
import SVG from 'UI/SVG';
@ -29,8 +28,9 @@ import {
PREFERENCES_MENU,
categories as main_menu,
preferences,
spotOnlyCats,
spotOnlyCats
} from './data';
import { useStore } from 'App/mstore';
const { Text } = Typography;
@ -38,14 +38,12 @@ const TabToUrlMap = {
all: sessions() as '/sessions',
bookmark: bookmarks() as '/bookmarks',
notes: notes() as '/notes',
flags: fflags() as '/feature-flags',
flags: fflags() as '/feature-flags'
};
interface Props extends RouteComponentProps {
siteId?: string;
modules: string[];
setActiveTab: (tab: any) => void;
activeTab: string;
isEnterprise: boolean;
isCollapsed?: boolean;
spotOnly?: boolean;
@ -54,18 +52,18 @@ interface Props extends RouteComponentProps {
function SideMenu(props: Props) {
const {
activeTab,
siteId,
modules,
location,
account,
isEnterprise,
isCollapsed,
spotOnly,
spotOnly
} = props;
const isPreferencesActive = location.pathname.includes('/client/');
const [supportOpen, setSupportOpen] = React.useState(false);
const isAdmin = account.admin || account.superAdmin;
const { searchStore } = useStore();
const [isModalVisible, setIsModalVisible] = React.useState(false);
@ -117,18 +115,18 @@ function SideMenu(props: Props) {
const isHidden = [
item.key === MENU.RECOMMENDATIONS &&
modules.includes(MODULES.RECOMMENDATIONS),
modules.includes(MODULES.RECOMMENDATIONS),
item.key === MENU.FEATURE_FLAGS &&
modules.includes(MODULES.FEATURE_FLAGS),
modules.includes(MODULES.FEATURE_FLAGS),
item.key === MENU.NOTES && modules.includes(MODULES.NOTES),
item.key === MENU.LIVE_SESSIONS &&
modules.includes(MODULES.ASSIST),
modules.includes(MODULES.ASSIST),
item.key === MENU.SESSIONS &&
modules.includes(MODULES.OFFLINE_RECORDINGS),
modules.includes(MODULES.OFFLINE_RECORDINGS),
item.key === MENU.ALERTS && modules.includes(MODULES.ALERTS),
item.key === MENU.USABILITY_TESTS && modules.includes(MODULES.USABILITY_TESTS),
item.isAdmin && !isAdmin,
item.isEnterprise && !isEnterprise,
item.isEnterprise && !isEnterprise
].some((cond) => cond);
return { ...item, hidden: isHidden };
@ -139,7 +137,7 @@ function SideMenu(props: Props) {
return {
...category,
items: updatedItems,
hidden: allItemsHidden,
hidden: allItemsHidden
};
});
}, [isAdmin, isEnterprise, isPreferencesActive, modules, spotOnly]);
@ -149,8 +147,8 @@ function SideMenu(props: Props) {
const tab = Object.keys(TabToUrlMap).find((tab: keyof typeof TabToUrlMap) =>
currentLocation.includes(TabToUrlMap[tab])
);
if (tab && tab !== activeTab) {
props.setActiveTab({ type: tab });
if (tab && tab !== searchStore.activeTab) {
searchStore.setActiveTab({ type: tab });
}
}, [location.pathname]);
@ -181,7 +179,7 @@ function SideMenu(props: Props) {
[PREFERENCES_MENU.TEAM]: () => client(CLIENT_TABS.MANAGE_USERS),
[PREFERENCES_MENU.NOTIFICATIONS]: () => client(CLIENT_TABS.NOTIFICATIONS),
[PREFERENCES_MENU.BILLING]: () => client(CLIENT_TABS.BILLING),
[PREFERENCES_MENU.MODULES]: () => client(CLIENT_TABS.MODULES),
[PREFERENCES_MENU.MODULES]: () => client(CLIENT_TABS.MODULES)
};
const handleClick = (item: any) => {
@ -211,10 +209,10 @@ function SideMenu(props: Props) {
props.history.push(path);
};
const RenderDivider = (props: {index: number}) => {
const RenderDivider = (props: { index: number }) => {
if (props.index === 0) return null;
return <Divider style={{ margin: '6px 0' }} />;
}
};
return (
<>
<Menu
@ -284,7 +282,7 @@ function SideMenu(props: Props) {
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
alignItems: 'center'
}}
>
{item.label}
@ -315,7 +313,7 @@ function SideMenu(props: Props) {
<Menu.Item
className={cn('ml-8', {
'ant-menu-item-selected !bg-active-dark-blue':
isMenuItemActive(child.key),
isMenuItemActive(child.key)
})}
key={child.key}
>
@ -373,11 +371,9 @@ export default withRouter(
connect(
(state: any) => ({
modules: state.getIn(['user', 'account', 'settings', 'modules']) || [],
activeTab: state.getIn(['search', 'activeTab', 'type']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
account: state.getIn(['user', 'account']),
spotOnly: getScope(state) === 1,
}),
{ setActiveTab }
spotOnly: getScope(state) === 1
})
)(SideMenu)
);

View file

@ -27,6 +27,7 @@ import FilterStore from './filterStore';
import UiPlayerStore from './uiPlayerStore';
import IssueReportingStore from './issueReportingStore';
import CustomFieldStore from './customFieldStore';
import SearchStore from './searchStore';
export class RootStore {
dashboardStore: DashboardStore;
@ -55,6 +56,7 @@ export class RootStore {
uiPlayerStore: UiPlayerStore;
issueReportingStore: IssueReportingStore;
customFieldStore: CustomFieldStore;
searchStore: SearchStore;
constructor() {
this.dashboardStore = new DashboardStore();
@ -83,6 +85,7 @@ export class RootStore {
this.uiPlayerStore = new UiPlayerStore();
this.issueReportingStore = new IssueReportingStore();
this.customFieldStore = new CustomFieldStore();
this.searchStore = new SearchStore();
}
initClient() {

View file

@ -0,0 +1,238 @@
import Period, { CUSTOM_RANGE } from 'Types/app/period';
import { FilterCategory, FilterKey } from 'Types/filter/filterType';
import {
conditionalFiltersMap,
filtersMap,
generateFilterOptions,
liveFiltersMap,
mobileConditionalFiltersMap
} from 'Types/filter/newFilter';
import { List } from 'immutable';
import { makeAutoObservable, action } from 'mobx';
import { searchService } from 'App/services';
import Search from 'App/mstore/types/search';
import Filter, { checkFilterValue } from 'App/mstore/types/filter';
import FilterItem from 'MOBX/types/filterItem';
const PER_PAGE = 10;
export const checkValues = (key: any, value: any) => {
if (key === FilterKey.DURATION) {
return value[0] === '' || value[0] === null ? [0, value[1]] : value;
}
return value.filter((i: any) => i !== '' && i !== null);
};
export const filterMap = ({
category,
value,
key,
operator,
sourceOperator,
source,
custom,
isEvent,
filters,
sort,
order
}: any) => ({
value: checkValues(key, value),
custom,
type: category === FilterCategory.METADATA ? FilterKey.METADATA : key,
operator,
source: category === FilterCategory.METADATA ? key.replace(/^_/, '') : source,
sourceOperator,
isEvent,
filters: filters ? filters.map(filterMap) : []
});
class SearchStore {
filterList = generateFilterOptions(filtersMap);
filterListLive = generateFilterOptions(liveFiltersMap);
filterListConditional = generateFilterOptions(conditionalFiltersMap);
filterListMobileConditional = generateFilterOptions(mobileConditionalFiltersMap);
list = List();
latestRequestTime: number | null = null;
latestList = List();
alertMetricId: number | null = null;
instance = new Search();
savedSearch = new Search();
filterSearchList: any = {};
currentPage = 1;
pageSize = PER_PAGE;
activeTab = { name: 'All', type: 'all' };
scrollY = 0;
constructor() {
makeAutoObservable(this);
}
applySavedSearch(savedSearch: any) {
this.savedSearch = savedSearch;
this.instance = new Search(savedSearch.filter);
this.currentPage = 1;
}
editSavedSearch(savedSearch: any) {
this.savedSearch = savedSearch;
}
async fetchList() {
const response = await searchService.fetchSavedSearch();
this.list = List(response.map((item: any) => new Search(item)));
}
edit(instance: any) {
this.instance = instance;
this.currentPage = 1;
}
apply(filter: any, fromUrl: boolean) {
if (fromUrl) {
this.instance = new Search(filter);
this.currentPage = 1;
} else {
this.instance = { ...this.instance, ...filter };
}
}
applyFilter(filter: any, force = false) {
this.apply(filter, false);
}
fetchSessions(force = false) {
const filter = this.instance.toData();
if (this.activeTab === 'bookmark' || this.activeTab === 'vault') {
filter.bookmarked = true;
}
filter.filters = filter.filters.map(filterMap);
filter.limit = this.pageSize;
filter.page = this.currentPage;
// Further logic based on force, dispatching actions, etc.
}
fetchFilterSearch(params: any) {
searchService.fetchFilterSearch(params).then((response: any) => {
this.filterSearchList = response.reduce((acc: any, item: any) => {
const { projectId, type, value } = item;
const key = type;
if (!acc[key]) acc[key] = [];
acc[key].push({ projectId, value });
return acc;
}, {});
});
}
updateCurrentPage(page: number) {
this.currentPage = page;
this.fetchSessions();
}
setActiveTab(tab: any) {
this.activeTab = tab;
this.currentPage = 1;
this.fetchSessions();
}
async remove(id: string): Promise<void> {
await searchService.deleteSavedSearch(id);
this.savedSearch = new Search({});
await this.fetchList();
}
async save(id: string, rename = false): Promise<void> {
const filter = this.instance.toData();
const isNew = !id;
const instance = this.savedSearch.toData();
const newInstance = rename ? instance : { ...instance, filter };
newInstance.filter.filters = newInstance.filter.filters.map(filterMap);
await searchService.saveSavedSearch(newInstance, id);
await this.fetchList();
if (isNew) {
const lastSavedSearch = this.list.last();
this.applySavedSearch(lastSavedSearch);
}
}
clearSearch() {
const instance = this.instance;
this.edit(new Search({
rangeValue: instance.rangeValue,
startDate: instance.startDate,
endDate: instance.endDate,
filters: []
}));
}
checkForLatestSessions() {
const filter = this.instance.toData();
if (this.latestRequestTime) {
const period = Period({ rangeName: CUSTOM_RANGE, start: this.latestRequestTime, end: Date.now() });
const newTimestamps: any = period.toJSON();
filter.startTimestamp = newTimestamps.startDate;
filter.endTimestamp = newTimestamps.endDate;
}
searchService.checkLatestSessions(filter).then((response: any) => {
this.latestList = response;
});
}
addFilter(filter: any) {
const index = this.instance.filters.findIndex((i: FilterItem) => i.key === filter.key);
filter.value = checkFilterValue(filter.value);
filter.filters = filter.filters
? filter.filters.map((subFilter: any) => ({
...subFilter,
value: checkFilterValue(subFilter.value)
}))
: null;
if (index > -1) {
const oldFilter = this.instance.filters[index];
const updatedFilter = {
...oldFilter,
value: oldFilter.value.concat(filter.value)
};
oldFilter.merge(updatedFilter);
} else {
this.instance.filters.push(filter);
}
}
addFilterByKeyAndValue(key: any, value: any, operator?: string, sourceOperator?: string, source?: string) {
let defaultFilter = { ...filtersMap[key] };
defaultFilter.value = value;
if (operator) {
defaultFilter.operator = operator;
}
if (defaultFilter.hasSource && source && sourceOperator) {
defaultFilter.sourceOperator = sourceOperator;
defaultFilter.source = source;
}
this.addFilter(defaultFilter);
}
refreshFilterOptions() {
// TODO
}
updateFilter = (index: number, search: Partial<Search>) => {
Object.assign(this.instance!, search);
};
setScrollPosition = (y: number) => {
// TODO
};
async fetchAutoplaySessions(page: number): Promise<void> {
// TODO
}
}
export default SearchStore;

View file

@ -16,9 +16,9 @@ import {
getSessionFilter,
setSessionFilter,
} from 'App/utils';
import { filterMap } from 'Duck/search';
import { loadFile } from '../player/web/network/loadFiles';
import { filterMap } from 'App/mstore/searchStore';
class UserFilter {
endDate: number = new Date().getTime();

View file

@ -1,134 +1,226 @@
import {makeAutoObservable, runInAction, observable, action} from "mobx"
import FilterItem from "./filterItem"
import {filtersMap, conditionalFiltersMap} from 'Types/filter/newFilter';
import {FilterKey} from "Types/filter/filterType";
import { makeAutoObservable, runInAction, observable, action } from 'mobx';
import FilterItem from './filterItem';
import { filtersMap, conditionalFiltersMap } from 'Types/filter/newFilter';
import { FilterKey } from 'Types/filter/filterType';
export default class Filter {
public static get ID_KEY(): string {
return "filterId"
}
export const checkFilterValue = (value: any) => {
return Array.isArray(value) ? (value.length === 0 ? [''] : value) : [value];
};
filterId: string = ''
name: string = ''
filters: FilterItem[] = []
excludes: FilterItem[] = []
eventsOrder: string = 'then'
eventsOrderSupport: string[] = ['then', 'or', 'and']
startTimestamp: number = 0
endTimestamp: number = 0
eventsHeader: string = "EVENTS"
page: number = 1
limit: number = 10
export interface IFilter {
filterId: string;
name: string;
filters: FilterItem[];
excludes: FilterItem[];
eventsOrder: string;
eventsOrderSupport: string[];
startTimestamp: number;
endTimestamp: number;
eventsHeader: string;
page: number;
limit: number;
constructor(private readonly isConditional = false, private readonly isMobile = false) {
makeAutoObservable(this, {
filters: observable,
eventsOrder: observable,
startTimestamp: observable,
endTimestamp: observable,
merge(filter: any): void;
addFilter: action,
removeFilter: action,
updateKey: action,
merge: action,
addExcludeFilter: action,
updateFilter: action,
replaceFilters: action,
})
}
addFilter(filter: any): void;
merge(filter: any) {
runInAction(() => {
Object.assign(this, filter)
})
}
replaceFilters(filters: any): void;
addFilter(filter: any) {
filter.value = [""]
if (Array.isArray(filter.filters)) {
filter.filters = filter.filters.map((i: Record<string, any>) => {
i.value = [""]
return new FilterItem(i)
})
}
this.filters.push(new FilterItem(filter))
}
updateFilter(index: number, filter: any): void;
replaceFilters(filters: any) {
this.filters = filters;
}
updateKey(key: string, value: any): void;
updateFilter(index: number, filter: any) {
this.filters[index] = new FilterItem(filter)
}
removeFilter(index: number): void;
updateKey(key: string, value: any) {
// @ts-ignore fix later
this[key] = value
}
fromJson(json: any, isHeatmap?: boolean): IFilter;
removeFilter(index: number) {
this.filters.splice(index, 1)
}
fromData(data: any): IFilter;
fromJson(json: any, isHeatmap?: boolean) {
this.name = json.name
this.filters = json.filters.map((i: Record<string, any>) =>
new FilterItem(undefined, this.isConditional, this.isMobile).fromJson(i, undefined, isHeatmap)
);
this.eventsOrder = json.eventsOrder
return this
}
toJsonDrilldown(): any;
fromData(data) {
this.name = data.name
this.filters = data.filters.map((i: Record<string, any>) =>
new FilterItem(undefined, this.isConditional, this.isMobile).fromData(i)
)
this.eventsOrder = data.eventsOrder
return this
}
createFilterBykey(key: string): FilterItem;
toJsonDrilldown() {
const json = {
name: this.name,
filters: this.filters.map(i => i.toJson()),
eventsOrder: this.eventsOrder,
startTimestamp: this.startTimestamp,
endTimestamp: this.endTimestamp,
}
return json
}
toJson(): any;
createFilterBykey(key: string) {
const usedMap = this.isConditional ? conditionalFiltersMap : filtersMap
return usedMap[key] ? new FilterItem(usedMap[key]) : new FilterItem()
}
addExcludeFilter(filter: FilterItem): void;
toJson() {
const json = {
name: this.name,
filters: this.filters.map(i => i.toJson()),
eventsOrder: this.eventsOrder,
}
return json
}
updateExcludeFilter(index: number, filter: FilterItem): void;
addExcludeFilter(filter: FilterItem) {
this.excludes.push(filter)
}
removeExcludeFilter(index: number): void;
updateExcludeFilter(index: number, filter: FilterItem) {
this.excludes[index] = new FilterItem(filter)
}
addFunnelDefaultFilters(): void;
removeExcludeFilter(index: number) {
this.excludes.splice(index, 1)
}
toData(): any;
addFunnelDefaultFilters() {
this.filters = []
this.addFilter({...filtersMap[FilterKey.LOCATION], value: [''], operator: 'isAny'})
this.addFilter({...filtersMap[FilterKey.CLICK], value: [''], operator: 'onAny'})
}
addOrUpdateFilter(filter: any): void;
}
export default class Filter implements IFilter {
public static get ID_KEY(): string {
return 'filterId';
}
filterId: string = '';
name: string = '';
filters: FilterItem[] = [];
excludes: FilterItem[] = [];
eventsOrder: string = 'then';
eventsOrderSupport: string[] = ['then', 'or', 'and'];
startTimestamp: number = 0;
endTimestamp: number = 0;
eventsHeader: string = 'EVENTS';
page: number = 1;
limit: number = 10;
constructor(
filters: any[] = [],
private readonly isConditional = false,
private readonly isMobile = false) {
makeAutoObservable(this, {
filters: observable,
eventsOrder: observable,
startTimestamp: observable,
endTimestamp: observable,
addFilter: action,
removeFilter: action,
updateKey: action,
merge: action,
addExcludeFilter: action,
updateFilter: action,
replaceFilters: action
});
this.filters = filters.map(i => new FilterItem(i));
}
merge(filter: any) {
runInAction(() => {
Object.assign(this, filter);
});
}
addFilter(filter: any) {
filter.value = [''];
if (Array.isArray(filter.filters)) {
filter.filters = filter.filters.map((i: Record<string, any>) => {
i.value = [''];
return new FilterItem(i);
});
}
this.filters.push(new FilterItem(filter));
}
replaceFilters(filters: any) {
this.filters = filters;
}
updateFilter(index: number, filter: any) {
this.filters[index] = new FilterItem(filter);
}
updateKey(key: string, value: any) {
// @ts-ignore fix later
this[key] = value;
}
removeFilter(index: number) {
this.filters.splice(index, 1);
}
fromJson(json: any, isHeatmap?: boolean) {
this.name = json.name;
this.filters = json.filters.map((i: Record<string, any>) =>
new FilterItem(undefined, this.isConditional, this.isMobile).fromJson(i, undefined, isHeatmap)
);
this.eventsOrder = json.eventsOrder;
return this;
}
fromData(data: any) {
this.name = data.name;
this.filters = data.filters.map((i: Record<string, any>) =>
new FilterItem(undefined, this.isConditional, this.isMobile).fromData(i)
);
this.eventsOrder = data.eventsOrder;
return this;
}
toJsonDrilldown() {
const json = {
name: this.name,
filters: this.filters.map(i => i.toJson()),
eventsOrder: this.eventsOrder,
startTimestamp: this.startTimestamp,
endTimestamp: this.endTimestamp
};
return json;
}
createFilterBykey(key: string) {
const usedMap = this.isConditional ? conditionalFiltersMap : filtersMap;
return usedMap[key] ? new FilterItem(usedMap[key]) : new FilterItem();
}
toJson() {
const json = {
name: this.name,
filters: this.filters.map(i => i.toJson()),
eventsOrder: this.eventsOrder
};
return json;
}
addExcludeFilter(filter: FilterItem) {
this.excludes.push(filter);
}
updateExcludeFilter(index: number, filter: FilterItem) {
this.excludes[index] = new FilterItem(filter);
}
removeExcludeFilter(index: number) {
this.excludes.splice(index, 1);
}
addFunnelDefaultFilters() {
this.filters = [];
this.addFilter({ ...filtersMap[FilterKey.LOCATION], value: [''], operator: 'isAny' });
this.addFilter({ ...filtersMap[FilterKey.CLICK], value: [''], operator: 'onAny' });
}
toData() {
return {
name: this.name,
filters: this.filters.map(i => i.toJson()),
eventsOrder: this.eventsOrder
};
}
addOrUpdateFilter(filter: any) {
const index = this.filters.findIndex(i => i.key === filter.key);
filter.value = checkFilterValue;
if (index > -1) {
this.updateFilter(index, filter);
} else {
this.addFilter(filter);
}
}
addFilterByKeyAndValue(key: any, value: any, operator: undefined, sourceOperator: undefined, source: undefined) {
let defaultFilter = { ...filtersMap[key] };
if (defaultFilter) {
defaultFilter = { ...defaultFilter, value: checkFilterValue(value) };
if (operator) {
defaultFilter.operator = operator;
}
if (sourceOperator) {
defaultFilter.sourceOperator = sourceOperator;
}
if (source) {
defaultFilter.source = source;
}
this.addOrUpdateFilter(defaultFilter);
}
}
}

View file

@ -32,19 +32,7 @@ export default class FilterItem {
private readonly isConditional?: boolean,
private readonly isMobile?: boolean
) {
makeAutoObservable(this, {
type: observable,
key: observable,
value: observable,
operator: observable,
source: observable,
filters: observable,
isActive: observable,
sourceOperator: observable,
category: observable,
merge: action,
});
makeAutoObservable(this);
if (Array.isArray(data.filters)) {
data.filters = data.filters.map(function (i: Record<string, any>) {

View file

@ -0,0 +1,70 @@
import Filter from './filter';
import { notEmptyString } from 'App/validate';
interface FilterType {
filters: Array<{ value: any }>;
}
interface ISavedSearch {
searchId?: string;
projectId?: string;
userId?: string;
name: string;
filter: FilterType;
createdAt?: string;
count: number;
isPublic: boolean;
}
class SavedSearch {
searchId?: string;
projectId?: string;
userId?: string;
name: string;
filter: FilterType;
createdAt?: string;
count: number;
isPublic: boolean;
constructor({
searchId,
projectId,
userId,
name = '',
filter = new Filter(),
createdAt,
count = 0,
isPublic = false
}: Partial<ISavedSearch> = {}) {
this.searchId = searchId;
this.projectId = projectId;
this.userId = userId;
this.name = name;
this.filter = filter;
this.createdAt = createdAt;
this.count = count;
this.isPublic = isPublic;
}
validate(): boolean {
return notEmptyString(this.name);
}
toData() {
const js = { ...this };
js.filter.filters = js.filter.filters.map(f => ({
...f,
value: Array.isArray(f.value) ? f.value : [f.value]
}));
return js;
}
static fromJS(data: Partial<ISavedSearch>): SavedSearch {
return new SavedSearch({
...data,
filter: new Filter().fromJson(data.filter)
});
}
}
export default SavedSearch;

View file

@ -0,0 +1,161 @@
import { DATE_RANGE_VALUES, CUSTOM_RANGE, getDateRangeFromValue } from 'App/dateRange';
import Filter, { IFilter } from 'App/mstore/types/filter';
import FilterItem from 'MOBX/types/filterItem';
// @ts-ignore
const rangeValue = DATE_RANGE_VALUES.LAST_24_HOURS;
const range: any = getDateRangeFromValue(rangeValue);
const startDate = range.start.ts;
const endDate = range.end.ts;
interface ISearch {
name: string;
searchId?: number;
referrer?: string;
userBrowser?: string;
userOs?: string;
userCountry?: string;
userDevice?: string;
fid0?: string;
events: Event[];
filters: IFilter[];
minDuration?: number;
maxDuration?: number;
custom: Record<string, any>;
rangeValue: string;
startDate: number;
endDate: number;
groupByUser: boolean;
sort: string;
order: string;
viewed?: boolean;
consoleLogCount?: number;
eventsCount?: number;
suspicious?: boolean;
consoleLevel?: string;
strict: boolean;
eventsOrder: string;
}
export default class Search {
name: string;
searchId?: number;
referrer?: string;
userBrowser?: string;
userOs?: string;
userCountry?: string;
userDevice?: string;
fid0?: string;
events: Event[];
filters: FilterItem[];
minDuration?: number;
maxDuration?: number;
custom: Record<string, any>;
rangeValue: string;
startDate: number;
endDate: number;
groupByUser: boolean;
sort: string;
order: string;
viewed?: boolean;
consoleLogCount?: number;
eventsCount?: number;
suspicious?: boolean;
consoleLevel?: string;
strict: boolean;
eventsOrder: string;
constructor(initialData?: Partial<ISearch>) {
Object.assign(this, {
name: '',
searchId: undefined,
referrer: undefined,
userBrowser: undefined,
userOs: undefined,
userCountry: undefined,
userDevice: undefined,
fid0: undefined,
events: [],
filters: [],
minDuration: undefined,
maxDuration: undefined,
custom: {},
rangeValue,
startDate,
endDate,
groupByUser: false,
sort: 'startTs',
order: 'desc',
viewed: undefined,
consoleLogCount: undefined,
eventsCount: undefined,
suspicious: undefined,
consoleLevel: undefined,
strict: false,
eventsOrder: 'then',
...initialData
});
}
exists() {
return Boolean(this.searchId);
}
toSaveData() {
const js: any = { ...this };
js.filters = js.filters.map((filter: any) => {
filter.type = filter.key;
delete filter.category;
delete filter.icon;
delete filter.operatorOptions;
delete filter._key;
delete filter.key;
return filter;
});
delete js.createdAt;
delete js.key;
delete js._key;
return js;
}
toData() {
const js: any = { ...this };
js.filters = js.filters.map((filter: any) => {
return filter;
});
delete js.createdAt;
delete js.key;
return js;
}
static fromJS({ eventsOrder, filters, events, custom, ...filterData }: any) {
let startDate, endDate;
const rValue = filterData.rangeValue || rangeValue;
if (rValue !== CUSTOM_RANGE) {
const range: any = getDateRangeFromValue(rValue);
startDate = range.start.ts;
endDate = range.end.ts;
} else if (filterData.startDate && filterData.endDate) {
startDate = filterData.startDate;
endDate = filterData.endDate;
}
return new Search({
...filterData,
eventsOrder,
startDate,
endDate,
events: events.map((event: any) => new Event(event)),
filters: filters.map((i: any) => {
const filter = new Filter(i).toData();
if (Array.isArray(i.filters)) {
filter.filters = i.filters.map((f: any) => new Filter({ ...f, subFilter: i.type }).toData());
}
return filter;
})
});
}
}

View file

@ -0,0 +1,39 @@
import BaseService from 'App/services/BaseService';
export default class SearchService extends BaseService {
async fetchSavedSearchList() {
const r = await this.client.get('/PROJECT_ID/saved_search');
const j = await r.json();
return j.data;
}
async deleteSavedSearch(id: string) {
const r = await this.client.delete(`/saved_search/${id}`);
const j = await r.json();
return j.data;
}
async fetchFilterSearch(params: any) {
const r = await this.client.get('/PROJECT_ID/events/search', params);
const j = await r.json();
return j.data;
}
async saveSavedSearch(data: any, id: string) {
const r = await this.client.post(`/search/${id ? id : ''}`, data);
const j = await r.json();
return j.data;
}
async fetchSavedSearch() {
const r = await this.client.get('/PROJECT_ID/search');
const j = await r.json();
return j.data;
}
async checkLatestSessions(filter: any) {
const r = await this.client.post('/PROJECT_ID/search/check', filter);
const j = await r.json();
return j.data;
}
}

View file

@ -18,11 +18,12 @@ import UserService from './UserService';
import UxtestingService from './UxtestingService';
import WebhookService from './WebhookService';
import SpotService from './spotService';
import LoginService from "./loginService";
import FilterService from "./FilterService";
import IssueReportsService from "./IssueReportsService";
import LoginService from './loginService';
import FilterService from './FilterService';
import IssueReportsService from './IssueReportsService';
import CustomFieldService from './CustomFieldService';
import IntegrationsService from './IntegrationsService';
import SearchService from 'App/services/SearchService';
export const dashboardService = new DashboardService();
export const metricService = new MetricService();
@ -48,6 +49,7 @@ export const filterService = new FilterService();
export const issueReportsService = new IssueReportsService();
export const customFieldService = new CustomFieldService();
export const integrationsService = new IntegrationsService();
export const searchService = new SearchService();
export const services = [
dashboardService,
@ -74,4 +76,5 @@ export const services = [
issueReportsService,
customFieldService,
integrationsService,
searchService
];

View file

@ -4,7 +4,7 @@ import Filter from 'Types/filter';
import { validateName } from 'App/validate';
import { LAST_7_DAYS } from 'Types/app/period';
import { FilterKey } from 'Types/filter/filterType';
import { filterMap } from 'Duck/search';
import { filterMap } from 'App/mstore/searchStore';
export const FilterSeries = Record({
seriesId: undefined,
@ -50,7 +50,7 @@ export default Record({
const js = this.toJS();
js.metricValue = js.metricValue.map(value => value === 'all' ? '' : value);
js.series = js.series.map(series => {
series.filter.filters = series.filter.filters.map(filterMap);
// delete series._key