From 592ae5ac4da80352f8b7b9955d6332f0be4296e4 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Fri, 27 Dec 2024 10:53:22 +0100 Subject: [PATCH] ui: use default filter for sessions, move around saved search actions, remove tags modal --- .../shared/MainSearchBar/MainSearchBar.tsx | 6 - .../shared/SavedSearch/SavedSearch.tsx | 49 +--- .../SavedSearchModal/SavedSearchModal.tsx | 4 +- .../shared/SearchActions/SearchActions.tsx | 28 +- .../SessionFilters/AiSessionSearchField.tsx | 247 +----------------- .../shared/SessionFilters/SessionFilters.tsx | 6 + .../SessionsTabOverview.tsx | 3 +- .../SessionHeader/SessionHeader.tsx | 19 +- .../app/hooks/useSessionSearchQueryHandler.ts | 3 +- frontend/app/mstore/searchStore.ts | 130 +++++---- frontend/app/mstore/types/filterItem.ts | 2 +- 11 files changed, 138 insertions(+), 359 deletions(-) diff --git a/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx b/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx index 26dc0f4a2..8f174a827 100644 --- a/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx +++ b/frontend/app/components/shared/MainSearchBar/MainSearchBar.tsx @@ -1,6 +1,5 @@ import React from 'react'; import SessionFilters from 'Shared/SessionFilters'; -import AiSessionSearchField from 'Shared/SessionFilters/AiSessionSearchField'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; @@ -9,10 +8,6 @@ const MainSearchBar = () => { const projectId = projectsStore.siteId; const currSite = React.useRef(projectId); - // @ts-ignore - const originStr = window.env.ORIGIN || window.location.origin; - const isSaas = /app\.openreplay\.com/.test(originStr); - React.useEffect(() => { if (projectId !== currSite.current && currSite.current !== undefined) { console.debug('clearing filters due to project change'); @@ -22,7 +17,6 @@ const MainSearchBar = () => { }, [projectId]); return (
- {isSaas ? : null}
); diff --git a/frontend/app/components/shared/SavedSearch/SavedSearch.tsx b/frontend/app/components/shared/SavedSearch/SavedSearch.tsx index 904af1b75..566dda0f5 100644 --- a/frontend/app/components/shared/SavedSearch/SavedSearch.tsx +++ b/frontend/app/components/shared/SavedSearch/SavedSearch.tsx @@ -1,49 +1,22 @@ -import React, { useEffect } from 'react'; -import { Icon } from 'UI'; -import { Button } from 'antd'; -import cn from 'classnames'; -import stl from './SavedSearch.module.css'; -import { useModal } from 'App/components/Modal'; -import SavedSearchModal from './components/SavedSearchModal'; +import React from 'react'; +import { Dropdown } from 'antd'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; function SavedSearch() { - const { showModal } = useModal(); - const { searchStore, customFieldStore } = useStore(); + const { searchStore } = useStore(); const savedSearch = searchStore.savedSearch; - const list = searchStore.list; - const fetchedMeta = customFieldStore.fetchedMetadata; - // useEffect(() => { - // if (list.size === 0 && !fetchedMeta) { - // void searchStore.fetchSavedSearchList(); - // } - // }, [fetchedMeta]); + const options = searchStore.list.map((item) => ({ + key: item.searchId, + label: item.name, + onClick: () => searchStore.applySavedSearch(item) + })) return ( -
- - {savedSearch.exists() && ( -
- - Viewing: - - {savedSearch.name.length > 15 ? `${savedSearch.name.slice(0, 15)}...` : savedSearch.name} - -
- )} -
+ + {savedSearch.exists() ? 'Update' : 'Save'} Search + ); } diff --git a/frontend/app/components/shared/SavedSearch/components/SavedSearchModal/SavedSearchModal.tsx b/frontend/app/components/shared/SavedSearch/components/SavedSearchModal/SavedSearchModal.tsx index fd5af1204..81c94d65f 100644 --- a/frontend/app/components/shared/SavedSearch/components/SavedSearchModal/SavedSearchModal.tsx +++ b/frontend/app/components/shared/SavedSearch/components/SavedSearchModal/SavedSearchModal.tsx @@ -67,10 +67,10 @@ function SavedSearchModal(props: Props) {

- Saved Search {searchStore.list.size} + Saved Search {searchStore.list.length}

- {searchStore.list.size > 1 && ( + {searchStore.list.length > 1 && (
i.isEvent).length > 0; @@ -19,12 +20,23 @@ function SearchActions() { const hasSavedSearch = savedSearch && savedSearch.exists(); const hasSearch = hasFilters || hasSavedSearch; + const title = useMemo(() => { + if (activeTab && activeTab.type === 'bookmarks') { + return isEnterprise ? 'Vault' : 'Bookmarks'; + } + return 'Sessions'; + }, [activeTab?.type, isEnterprise]); + + // @ts-ignore + const originStr = window.env.ORIGIN || window.location.origin; + const isSaas = /app\.openreplay\.com/.test(originStr); + const showAiField = isSaas && activeTab.type === 'sessions'; const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading; return !metaLoading ? (
- - +

{title}

+ {isSaas && showAiField ? : null}
- +
{showPanel ? ( <> diff --git a/frontend/app/components/shared/SessionFilters/AiSessionSearchField.tsx b/frontend/app/components/shared/SessionFilters/AiSessionSearchField.tsx index 334035914..b491ba5a7 100644 --- a/frontend/app/components/shared/SessionFilters/AiSessionSearchField.tsx +++ b/frontend/app/components/shared/SessionFilters/AiSessionSearchField.tsx @@ -1,87 +1,8 @@ import { CloseOutlined, EnterOutlined } from '@ant-design/icons'; -import { Tour } from 'antd'; import { observer } from 'mobx-react-lite'; import React, { useState } from 'react'; import { useStore } from 'App/mstore'; -import { assist as assistRoute, isRoute } from 'App/routes'; -import { debounce } from 'App/utils'; -import { Icon, Input } from 'UI'; - -import FilterModal from 'Shared/Filters/FilterModal'; - -import OutsideClickDetectingDiv from '../OutsideClickDetectingDiv'; - -const ASSIST_ROUTE = assistRoute(); - -interface Props { - setFocused?: (focused: boolean) => void; -} - -function SessionSearchField(props: Props) { - const { searchStore, searchStoreLive } = useStore(); - const isLive = - isRoute(ASSIST_ROUTE, window.location.pathname) || - window.location.pathname.includes('multiview'); - const debounceFetchFilterSearch = React.useCallback( - debounce( - isLive ? searchStoreLive.fetchFilterSearch : searchStore.fetchFilterSearch, - 1000 - ), - [] - ); - - const [showModal, setShowModal] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - - const onSearchChange = ({ target: { value } }: any) => { - setSearchQuery(value); - debounceFetchFilterSearch({ q: value }); - }; - - const onAddFilter = (filter: any) => { - isLive - ? searchStoreLive.addFilterByKeyAndValue(filter.key, filter.value) - : searchStore.addFilterByKeyAndValue(filter.key, filter.value); - }; - - const onFocus = () => { - setShowModal(true); - props.setFocused?.(true); - }; - const onBlur = () => { - setTimeout(() => { - setShowModal(false); - props.setFocused?.(false); - }, 200); - }; - return ( -
- - - {showModal && ( -
- -
- )} -
- ); -} +import { Input } from 'UI'; const AiSearchField = observer(() => { const { searchStore } = useStore(); @@ -129,7 +50,7 @@ const AiSearchField = observer(() => { value={searchQuery} style={{ minWidth: 360, height: 30 }} autoComplete="off" - className="px-2 py-1 pe-9 text-lg placeholder-lg !border-0 rounded-e-full nofocus" + className="px-4 py-1 text-lg placeholder-lg !border-0 nofocus" leadingButton={ searchQuery !== '' ? (
{ } ); -function AiSessionSearchField(props: Props) { - const askTourKey = '__or__ask-tour'; - const tabKey = '__or__tab'; +function AiSessionSearchField() { const { aiFiltersStore } = useStore(); - const isTourShown = localStorage.getItem(askTourKey) !== null; - const [tab, setTab] = useState(localStorage.getItem(tabKey) || 'search'); - const [touring, setTouring] = useState(!isTourShown); - const [isFocused, setFocused] = React.useState(false); - const askAiRef = React.useRef(null); - const closeTour = () => { - setTouring(false); - localStorage.setItem(askTourKey, 'true'); - }; - const changeValue = (v?: string) => { - const newTab = v ? v : tab !== 'ask' ? 'ask' : 'search'; - setTab(newTab); - localStorage.setItem(tabKey, newTab); - }; - - const boxStyle = tab === 'ask' - ? gradientBox - : isFocused ? regularBoxFocused : regularBoxUnfocused; return ( -
+
-
- -
- {tab === 'ask' ? ( - - ) : ( - - )} - - Introducing - - Ask AI -
- ), - target: () => askAiRef.current, - description: - 'Easily find sessions with our AI search. Just enable Ask AI, type in your query naturally, and the AI will swiftly and precisely display relevant sessions.', - nextButtonProps: { - children: ( - - Ask AI - - - ), - onClick: () => { - changeValue('ask'); - closeTour(); - } - } - } - ]} - /> +
); } -export const AskAiSwitchToggle = ({ - enabled, - setEnabled, - loading - }: { - enabled: boolean; - loading: boolean; - setEnabled: () => void; -}) => { - return ( -
setEnabled()} - className={loading ? 'animate-bg-spin' : ''} - style={{ - position: 'relative', - display: 'inline-block', - height: 24, - background: enabled - ? 'linear-gradient(-25deg, #394eff, #3EAAAf, #3ccf65)' - : 'rgb(170 170 170)', - backgroundSize: loading ? '200% 200%' : 'unset', - borderRadius: 100, - cursor: 'pointer', - transition: 'all 0.2s ease-in-out', - border: 0, - verticalAlign: 'middle' - }} - > -
-
-
Ask AI
-
-
- ); -}; - export const gradientBox = { border: 'double 1.5px transparent', borderRadius: '100px', @@ -307,27 +95,8 @@ export const gradientBox = { display: 'flex', gap: '0.25rem', alignItems: 'center', - width: '100%' -}; - -const regularBoxUnfocused = { - borderRadius: '100px', - border: 'solid 1.5px #BFBFBF', - background: '#fffff', - display: 'flex', - gap: '0.25rem', - alignItems: 'center', - width: '100%' -}; - -const regularBoxFocused = { - borderRadius: '100px', - border: 'solid 1.5px #394EFF', - background: '#fffff', - display: 'flex', - gap: '0.25rem', - alignItems: 'center', - width: '100%' + width: '100%', + overflow: 'hidden', }; export default observer(AiSessionSearchField); diff --git a/frontend/app/components/shared/SessionFilters/SessionFilters.tsx b/frontend/app/components/shared/SessionFilters/SessionFilters.tsx index acd78dfb5..d09ec6333 100644 --- a/frontend/app/components/shared/SessionFilters/SessionFilters.tsx +++ b/frontend/app/components/shared/SessionFilters/SessionFilters.tsx @@ -22,6 +22,12 @@ function SessionFilters() { const saveRequestPayloads = projectsStore.instance?.saveRequestPayloads ?? false; + useEffect(() => { + if (searchStore.instance.filters.length === 0) { + searchStore.addFilterByKeyAndValue(FilterKey.LOCATION, '', 'isAny') + } + }, []) + useSessionSearchQueryHandler({ appliedFilter, loading: metaLoading, diff --git a/frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx b/frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx index dbae77167..eda0a13c1 100644 --- a/frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx @@ -14,6 +14,7 @@ function SessionsTabOverview() { const [query, setQuery] = React.useState(''); const { aiFiltersStore, searchStore } = useStore(); const appliedFilter = searchStore.instance; + const activeTab = searchStore.activeTab; const handleKeyDown = (event: any) => { if (event.key === 'Enter') { @@ -41,7 +42,7 @@ function SessionsTabOverview() { placeholder={'ask session ai'} /> ) : null} - + {activeTab.type !== 'bookmarks' && }
diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/SessionHeader.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/SessionHeader.tsx index 0fd8d2ddc..10138f5ae 100644 --- a/frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/SessionHeader.tsx +++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/SessionHeader.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import Period from 'Types/app/period'; import SelectDateRange from 'Shared/SelectDateRange'; import SessionTags from '../SessionTags'; @@ -8,20 +8,11 @@ import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; function SessionHeader() { - const { searchStore, userStore } = useStore(); - const isEnterprise = userStore.isEnterprise; - const activeTab = searchStore.activeTab; + const { searchStore } = useStore(); const { startDate, endDate, rangeValue } = searchStore.instance; const period = Period({ start: startDate, end: endDate, rangeName: rangeValue }); - const title = useMemo(() => { - if (activeTab && activeTab.type === 'bookmarks') { - return isEnterprise ? 'Vault' : 'Bookmarks'; - } - return 'Sessions'; - }, [activeTab?.type, isEnterprise]); - const onDateChange = (e: any) => { const dateValues = e.toJSON(); searchStore.edit(dateValues); @@ -30,13 +21,11 @@ function SessionHeader() { return (
-

{title}

- {activeTab.type !== 'bookmarks' && } +
- {activeTab.type !== 'bookmarks' && - } +
diff --git a/frontend/app/hooks/useSessionSearchQueryHandler.ts b/frontend/app/hooks/useSessionSearchQueryHandler.ts index 12511be38..c9335ce9b 100644 --- a/frontend/app/hooks/useSessionSearchQueryHandler.ts +++ b/frontend/app/hooks/useSessionSearchQueryHandler.ts @@ -29,8 +29,9 @@ const useSessionSearchQueryHandler = ({ onBeforeLoad, appliedFilter, loading }: const converter = JsonUrlConverter.urlParamsToJson(history.location.search); const json = getFilterFromJson(converter.toJSON()); const filter = new Search(json); - searchStore.applyFilter(filter, true); searchStore.setUrlParsed(); + if (filter.filters.length === 0) return; + searchStore.applyFilter(filter, true); } catch (error) { console.error('Error applying filter from query:', error); } diff --git a/frontend/app/mstore/searchStore.ts b/frontend/app/mstore/searchStore.ts index dab983d79..d354ec45d 100644 --- a/frontend/app/mstore/searchStore.ts +++ b/frontend/app/mstore/searchStore.ts @@ -5,7 +5,7 @@ import { filtersMap, generateFilterOptions, liveFiltersMap, - mobileConditionalFiltersMap + mobileConditionalFiltersMap, } from 'Types/filter/newFilter'; import { List } from 'immutable'; import { makeAutoObservable, runInAction } from 'mobx'; @@ -28,18 +28,18 @@ export const checkValues = (key: any, value: any) => { }; export const filterMap = ({ - category, - value, - key, - operator, - sourceOperator, - source, - custom, - isEvent, - filters, - sort, - order - }: any) => ({ + category, + value, + key, + operator, + sourceOperator, + source, + custom, + isEvent, + filters, + sort, + order, +}: any) => ({ value: checkValues(key, value), custom, type: category === FilterCategory.METADATA ? FilterKey.METADATA : key, @@ -47,7 +47,7 @@ export const filterMap = ({ source: category === FilterCategory.METADATA ? key.replace(/^_/, '') : source, sourceOperator, isEvent, - filters: filters ? filters.map(filterMap) : [] + filters: filters ? filters.map(filterMap) : [], }); export const TAB_MAP: any = { @@ -55,11 +55,11 @@ export const TAB_MAP: any = { sessions: { name: 'Sessions', type: 'sessions' }, bookmarks: { name: 'Bookmarks', type: 'bookmarks' }, notes: { name: 'Notes', type: 'notes' }, - recommendations: { name: 'Recommendations', type: 'recommendations' } + recommendations: { name: 'Recommendations', type: 'recommendations' }, }; class SearchStore { - list = List(); + list: SavedSearch[] = []; latestRequestTime: number | null = null; latestList = List(); alertMetricId: number | null = null; @@ -107,13 +107,19 @@ class SearchStore { applySavedSearch(savedSearch: ISavedSearch) { this.savedSearch = savedSearch; - this.edit({ filters: savedSearch.filter ? savedSearch.filter.filters.map((i: FilterItem) => new FilterItem().fromJson(i)) : [] }); + this.edit({ + filters: savedSearch.filter + ? savedSearch.filter.filters.map((i: FilterItem) => + new FilterItem().fromJson(i) + ) + : [], + }); this.currentPage = 1; } async fetchSavedSearchList() { const response = await searchService.fetchSavedSearch(); - this.list = List(response.map((item: any) => new SavedSearch(item))); + this.list = response.map((item: any) => new SavedSearch(item)); } edit(instance: Partial) { @@ -122,7 +128,9 @@ class SearchStore { } editSavedSearch(instance: Partial) { - this.savedSearch = new SavedSearch(Object.assign(this.savedSearch.toData(), instance)); + this.savedSearch = new SavedSearch( + Object.assign(this.savedSearch.toData(), instance) + ); } apply(filter: any, fromUrl: boolean) { @@ -145,7 +153,10 @@ class SearchStore { .fetchFilterSearch(params) .then((response: any[]) => { this.filterSearchList = response.reduce( - (acc: Record, item: any) => { + ( + acc: Record, + item: any + ) => { const { projectId, type, value } = item; if (!acc[type]) acc[type] = []; acc[type].push({ projectId, value }); @@ -206,17 +217,19 @@ class SearchStore { } clearList() { - this.list = List(); + this.list = []; } clearSearch() { const instance = this.instance; - this.edit(new Search({ - rangeValue: instance.rangeValue, - startDate: instance.startDate, - endDate: instance.endDate, - filters: [] - })); + this.edit( + new Search({ + rangeValue: instance.rangeValue, + startDate: instance.startDate, + endDate: instance.endDate, + filters: [], + }) + ); this.savedSearch = new SavedSearch({}); sessionStore.clearList(); @@ -226,7 +239,11 @@ class SearchStore { checkForLatestSessions() { const filter = this.instance.toSearch(); if (this.latestRequestTime) { - const period = Period({ rangeName: CUSTOM_RANGE, start: this.latestRequestTime, end: Date.now() }); + const period = Period({ + rangeName: CUSTOM_RANGE, + start: this.latestRequestTime, + end: Date.now(), + }); const newTimestamps: any = period.toJSON(); filter.startDate = newTimestamps.startDate; filter.endDate = newTimestamps.endDate; @@ -242,29 +259,32 @@ class SearchStore { } addFilter(filter: any) { - const index = filter.isEvent ? -1 : this.instance.filters.findIndex((i: FilterItem) => i.key === filter.key); + const index = filter.isEvent + ? -1 + : this.instance.filters.findIndex( + (i: FilterItem) => i.key === filter.key + ); - console.log(filter) filter.value = checkFilterValue(filter.value); filter.filters = filter.filters ? filter.filters.map((subFilter: any) => ({ - ...subFilter, - value: checkFilterValue(subFilter.value) - })) + ...subFilter, + value: checkFilterValue(subFilter.value), + })) : null; if (index > -1) { const oldFilter = new FilterItem(this.instance.filters[index]); const updatedFilter = { ...oldFilter, - value: oldFilter.value.concat(filter.value) + value: oldFilter.value.concat(filter.value), }; oldFilter.merge(updatedFilter); this.updateFilter(index, updatedFilter); } else { this.instance.filters.push(filter); this.instance = new Search({ - ...this.instance.toData() + ...this.instance.toData(), }); } @@ -275,7 +295,13 @@ class SearchStore { } } - addFilterByKeyAndValue(key: any, value: any, operator?: string, sourceOperator?: string, source?: string) { + addFilterByKeyAndValue( + key: any, + value: any, + operator?: string, + sourceOperator?: string, + source?: string + ) { let defaultFilter = { ...filtersMap[key] }; defaultFilter.value = value; @@ -305,7 +331,7 @@ class SearchStore { this.instance = new Search({ ...this.instance.toData(), - filters: newFilters + filters: newFilters, }); }; @@ -316,7 +342,7 @@ class SearchStore { this.instance = new Search({ ...this.instance.toData(), - filters: newFilters + filters: newFilters, }); }; @@ -328,12 +354,17 @@ class SearchStore { // TODO } - async fetchSessions(force: boolean = false, bookmarked: boolean = false): Promise { + async fetchSessions( + force: boolean = false, + bookmarked: boolean = false + ): Promise { const filter = this.instance.toSearch(); if (this.activeTags[0] && this.activeTags[0] !== 'all') { const tagFilter = filtersMap[FilterKey.ISSUE]; - tagFilter.value = [issues_types.find((i: any) => i.type === this.activeTags[0])?.type]; + tagFilter.value = [ + issues_types.find((i: any) => i.type === this.activeTags[0])?.type, + ]; delete tagFilter.operatorOptions; delete tagFilter.options; delete tagFilter.placeholder; @@ -345,14 +376,17 @@ class SearchStore { this.latestRequestTime = Date.now(); this.latestList = List(); - await sessionStore.fetchSessions({ - ...filter, - page: this.currentPage, - perPage: this.pageSize, - tab: this.activeTab.type, - bookmarked: bookmarked ? true : undefined - }, force); - }; + await sessionStore.fetchSessions( + { + ...filter, + page: this.currentPage, + perPage: this.pageSize, + tab: this.activeTab.type, + bookmarked: bookmarked ? true : undefined, + }, + force + ); + } } export default SearchStore; diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts index 8809aa16c..02f231c87 100644 --- a/frontend/app/mstore/types/filterItem.ts +++ b/frontend/app/mstore/types/filterItem.ts @@ -4,7 +4,7 @@ import { filtersMap, mobileConditionalFiltersMap, } from 'Types/filter/newFilter'; -import { action, makeAutoObservable, observable } from 'mobx'; +import { makeAutoObservable } from 'mobx'; import { pageUrlOperators } from '../../constants/filterOptions';