From 64a3eb7e89987415d6f13bf48a0b421fa9a7c27c Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Thu, 19 Sep 2024 18:29:43 +0530 Subject: [PATCH] change(ui): duck/search wip --- .../components/Client/Sites/NewSiteForm.tsx | 8 +- .../components/Errors/Error/MainSection.js | 11 +- frontend/app/components/Overview/Overview.tsx | 4 +- .../Session_/QueueControls/QueueControls.tsx | 24 +- .../shared/Filters/FilterList/FilterList.tsx | 7 +- .../LiveSessionSearchField.tsx | 31 +- .../shared/MainSearchBar/MainSearchBar.tsx | 41 +-- .../MainSearchBar/components/TagList.tsx | 33 +- .../shared/OverviewMenu/OverviewMenu.tsx | 14 +- .../ProjectDropdown/ProjectDropdown.tsx | 24 +- .../SaveSearchModal/SaveSearchModal.tsx | 18 +- .../shared/SavedSearch/SavedSearch.tsx | 36 +- .../SavedSearchModal/SavedSearchModal.tsx | 179 +++++----- .../shared/SessionSearch/SessionSearch.tsx | 61 ++-- .../AiSessionSearchField.tsx | 71 ++-- .../SessionSearchField/SessionSearchField.tsx | 15 +- .../LatestSessionsMessage.tsx | 14 +- .../SessionHeader/SessionHeader.tsx | 45 +-- .../SessionList/SessionDateRange.tsx | 31 +- .../components/SessionList/SessionList.tsx | 82 ++--- .../components/SessionSort/SessionSort.tsx | 21 +- .../components/SessionTags/SessionTags.tsx | 40 +-- frontend/app/layout/SideMenu.tsx | 46 ++- frontend/app/mstore/index.tsx | 3 + frontend/app/mstore/searchStore.ts | 238 +++++++++++++ frontend/app/mstore/sessionStore.ts | 2 +- frontend/app/mstore/types/filter.ts | 320 +++++++++++------- frontend/app/mstore/types/filterItem.ts | 14 +- frontend/app/mstore/types/savedSearch.ts | 70 ++++ frontend/app/mstore/types/search.ts | 161 +++++++++ frontend/app/services/SearchService.ts | 39 +++ frontend/app/services/index.ts | 9 +- frontend/app/types/customMetric.js | 4 +- 33 files changed, 1104 insertions(+), 612 deletions(-) create mode 100644 frontend/app/mstore/searchStore.ts create mode 100644 frontend/app/mstore/types/savedSearch.ts create mode 100644 frontend/app/mstore/types/search.ts create mode 100644 frontend/app/services/SearchService.ts diff --git a/frontend/app/components/Client/Sites/NewSiteForm.tsx b/frontend/app/components/Client/Sites/NewSiteForm.tsx index 66a31948e..c4beec33b 100644 --- a/frontend/app/components/Client/Sites/NewSiteForm.tsx +++ b/frontend/app/components/Client/Sites/NewSiteForm.tsx @@ -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, }); diff --git a/frontend/app/components/Errors/Error/MainSection.js b/frontend/app/components/Errors/Error/MainSection.js index 621c9f2b3..d80c30a34 100644 --- a/frontend/app/components/Errors/Error/MainSection.js +++ b/frontend/app/components/Errors/Error/MainSection.js @@ -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) {
{Object.entries(tag)[0][0]} -
{' '} +
+ {' '}
{Object.entries(tag)[0][1]}
@@ -130,5 +129,5 @@ function MainSection(props) { } export default withRouter( - connect(null, { addFilterByKeyAndValue })(observer(MainSection)) + connect(null)(observer(MainSection)) ); diff --git a/frontend/app/components/Overview/Overview.tsx b/frontend/app/components/Overview/Overview.tsx index 0d73da1e1..8d4d61fd1 100644 --- a/frontend/app/components/Overview/Overview.tsx +++ b/frontend/app/components/Overview/Overview.tsx @@ -30,11 +30,11 @@ function Overview({ match: { params } }: IProps) { -
+
-
+
diff --git a/frontend/app/components/Session_/QueueControls/QueueControls.tsx b/frontend/app/components/Session_/QueueControls/QueueControls.tsx index a3ebb0557..975b0c6c3 100644 --- a/frontend/app/components/Session_/QueueControls/QueueControls.tsx +++ b/frontend/app/components/Session_/QueueControls/QueueControls.tsx @@ -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; } + 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 })} > 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; diff --git a/frontend/app/components/shared/LiveSessionSearchField/LiveSessionSearchField.tsx b/frontend/app/components/shared/LiveSessionSearchField/LiveSessionSearchField.tsx index cac367484..029fe3fcd 100644 --- a/frontend/app/components/shared/LiveSessionSearchField/LiveSessionSearchField.tsx +++ b/frontend/app/components/shared/LiveSessionSearchField/LiveSessionSearchField.tsx @@ -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 (
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 && (
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 (
@@ -44,10 +44,10 @@ const MainSearchBar = (props: Props) => { @@ -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)); diff --git a/frontend/app/components/shared/MainSearchBar/components/TagList.tsx b/frontend/app/components/shared/MainSearchBar/components/TagList.tsx index 983622a32..d48652eed 100644 --- a/frontend/app/components/shared/MainSearchBar/components/TagList.tsx +++ b/frontend/app/components/shared/MainSearchBar/components/TagList.tsx @@ -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(, { right: true, - width: 400, + width: 400 }); }; return ( -
- ), + ) })); if (isAdmin) { menuItems.unshift({ @@ -99,7 +98,7 @@ function ProjectDropdown(props: Props) {
- ), + ) }); } @@ -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)) ); diff --git a/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx b/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx index 2d3013688..b640fac1a 100644 --- a/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx +++ b/frontend/app/components/shared/SaveSearchModal/SaveSearchModal.tsx @@ -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; show: boolean; closeHandler: () => void; savedSearch: any; - remove: (filterId: number) => Promise; 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 ( @@ -88,7 +85,7 @@ function SaveSearchModal(props: Props) { />
props.edit({ isPublic: !savedSearch.isPublic })} + onClick={() => searchStore.edit({ isPublic: !savedSearch.isPublic })} > Team Visible @@ -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); diff --git a/frontend/app/components/shared/SavedSearch/SavedSearch.tsx b/frontend/app/components/shared/SavedSearch/SavedSearch.tsx index 5770d5e76..e7c08d40a 100644 --- a/frontend/app/components/shared/SavedSearch/SavedSearch.tsx +++ b/frontend/app/components/shared/SavedSearch/SavedSearch.tsx @@ -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 ( -
+
- { savedSearch.exists() && ( + {savedSearch.exists() && (
Viewing: @@ -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); diff --git a/frontend/app/components/shared/SavedSearch/components/SavedSearchModal/SavedSearchModal.tsx b/frontend/app/components/shared/SavedSearch/components/SavedSearchModal/SavedSearchModal.tsx index 68c278b59..b2fe069a9 100644 --- a/frontend/app/components/shared/SavedSearch/components/SavedSearchModal/SavedSearchModal.tsx +++ b/frontend/app/components/shared/SavedSearch/components/SavedSearchModal/SavedSearchModal.tsx @@ -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) => void; + title: string; + name: string; + onClick: (e: MouseEvent) => void; } + function TooltipIcon(props: ITooltipIcon) { - return ( -
props.onClick(e)}> - - {/* @ts-ignore */} - - -
- ); + return ( +
props.onClick(e)}> + + {/* @ts-ignore */} + + +
+ ); } interface Props { - list: List; - applySavedSearch: (item: SavedSearch) => void; - remove: (itemId: number) => void; - editSavedSearch: (item: SavedSearch) => void; + list: List; + 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) => { - 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) => { - 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) => { + 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) => { + 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 ( -
-
-

- Saved Search {props.list.size} -

-
- {props.list.size > 1 && ( -
- setFilterQuery(value)} - placeholder="Filter by name" - /> -
- )} -
- {shownItems.map((item) => ( -
onClick(item, e)} - > - -
-
{item.name}
- {item.isPublic && ( -
- -
Team
-
- )} -
-
-
- onEdit(item, e)} title="Rename" /> -
-
- onDelete(item, e)} title="Delete" /> -
-
-
- ))} -
- {showModal && setshowModal(false)} rename={true} />} + return ( +
+
+

+ Saved Search {props.list.size} +

+
+ {props.list.size > 1 && ( +
+ setFilterQuery(value)} + placeholder="Filter by name" + />
- ); + )} +
+ {shownItems.map((item) => ( +
onClick(item, e)} + > + +
+
{item.name}
+ {item.isPublic && ( +
+ +
Team
+
+ )} +
+
+
+ onEdit(item, e)} title="Rename" /> +
+
+ onDelete(item, e)} title="Delete" /> +
+
+
+ ))} +
+ {showModal && setshowModal(false)} rename={true} />} +
+ ); } -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); diff --git a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx index f4038f396..0e5d9faff 100644 --- a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx +++ b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx @@ -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)); diff --git a/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx b/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx index 7d5111f40..895f211c3 100644 --- a/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx +++ b/frontend/app/components/shared/SessionSearchField/AiSessionSearchField.tsx @@ -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 (
{ changeValue('ask'); closeTour(); - }, - }, - }, + } + } + } ]} />
@@ -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' }} >
Ask AI
@@ -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)); diff --git a/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx index 0f0520295..9c892c91a 100644 --- a/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx +++ b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx @@ -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)); diff --git a/frontend/app/components/shared/SessionsTabOverview/components/LatestSessionsMessage/LatestSessionsMessage.tsx b/frontend/app/components/shared/SessionsTabOverview/components/LatestSessionsMessage/LatestSessionsMessage.tsx index 39ed264bf..b96618784 100644 --- a/frontend/app/components/shared/SessionsTabOverview/components/LatestSessionsMessage/LatestSessionsMessage.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/components/LatestSessionsMessage/LatestSessionsMessage.tsx @@ -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 ? (
props.updateCurrentPage(1)} + onClick={() => searchStore.updateCurrentPage(1)} > Show {numberWithCommas(count)} New {count > 1 ? 'Sessions' : 'Session'}
@@ -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); diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/SessionHeader.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/SessionHeader.tsx index 00771d47f..8e1ca4de4 100644 --- a/frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/SessionHeader.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/SessionHeader.tsx @@ -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 ( -
-

{title}

- {activeTab !== 'notes' ? ( -
- {activeTab !== 'bookmark' && ( +
+

{title}

+ {activeTab.type !== 'notes' ? ( +
+ {activeTab.type !== 'bookmark' && ( <> -
+
@@ -61,8 +53,8 @@ function SessionHeader(props: Props) {
) : null} - {activeTab === 'notes' && ( -
+ {activeTab.type === 'notes' && ( +
)} @@ -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); diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionDateRange.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionDateRange.tsx index cc266613a..2bdba260a 100644 --- a/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionDateRange.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionDateRange.tsx @@ -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 (
No sessions {isCustom ? 'between' : 'in the'} - +
); } -export default connect( - (state: any) => ({ - filter: state.getIn(['search', 'instance']), - }), - { applyFilter } -)(SessionDateRange); +export default observer(SessionDateRange); diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx index 99fca152c..2bdb91347 100644 --- a/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx @@ -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; sites: object[]; isLoggedIn: boolean; @@ -62,21 +51,20 @@ interface Props extends RouteComponentProps { function SessionList(props: Props) { const [noContentType, setNoContentType] = React.useState(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) { <> - +
+ -
-
- {NO_CONTENT.message } +
+
+ {NO_CONTENT.message}
} subtext={ -
+
{(isVault || isBookmark) && (
{isVault @@ -230,11 +218,11 @@ function SessionList(props: Props) {
)} @@ -243,7 +231,7 @@ function SessionList(props: Props) { show={!loading && list.length === 0} > {list.map((session: any) => ( -
+
{total > 0 && ( -
+
- Showing {(currentPage - 1) * pageSize + 1} to{' '} - {(currentPage - 1) * pageSize + list.length} of{' '} - {numberWithCommas(total)} sessions. + Showing {(currentPage - 1) * pageSize + 1} to{' '} + {(currentPage - 1) * pageSize + list.length} of{' '} + {numberWithCommas(total)} sessions.
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 } diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/SessionSort.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/SessionSort.tsx index 57a258660..acb5d5ce4 100644 --- a/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/SessionSort.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/SessionSort.tsx @@ -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({ defaultOption, onSort, sortOptions, current }: items: sortOptions, defaultSelectedKeys: defaultOption ? [defaultOption] : undefined, // @ts-ignore - onClick: onSort, + onClick: onSort }} >
({ defaultOption, onSort, sortOptions, current }:
- ) + ); } 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); diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx index 641f09657..2d44f6b8c 100644 --- a/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx @@ -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]: , [types.BAD_REQUEST]: , [types.CLICK_RAGE]: , - [types.CRASH]: , -} as Record + [types.CRASH]: +} as Record; const SessionTags: React.FC = 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 = memo(
), 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); diff --git a/frontend/app/layout/SideMenu.tsx b/frontend/app/layout/SideMenu.tsx index 8a86fda75..a93f2e6ab 100644 --- a/frontend/app/layout/SideMenu.tsx +++ b/frontend/app/layout/SideMenu.tsx @@ -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 ; - } + }; return ( <> {item.label} @@ -315,7 +313,7 @@ function SideMenu(props: Props) { @@ -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) ); diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index 723655adb..179ded805 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -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() { diff --git a/frontend/app/mstore/searchStore.ts b/frontend/app/mstore/searchStore.ts new file mode 100644 index 000000000..212c6039d --- /dev/null +++ b/frontend/app/mstore/searchStore.ts @@ -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 { + await searchService.deleteSavedSearch(id); + this.savedSearch = new Search({}); + await this.fetchList(); + } + + async save(id: string, rename = false): Promise { + 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) => { + Object.assign(this.instance!, search); + }; + + setScrollPosition = (y: number) => { + // TODO + }; + + async fetchAutoplaySessions(page: number): Promise { + // TODO + } +} + +export default SearchStore; diff --git a/frontend/app/mstore/sessionStore.ts b/frontend/app/mstore/sessionStore.ts index 6d2ac4e1b..12a7bee45 100644 --- a/frontend/app/mstore/sessionStore.ts +++ b/frontend/app/mstore/sessionStore.ts @@ -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(); diff --git a/frontend/app/mstore/types/filter.ts b/frontend/app/mstore/types/filter.ts index 6a5dfc7dd..ccde4e386 100644 --- a/frontend/app/mstore/types/filter.ts +++ b/frontend/app/mstore/types/filter.ts @@ -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) => { - 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) => - 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) => - 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) => { + 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) => + 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) => + 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); + } + } } diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts index eeba50783..3a97b9008 100644 --- a/frontend/app/mstore/types/filterItem.ts +++ b/frontend/app/mstore/types/filterItem.ts @@ -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) { diff --git a/frontend/app/mstore/types/savedSearch.ts b/frontend/app/mstore/types/savedSearch.ts new file mode 100644 index 000000000..dd4953049 --- /dev/null +++ b/frontend/app/mstore/types/savedSearch.ts @@ -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 = {}) { + 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): SavedSearch { + return new SavedSearch({ + ...data, + filter: new Filter().fromJson(data.filter) + }); + } +} + +export default SavedSearch; diff --git a/frontend/app/mstore/types/search.ts b/frontend/app/mstore/types/search.ts new file mode 100644 index 000000000..6229f653d --- /dev/null +++ b/frontend/app/mstore/types/search.ts @@ -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; + 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; + 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) { + 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; + }) + }); + } +} diff --git a/frontend/app/services/SearchService.ts b/frontend/app/services/SearchService.ts new file mode 100644 index 000000000..56fe30fc6 --- /dev/null +++ b/frontend/app/services/SearchService.ts @@ -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; + } +} diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts index 6afacb791..3a0f96043 100644 --- a/frontend/app/services/index.ts +++ b/frontend/app/services/index.ts @@ -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 ]; diff --git a/frontend/app/types/customMetric.js b/frontend/app/types/customMetric.js index 9db8e920e..688de4394 100644 --- a/frontend/app/types/customMetric.js +++ b/frontend/app/types/customMetric.js @@ -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