change(ui): duck/search wip
This commit is contained in:
parent
b9590f702e
commit
64a3eb7e89
33 changed files with 1104 additions and 612 deletions
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
238
frontend/app/mstore/searchStore.ts
Normal file
238
frontend/app/mstore/searchStore.ts
Normal 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;
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
70
frontend/app/mstore/types/savedSearch.ts
Normal file
70
frontend/app/mstore/types/savedSearch.ts
Normal 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;
|
||||
161
frontend/app/mstore/types/search.ts
Normal file
161
frontend/app/mstore/types/search.ts
Normal 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;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
39
frontend/app/services/SearchService.ts
Normal file
39
frontend/app/services/SearchService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue