ui: finish with omnisearch thing

This commit is contained in:
nick-delirium 2024-11-22 10:55:21 +01:00
parent 9909511e94
commit 614c0655c2
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
30 changed files with 726 additions and 912 deletions

View file

@ -1,7 +1,6 @@
import React from 'react';
import { Button } from 'antd';
import { useModal } from 'App/components/Modal';
import SessionSearchField from 'Shared/SessionSearchField';
import { MODULES } from 'Components/Client/Modules';
import AssistStats from '../../AssistStats';
@ -9,7 +8,7 @@ import Recordings from '../RecordingsList/Recordings';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
function AssistSearchField() {
function AssistSearchActions() {
const { searchStoreLive, userStore } = useStore();
const modules = userStore.account.settings?.modules ?? [];
const isEnterprise = userStore.isEnterprise
@ -27,9 +26,6 @@ function AssistSearchField() {
};
return (
<div className="flex items-center w-full gap-2">
<div style={{ width: '60%' }}>
<SessionSearchField />
</div>
{isEnterprise && modules.includes(MODULES.OFFLINE_RECORDINGS)
? <Button type="primary" ghost onClick={showRecords}>Training Videos</Button> : null
}
@ -50,4 +46,4 @@ function AssistSearchField() {
);
}
export default observer(AssistSearchField);
export default observer(AssistSearchActions);

View file

@ -0,0 +1 @@
export { default } from './AssistSearchActions'

View file

@ -1 +0,0 @@
export { default } from './AssistSearchField'

View file

@ -1,11 +1,12 @@
import React from 'react';
import LiveSessionList from 'Shared/LiveSessionList';
import LiveSessionSearch from 'Shared/LiveSessionSearch';
import AssistSearchField from './AssistSearchField';
import AssistSearchActions from './AssistSearchActions';
function AssistView() {
return (
<div className="w-full mx-auto" style={{ maxWidth: '1360px'}}>
<AssistSearchActions />
<LiveSessionSearch />
<div className="my-4" />
<LiveSessionList />

View file

@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite';
import React from 'react';
import colors from 'tailwindcss/colors';
import { gradientBox } from 'App/components/shared/SessionSearchField/AiSessionSearchField';
import { gradientBox } from 'App/components/shared/SessionFilters/AiSessionSearchField';
import aiSpinner from 'App/lottie/aiSpinner.json';
import { useStore } from 'App/mstore';
import { Icon, Input } from 'UI';

View file

@ -2,181 +2,219 @@ import React, { useEffect, useState } from 'react';
import FilterList from 'Shared/Filters/FilterList';
import SeriesName from './SeriesName';
import cn from 'classnames';
import {observer} from 'mobx-react-lite';
import { observer } from 'mobx-react-lite';
import ExcludeFilters from './ExcludeFilters';
import AddStepButton from "Components/Dashboard/components/FilterSeries/AddStepButton";
import {Button, Space} from "antd";
import {ChevronDown, ChevronUp, Trash} from "lucide-react";
import { Button, Space } from 'antd';
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
const FilterCountLabels = observer((props: { filters: any, toggleExpand: any }) => {
const FilterCountLabels = observer(
(props: { filters: any; toggleExpand: any }) => {
const events = props.filters.filter((i: any) => i && i.isEvent).length;
const filters = props.filters.filter((i: any) => i && !i.isEvent).length;
return <div className="flex items-center">
return (
<div className="flex items-center">
<Space>
{events > 0 && (
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
{`${events} Event${events > 1 ? 's' : ''}`}
</Button>
)}
{events > 0 && (
<Button
type="primary"
ghost
size="small"
onClick={props.toggleExpand}
>
{`${events} Event${events > 1 ? 's' : ''}`}
</Button>
)}
{filters > 0 && (
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
{`${filters} Filter${filters > 1 ? 's' : ''}`}
</Button>
)}
{filters > 0 && (
<Button
type="primary"
ghost
size="small"
onClick={props.toggleExpand}
>
{`${filters} Filter${filters > 1 ? 's' : ''}`}
</Button>
)}
</Space>
</div>;
});
</div>
);
}
);
const FilterSeriesHeader = observer((props: {
expanded: boolean,
hidden: boolean,
seriesIndex: number,
series: any,
onRemove: (seriesIndex: any) => void,
canDelete: boolean | undefined,
toggleExpand: () => void
}) => {
const onUpdate = (name: any) => {
props.series.update('name', name)
}
return <div className={cn("border-b px-5 h-12 flex items-center relative", {hidden: props.hidden})}>
<Space className="mr-auto" size={30}>
<SeriesName
seriesIndex={props.seriesIndex}
name={props.series.name}
onUpdate={onUpdate}
/>
{!props.expanded &&
<FilterCountLabels filters={props.series.filter.filters} toggleExpand={props.toggleExpand}/>}
</Space>
<Space>
<Button onClick={props.onRemove}
size="small"
disabled={!props.canDelete}
icon={<Trash size={14}/>}/>
<Button onClick={props.toggleExpand}
size="small"
icon={props.expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
</Space>
</div>;
})
interface Props {
const FilterSeriesHeader = observer(
(props: {
expanded: boolean;
hidden: boolean;
seriesIndex: number;
series: any;
onRemoveSeries: (seriesIndex: any) => void;
canDelete?: boolean;
supportsEmpty?: boolean;
hideHeader?: boolean;
emptyMessage?: any;
observeChanges?: () => void;
excludeFilterKeys?: Array<string>;
canExclude?: boolean;
expandable?: boolean;
onRemove: (seriesIndex: any) => void;
canDelete: boolean | undefined;
toggleExpand: () => void;
}) => {
const onUpdate = (name: any) => {
props.series.update('name', name);
};
return (
<div
className={cn('px-4 h-12 flex items-center relative', {
hidden: props.hidden,
})}
>
<Space className="mr-auto" size={30}>
<SeriesName
seriesIndex={props.seriesIndex}
name={props.series.name}
onUpdate={onUpdate}
/>
{!props.expanded && (
<FilterCountLabels
filters={props.series.filter.filters}
toggleExpand={props.toggleExpand}
/>
)}
</Space>
<Space>
<Button
onClick={props.onRemove}
size="small"
disabled={!props.canDelete}
icon={<Trash size={14} />}
/>
<Button
onClick={props.toggleExpand}
size="small"
icon={
props.expanded ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)
}
/>
</Space>
</div>
);
}
);
interface Props {
seriesIndex: number;
series: any;
onRemoveSeries: (seriesIndex: any) => void;
canDelete?: boolean;
supportsEmpty?: boolean;
hideHeader?: boolean;
emptyMessage?: any;
observeChanges?: () => void;
excludeFilterKeys?: Array<string>;
canExclude?: boolean;
expandable?: boolean;
}
function FilterSeries(props: Props) {
const {
observeChanges = () => {
},
canDelete,
hideHeader = false,
emptyMessage = 'Add an event or filter step to define the series.',
supportsEmpty = true,
excludeFilterKeys = [],
canExclude = false,
expandable = false
} = props;
const [expanded, setExpanded] = useState(!expandable);
const {series, seriesIndex} = props;
const [prevLength, setPrevLength] = useState(0);
const {
observeChanges = () => {},
canDelete,
hideHeader = false,
emptyMessage = 'Add an event or filter step to define the series.',
supportsEmpty = true,
excludeFilterKeys = [],
canExclude = false,
expandable = false,
} = props;
const [expanded, setExpanded] = useState(!expandable);
const { series, seriesIndex } = props;
const [prevLength, setPrevLength] = useState(0);
useEffect(() => {
if (series.filter.filters.length === 1 && prevLength === 0 && seriesIndex === 0) {
setExpanded(true);
}
setPrevLength(series.filter.filters.length);
}, [series.filter.filters.length]);
const onUpdateFilter = (filterIndex: any, filter: any) => {
series.filter.updateFilter(filterIndex, filter);
observeChanges();
};
const onFilterMove = (newFilters: any) => {
series.filter.replaceFilters(newFilters.toArray())
observeChanges();
useEffect(() => {
if (
series.filter.filters.length === 1 &&
prevLength === 0 &&
seriesIndex === 0
) {
setExpanded(true);
}
setPrevLength(series.filter.filters.length);
}, [series.filter.filters.length]);
const onChangeEventsOrder = (_: any, {name, value}: any) => {
series.filter.updateKey(name, value);
observeChanges();
};
const onUpdateFilter = (filterIndex: any, filter: any) => {
series.filter.updateFilter(filterIndex, filter);
observeChanges();
};
const onRemoveFilter = (filterIndex: any) => {
series.filter.removeFilter(filterIndex);
observeChanges();
};
const onFilterMove = (newFilters: any) => {
series.filter.replaceFilters(newFilters.toArray());
observeChanges();
};
return (
<div className="border rounded-lg shadow-sm bg-white">
{canExclude && <ExcludeFilters filter={series.filter}/>}
const onChangeEventsOrder = (_: any, { name, value }: any) => {
series.filter.updateKey(name, value);
observeChanges();
};
{!hideHeader && (
<FilterSeriesHeader hidden={hideHeader}
seriesIndex={seriesIndex}
series={series}
onRemove={props.onRemoveSeries}
canDelete={canDelete}
expanded={expanded}
toggleExpand={() => setExpanded(!expanded)}/>
const onRemoveFilter = (filterIndex: any) => {
series.filter.removeFilter(filterIndex);
observeChanges();
};
const onAddFilter = (filter: any) => {
series.filter.addFilter(filter);
observeChanges();
}
return (
<div>
{canExclude && <ExcludeFilters filter={series.filter} />}
{!hideHeader && (
<FilterSeriesHeader
hidden={hideHeader}
seriesIndex={seriesIndex}
series={series}
onRemove={props.onRemoveSeries}
canDelete={canDelete}
expanded={expanded}
toggleExpand={() => setExpanded(!expanded)}
/>
)}
{expandable && (
<Space
className="justify-between w-full px-5 py-2 cursor-pointer"
onClick={() => setExpanded(!expanded)}
>
<div>
{!expanded && (
<FilterCountLabels
filters={series.filter.filters}
toggleExpand={() => setExpanded(!expanded)}
/>
)}
</div>
<Button
size="small"
icon={
expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />
}
/>
</Space>
)}
{expandable && (
<Space className="justify-between w-full px-5 py-2 cursor-pointer" onClick={() => setExpanded(!expanded)}>
<div>{!expanded && <FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>}</div>
<Button size="small"
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
</Space>
)}
{expanded && (
<>
<div className="p-5">
{series.filter.filters.length > 0 ? (
<FilterList
filter={series.filter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
supportsEmpty={supportsEmpty}
onFilterMove={onFilterMove}
excludeFilterKeys={excludeFilterKeys}
// actions={[
// expandable && (
// <Button onClick={() => setExpanded(!expanded)}
// size="small"
// icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
// )
// ]}
/>
) : (
<div className="color-gray-medium">{emptyMessage}</div>
)}
</div>
<div className="border-t h-12 flex items-center">
<div className="-mx-4 px-5">
<AddStepButton excludeFilterKeys={excludeFilterKeys} series={series}/>
</div>
</div>
</>
)}
</div>
);
{expanded ? (
<FilterList
filter={series.filter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
supportsEmpty={supportsEmpty}
onFilterMove={onFilterMove}
excludeFilterKeys={excludeFilterKeys}
onAddFilter={onAddFilter}
/>
) : null}
</div>
);
}
export default observer(FilterSeries);

View file

@ -44,17 +44,6 @@ function WidgetFormNew() {
export default observer(WidgetFormNew);
function DefineSteps({ metric, excludeFilterKeys }: any) {
return (
<div className="px-4 py-2 bg-white rounded-lg shadow-sm flex items-center">
<Typography.Text strong>Filter</Typography.Text>
<AddStepButton excludeFilterKeys={excludeFilterKeys} series={metric.series[0]} />
</div>
);
}
const FilterSection = observer(({ metric, excludeFilterKeys }: any) => {
// const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries');
// const tableOptions = metricOf.filter((i) => i.type === 'table');

View file

@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import withPageTitle from 'HOCs/withPageTitle';
import NoSessionsMessage from 'Shared/NoSessionsMessage';
import MainSearchBar from 'Shared/MainSearchBar';
import SessionSearch from 'Shared/SessionSearch';
import SearchActions from 'Shared/SearchActions';
import SessionsTabOverview from 'Shared/SessionsTabOverview/SessionsTabOverview';
import FFlagsList from 'Components/FFlags';
import NewFFlag from 'Components/FFlags/NewFFlag';
@ -32,15 +32,14 @@ function Overview({ match: { params } }: IProps) {
React.useEffect(() => {
searchStore.setActiveTab(tab);
}, [tab]);
return (
<Switch>
<Route exact strict
path={[withSiteId(sessions(), siteId), withSiteId(notes(), siteId), withSiteId(bookmarks(), siteId)]}>
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
<NoSessionsMessage siteId={siteId} />
<SearchActions />
<MainSearchBar />
<SessionSearch />
<div className="my-4" />
<SessionsTabOverview />
</div>

View file

@ -7,7 +7,7 @@ import { KEYS } from 'Types/filter/customFilter';
import { capitalize } from 'App/utils';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import AssistSearchField from 'App/components/Assist/AssistSearchField';
import AssistSearchField from 'App/components/Assist/AssistSearchActions';
import LiveSessionSearch from 'Shared/LiveSessionSearch';
import cn from 'classnames';
import Session from 'App/mstore/types/session';
@ -70,7 +70,7 @@ function AssistSessionsModal(props: ConnectProps) {
icon="arrow-repeat"
/>
</Tooltip>
<AssistSearchField />
<AssistSearchActions />
</div>
<div className="flex self-end items-center gap-2" w-full>
<span className="color-gray-medium">Sort By</span>

View file

@ -0,0 +1,175 @@
import React, { useRef, useState } from 'react';
import { Button, Checkbox, Input } from 'antd';
import cn from 'classnames';
export function AutocompleteModal({
onClose,
onApply,
values,
handleFocus,
loadOptions,
options,
isLoading,
}: {
values: string[];
onClose: () => void;
onApply: (values: string[]) => void;
handleFocus: () => void;
loadOptions: (query: string) => void;
options: { value: string; label: string }[];
placeholder?: string;
isLoading?: boolean;
}) {
const [query, setQuery] = React.useState('');
const [selectedValues, setSelectedValues] = React.useState<string[]>(
values.filter((i) => i.length > 0)
);
const handleInputChange = (value: string) => {
setQuery(value);
loadOptions(value);
};
const onSelectOption = (item: { value: string; label: string }) => {
const selected = isSelected(item);
if (!selected) {
setSelectedValues([...selectedValues, item.value]);
} else {
setSelectedValues(selectedValues.filter((i) => i !== item.value));
}
};
const isSelected = (item: { value: string; label: string }) => {
return selectedValues.includes(item.value);
};
const applyValues = () => {
onApply(selectedValues);
};
const sortedOptions = React.useMemo(() => {
if (values[0] && values[0].length) {
const sorted = options.sort((a, b) => {
return values.includes(a.value) ? -1 : 1;
});
return sorted;
}
return options;
}, [options.length]);
return (
<div
className={cn(
'absolute left-0 mt-2 p-4 bg-white rounded-xl shadow border-gray-light z-10'
)}
style={{ minWidth: 320, minHeight: 100, top: '100%' }}
>
<Input.Search
value={query}
onFocus={handleFocus}
loading={isLoading}
onChange={(e) => handleInputChange(e.target.value)}
/>
<div
className={'flex flex-col gap-2 overflow-y-auto py-2'}
style={{ maxHeight: 200 }}
>
{sortedOptions.map((item) => (
<div
onClick={() => onSelectOption(item)}
className={
'cursor-pointer w-full py-1 hover:bg-active-blue rounded px-2'
}
>
<Checkbox checked={isSelected(item)} /> {item.label}
</div>
))}
</div>
{query.length ? (
<div className={'border-y border-y-gray-light py-2'}>
<div
className={
'rounded cursor-pointer text-blue hover:bg-active-blue px-2 py-1'
}
onClick={() => onApply([query])}
>
Apply "{query}"
</div>
</div>
) : null}
<div className={'flex gap-2 items-center pt-2'}>
<Button type={'primary'} onClick={applyValues}>
Apply
</Button>
<Button onClick={onClose}>Cancel</Button>
</div>
</div>
);
}
interface Props {
value: string[];
params?: any;
onApplyValues: (values: string[]) => void;
modalRenderer: (props: any) => React.ReactElement;
placeholder?: string;
modalProps?: any;
mapValues?: (value: string) => string;
}
export function AutoCompleteContainer(props: Props) {
const filterValueContainer = useRef<HTMLDivElement>(null);
const [showValueModal, setShowValueModal] = useState(false);
const isEmpty = props.value.length === 0 || !props.value[0].length;
const onClose = () => setShowValueModal(false);
const onApply = (values: string[]) => {
props.onApplyValues(values);
setShowValueModal(false);
};
return (
<div
className={
'rounded border border-gray-light px-2 relative w-fit whitespace-nowrap flex items-center'
}
style={{ height: 26 }}
ref={filterValueContainer}
>
<div
onClick={() => setShowValueModal(true)}
className={'flex items-center gap-2 cursor-pointer'}
>
{!isEmpty ? (
<>
<div
className={'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'}
>
{props.mapValues
? props.mapValues(props.value[0])
: props.value[0]}
</div>
{props.value.length > 1 ? (
<div
className={
'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'
}
>
+ {props.value.length - 1} More
</div>
) : null}
</>
) : (
<div className={'text-disabled-text'}>
{props.placeholder ? props.placeholder : 'Select values'}
</div>
)}
</div>
{showValueModal ? (
<props.modalRenderer
{...props.modalProps}
params={props.params}
onClose={onClose}
onApply={onApply}
values={props.value}
/>
) : null}
</div>
);
}

View file

@ -1,117 +1,25 @@
import React, { useState, useEffect, useCallback, useRef, ChangeEvent, KeyboardEvent } from 'react';
import { Icon } from 'UI';
import APIClient from 'App/api_client';
import React, {
useState,
useEffect,
useCallback,
useRef,
} from 'react';
import { debounce } from 'App/utils';
import stl from './FilterAutoComplete.module.css';
import colors from 'App/theme/colors';
import Select from 'react-select';
import cn from 'classnames';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { searchService} from 'App/services';
const dropdownStyles = {
option: (provided: any, state: any) => ({
...provided,
whiteSpace: 'nowrap',
width: '100%',
minWidth: 150,
transition: 'all 0.3s',
overflow: 'hidden',
textOverflow: 'ellipsis',
backgroundColor: state.isFocused ? colors['active-blue'] : 'transparent',
color: state.isFocused ? colors.teal : 'black',
fontSize: '14px',
'&:hover': {
transition: 'all 0.2s',
backgroundColor: colors['active-blue']
},
'&:focus': {
transition: 'all 0.2s',
backgroundColor: colors['active-blue']
}
}),
control: (provided: any) => {
const obj = {
...provided,
border: 'solid thin transparent !important',
backgroundColor: 'transparent',
cursor: 'pointer',
height: '26px',
minHeight: '26px',
borderRadius: '.5rem',
boxShadow: 'none !important'
};
return obj;
},
valueContainer: (provided: any) => ({
...provided,
// paddingRight: '0px',
width: 'fit-content',
alignItems: 'center',
height: '26px',
padding: '0 3px'
}),
indicatorsContainer: (provided: any) => ({
...provided,
padding: '0px',
height: '26px'
}),
menu: (provided: any, state: any) => ({
...provided,
top: 0,
borderRadius: '3px',
border: `1px solid ${colors['gray-light']}`,
backgroundColor: '#fff',
boxShadow: '1px 1px 1px rgba(0, 0, 0, 0.1)',
position: 'absolute',
width: 'unset',
maxWidth: '300px',
overflow: 'hidden',
zIndex: 100
}),
menuList: (provided: any, state: any) => ({
...provided,
padding: 0
}),
noOptionsMessage: (provided: any) => ({
...provided,
whiteSpace: 'nowrap !important'
// minWidth: 'fit-content',
}),
container: (provided: any) => ({
...provided,
top: '18px',
position: 'absolute'
}),
input: (provided: any) => ({
...provided,
height: '22px',
'& input:focus': {
border: 'none !important'
}
}),
singleValue: (provided: any, state: { isDisabled: any }) => {
const opacity = state.isDisabled ? 0.5 : 1;
const transition = 'opacity 300ms';
return {
...provided,
opacity,
transition,
display: 'flex',
alignItems: 'center',
height: '20px'
};
}
};
import { searchService } from 'App/services';
import { AutocompleteModal, AutoCompleteContainer } from './AutocompleteModal';
type FilterParam = { [key: string]: any };
function processKey(input: FilterParam): FilterParam {
const result: FilterParam = {};
for (const key in input) {
if (input.type === 'metadata' && typeof input[key] === 'string' && input[key].startsWith('_')) {
if (
input.type === 'metadata' &&
typeof input[key] === 'string' &&
input[key].startsWith('_')
) {
result[key] = input[key].substring(1);
} else {
result[key] = input[key];
@ -123,190 +31,110 @@ function processKey(input: FilterParam): FilterParam {
interface Props {
showOrButton?: boolean;
showCloseButton?: boolean;
onRemoveValue?: () => void;
onAddValue?: () => void;
onRemoveValue?: (ind: number) => void;
onAddValue?: (ind: number) => void;
endpoint?: string;
method?: string;
params?: any;
headerText?: string;
placeholder?: string;
onSelect: (e: any, item: any) => void;
onSelect: (e: any, item: any, index: number) => void;
value: any;
icon?: string;
hideOrText?: boolean;
onApplyValues: (values: string[]) => void;
}
const FilterAutoComplete: React.FC<Props> = ({
showCloseButton = false,
placeholder = 'Type to search',
method = 'GET',
showOrButton = false,
endpoint = '',
params = {},
value = '',
hideOrText = false,
onSelect,
onRemoveValue,
onAddValue
}: Props) => {
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState<{ value: string; label: string }[]>([]);
const [query, setQuery] = useState(value);
const [menuIsOpen, setMenuIsOpen] = useState(false);
const [initialFocus, setInitialFocus] = useState(false);
const [previousQuery, setPreviousQuery] = useState(value);
const selectRef = useRef<any>(null);
const inputRef = useRef<any>(null);
const { filterStore } = useStore();
const _params = processKey(params);
const filterKey = `${_params.type}${_params.key || ''}`;
const topValues = filterStore.topValues[filterKey] || [];
const [topValuesLoading, setTopValuesLoading] = useState(false);
const FilterAutoComplete = observer(
({
params = {},
onClose,
onApply,
values,
}: { params: any, values: string[], onClose: () => void, onApply: (values: string[]) => void }) => {
const [options, setOptions] = useState<{ value: string; label: string }[]>(
[]
);
const [initialFocus, setInitialFocus] = useState(false);
const [loading, setLoading] = useState(false);
const { filterStore } = useStore();
const _params = processKey(params);
const filterKey = `${_params.type}${_params.key || ''}`;
const topValues = filterStore.topValues[filterKey] || [];
const loadTopValues = () => {
setTopValuesLoading(true);
filterStore.fetchTopValues(_params.type, _params.key).finally(() => {
setTopValuesLoading(false);
setLoading(false);
});
};
const loadTopValues = () => {
void filterStore.fetchTopValues(_params.type, _params.key);
};
useEffect(() => {
if (topValues.length > 0) {
const mappedValues = topValues.map((i) => ({ value: i.value, label: i.value }));
setOptions(mappedValues);
if (!query.length && initialFocus) {
setMenuIsOpen(true);
useEffect(() => {
if (topValues.length > 0) {
const mappedValues = topValues.map((i) => ({
value: i.value,
label: i.value,
}));
setOptions(mappedValues);
}
}
}, [topValues, initialFocus, query.length]);
}, [topValues, initialFocus]);
useEffect(loadTopValues, [_params.type]);
useEffect(loadTopValues, [_params.type]);
useEffect(() => {
setQuery(value);
}, [value]);
const loadOptions = async (
inputValue: string,
) => {
if (!inputValue.length) {
const mappedValues = topValues.map((i) => ({
value: i.value,
label: i.value,
}));
setOptions(mappedValues);
return;
}
setLoading(true);
try {
const data = await searchService.fetchAutoCompleteValues({
..._params,
q: inputValue,
});
const _options =
data.map((i: any) => ({ value: i.value, label: i.value })) || [];
setOptions(_options);
} catch (e) {
throw new Error(e);
} finally {
setLoading(false);
}
};
const loadOptions = async (inputValue: string, callback: (options: { value: string; label: string }[]) => void) => {
if (!inputValue.length) {
const mappedValues = topValues.map((i) => ({ value: i.value, label: i.value }));
setOptions(mappedValues);
callback(mappedValues);
setLoading(false);
return;
}
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [
params,
topValues,
]);
try {
// const response = await new APIClient()[method.toLowerCase()](endpoint, { ..._params, q: inputValue });
const data = await searchService.fetchAutoCompleteValues({ ..._params, q: inputValue })
// const data = await response.json();
const _options = data.map((i: any) => ({ value: i.value, label: i.value })) || [];
setOptions(_options);
callback(_options);
} catch (e) {
throw new Error(e);
} finally {
setLoading(false);
}
};
const handleInputChange = (newValue: string) => {
setInitialFocus(true);
debouncedLoadOptions(newValue);
};
const debouncedLoadOptions = useCallback(debounce(loadOptions, 1000), [params, topValues]);
const handleInputChange = (newValue: string) => {
setLoading(true);
setInitialFocus(true);
setQuery(newValue);
debouncedLoadOptions(newValue, () => {
selectRef.current?.focus();
});
};
const handleChange = (item: { value: string }) => {
setMenuIsOpen(false);
setQuery(item.value);
onSelect(null, item.value);
};
const handleFocus = () => {
setInitialFocus(true);
if (!query.length) {
setLoading(topValuesLoading);
setMenuIsOpen(!topValuesLoading && topValues.length > 0);
const handleFocus = () => {
setInitialFocus(true);
setOptions(topValues.map((i) => ({ value: i.value, label: i.value })));
} else {
setMenuIsOpen(true);
}
};
};
const handleBlur = () => {
setMenuIsOpen(false);
setInitialFocus(false);
if (query !== previousQuery) {
onSelect(null, query);
}
setPreviousQuery(query);
};
console.log(options)
return <AutocompleteModal
values={values}
onClose={onClose}
onApply={onApply}
handleFocus={handleFocus}
loadOptions={handleInputChange}
options={options}
isLoading={loading}
/>
}
);
const selected = value ? options.find((i) => i.value === query) : null;
const uniqueOptions = options.filter((i) => i.value !== query);
const selectOptionsArr = query.length ? [{ value: query, label: query }, ...uniqueOptions] : options;
function AutoCompleteController(props: Props) {
return <AutoCompleteContainer {...props} modalRenderer={FilterAutoComplete} />
}
return (
<div className="relative flex items-center">
<div className={cn(stl.wrapper, 'relative')}>
<input
ref={inputRef}
className="w-full rounded px-2 no-focus"
value={query}
onChange={(e: ChangeEvent<HTMLInputElement>) => handleInputChange(e.target.value)}
onClick={handleFocus}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={placeholder}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
inputRef.current.blur();
}
}}
/>
{loading && (
<div
className="absolute top-0 right-0"
style={{
marginTop: '5px',
marginRight: !showCloseButton || (showCloseButton && !showOrButton) ? '34px' : '62px'
}}
>
<Icon name="spinner" className="animate-spin" size="14" />
</div>
)}
<Select
ref={selectRef}
options={selectOptionsArr}
value={selected}
onChange={(e) => handleChange(e as { value: string })}
menuIsOpen={initialFocus && menuIsOpen}
menuPlacement="auto"
styles={dropdownStyles}
components={{
Control: () => null
}}
/>
<div className={stl.right}>
{showCloseButton && (
<div onClick={onRemoveValue}>
<Icon name="close" size="12" />
</div>
)}
{/*{showOrButton && (*/}
{/* <div onClick={onAddValue} className="color-teal">*/}
{/* <span className="px-1">or</span>*/}
{/* </div>*/}
{/*)}*/}
</div>
</div>
</div>
);
};
export default observer(FilterAutoComplete);
export default AutoCompleteController;

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Icon } from 'UI';
import stl from './FilterAutoCompleteLocal.module.css';
import { Input } from 'antd';
interface Props {
showOrButton?: boolean;
@ -58,7 +59,7 @@ function FilterAutoCompleteLocal(props: Props & { index: number }) {
return (
<div className="relative flex items-center">
<div className={stl.wrapper}>
<input
<Input
name="query"
onInput={onInputChange}
value={query}

View file

@ -1,6 +1,6 @@
import React from 'react';
import styles from './FilterDuration.module.css';
import { Input } from 'UI'
import { Input } from 'antd'
const fromMs = value => value ? `${ value / 1000 / 60 }` : ''
const toMs = value => value !== '' ? value * 1000 * 60 : null

View file

@ -1,9 +1,7 @@
import { Space } from 'antd';
import { List } from 'immutable';
import { GripHorizontal } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import { Button, Card } from 'antd';
import { Button } from 'antd';
import { Icon } from 'UI';
import FilterItem from '../FilterItem';
@ -44,7 +42,6 @@ function FilterList(props: Props) {
const filters = filter.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;
@ -97,7 +94,7 @@ function FilterList(props: Props) {
(event: Record<string, any>) => {
event.preventDefault();
if (draggedInd === null) return;
const newItems = filters.toArray();
const newItems = filters;
const newPosition = calculateNewPosition(
draggedInd,
hoveredItem.i,
@ -107,23 +104,32 @@ function FilterList(props: Props) {
const reorderedItem = newItems.splice(draggedInd, 1)[0];
newItems.splice(newPosition, 0, reorderedItem);
props.onFilterMove?.(List(newItems));
props.onFilterMove?.(newItems);
setHoveredItem({ i: null, position: null });
setDraggedItem(null);
},
[draggedInd, hoveredItem, filters, props.onFilterMove]
);
const eventsNum = filters.filter((i: any) => i.isEvent).size;
const eventsNum = filters.filter((i: any) => i.isEvent).length;
return (
<div className="flex flex-col">
{onlyFilters ? null : (<Card size={'small'}>
<div className="flex items-center mb-2">
<div className="widget-wrapper flex flex-col">
{onlyFilters ? null : (<div className={'border-b border-b-gray-lighter py-2 px-4'}>
<div className="flex items-center mb-2 gap-2">
<div className="font-semibold">
{filter.eventsHeader || 'Events'}
</div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
<Button
icon={<Icon name={'filter'} />}
type="default"
size={'small'}
>
Add
</Button>
</FilterSelection>
<Space>
<div className={'ml-auto'}>
{!hideEventsOrder && (
<EventsOrder
filter={filter}
@ -132,7 +138,7 @@ function FilterList(props: Props) {
)}
{actions &&
actions.map((action, index) => <div key={index}>{action}</div>)}
</Space>
</div>
</div>
<div className={'flex flex-col'}>
{filters.map((filter: any, filterIndex: number) =>
@ -193,10 +199,9 @@ function FilterList(props: Props) {
) : null
)}
</div>
<div className="mb-2" />
</Card>)}
</div>)}
<Card size={'small'}>
<div className={'py-2 px-4'}>
<div className={'flex items-center gap-2 mb-2'}>
<div className="font-semibold">Filters</div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
@ -232,7 +237,7 @@ function FilterList(props: Props) {
</div>
) : null
)}
</Card>
</div>
</div>
);
}

View file

@ -17,6 +17,7 @@ const dropdownStyles = {
valueContainer: (provided: any) => ({
...provided,
width: 'fit-content',
height: 26,
'& input': {
marginTop: '-3px',
},

View file

@ -29,6 +29,10 @@ function FilterValue(props: Props) {
props.onUpdate({ ...filter, value: newValue });
};
const onApplyValues = (values: string[]) => {
props.onUpdate({ ...filter, value: values });
}
const onRemoveValue = (valueIndex: any) => {
const newValue = filter.value.filter(
(_: any, index: any) => index !== valueIndex
@ -37,6 +41,7 @@ function FilterValue(props: Props) {
};
const onChange = (e: any, item: any, valueIndex: any) => {
console.log('item', item, valueIndex);
const newValues = filter.value.map((_: any, _index: any) => {
if (_index === valueIndex) {
return item;
@ -86,7 +91,6 @@ function FilterValue(props: Props) {
const renderValueFiled = (value: any[]) => {
const showOrButton = filter.value.length > 1;
const valueIndex = 0;
const BaseFilterLocalAutoComplete = (props) => (
<FilterAutoCompleteLocal
value={value}
@ -98,6 +102,16 @@ function FilterValue(props: Props) {
{...props}
/>
);
const BaseDropDown = (props) => (
<FilterValueDropdown
value={value}
placeholder={filter.placeholder}
options={filter.options}
onApplyValues={onApplyValues}
// onChange={(item, index) => onChange(null, { value: item.value }, index)}
{...props}
/>
)
switch (filter.type) {
case FilterType.NUMBER_MULTIPLE:
return <BaseFilterLocalAutoComplete type="number" />;
@ -113,22 +127,13 @@ function FilterValue(props: Props) {
return <BaseFilterLocalAutoComplete />;
case FilterType.DROPDOWN:
return (
<FilterValueDropdown
value={value}
placeholder={filter.placeholder}
options={filter.options}
onChange={(item, index) => onChange(null, { value: item.value }, index)}
/>
<BaseDropDown />
);
case FilterType.ISSUE:
case FilterType.MULTIPLE_DROPDOWN:
return (
<FilterValueDropdown
<BaseDropDown
search={true}
value={value}
placeholder={filter.placeholder}
options={filter.options}
onChange={(item, index) => onChange(null, { value: item.value }, index)}
onAddValue={onAddValue}
onRemoveValue={(ind) => onRemoveValue(ind)}
showCloseButton={showCloseButton}
@ -151,14 +156,14 @@ function FilterValue(props: Props) {
value={value}
showCloseButton={showCloseButton}
showOrButton={showOrButton}
onAddValue={onAddValue}
onRemoveValue={() => onRemoveValue(valueIndex)}
onApplyValues={onApplyValues}
onRemoveValue={(index) => onRemoveValue(index)}
method={'GET'}
endpoint="/PROJECT_ID/events/search"
params={getParms(filter.key)}
headerText={''}
placeholder={filter.placeholder}
onSelect={(e, item) => onChange(e, item, valueIndex)}
onSelect={(e, item, index) => onChange(e, item, index)}
icon={filter.icon}
/>
);
@ -177,63 +182,4 @@ function FilterValue(props: Props) {
);
}
// const isEmpty = filter.value.length === 0 || !filter.value[0].length;
// return (
// <div
// className={
// 'rounded border border-gray-light px-2 relative w-fit whitespace-nowrap'
// }
// style={{ height: 26 }}
// ref={filterValueContainer}
// >
// <div onClick={() => setShowValueModal(true)} className={'flex items-center gap-2 '}>
// {!isEmpty ? (
// <>
// <div
// className={
// 'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'
// }
// >
// {filter.value[0]}
// </div>
// <div
// className={
// 'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'
// }
// >
// + {filter.value.length - 1} More
// </div>
// </>
// ) : (
// <div className={'text-disabled-text'}>Select values</div>
// )}
// </div>
// {showValueModal ? (
// <div
// className={cn(
// 'absolute left-0 mt-6 flex items-center gap-2 bg-white border shadow border-gray-light z-10',
// {
// 'grid-cols-2': filter.hasSource,
// 'grid-cols-3': !filter.hasSource,
// }
// )}
// style={{ minWidth: 200, minHeight: 100, top: '100%' }}
// >
// {filter.type === FilterType.DURATION
// ? renderValueFiled(filter.value, 0)
// : filter.value &&
// filter.value.map((value: any, valueIndex: any) => (
// <div key={valueIndex}>
// {renderValueFiled(value, valueIndex)}
// </div>
// ))}
// <div>
// <Button>Apply</Button>
// <Button>Cancel</Button>
// </div>
// </div>
// ) : null}
// </div>
// );
export default observer(FilterValue);

View file

@ -1,136 +1,41 @@
import React from 'react';
import { Icon } from 'UI';
import { AutoCompleteContainer, AutocompleteModal } from "../FilterAutoComplete/AutocompleteModal";
import stl from './FilterValueDropdown.module.css';
import Select from 'Shared/Select';
const dropdownStyles = {
control: (provided: any) => {
const obj = {
...provided,
border: 'solid thin transparent !important',
backgroundColor: 'transparent',
cursor: 'pointer',
height: '26px',
minHeight: '26px',
borderRadius: '.5rem',
boxShadow: 'none !important',
};
return obj;
},
valueContainer: (provided: any) => ({
...provided,
// paddingRight: '0px',
width: 'fit-content',
alignItems: 'center',
height: '26px',
padding: '0 3px',
}),
// placeholder: (provided: any) => ({
// ...provided,
// }),
indicatorsContainer: (provided: any) => ({
...provided,
padding: '0px',
height: '26px',
}),
option: (provided: any, state: any) => ({
...provided,
whiteSpace: 'nowrap',
}),
menu: (provided: any, state: any) => ({
...provided,
top: 20,
left: 0,
minWidth: 'fit-content',
overflow: 'hidden',
}),
container: (provided: any) => ({
...provided,
width: '100%',
}),
input: (provided: any) => ({
...provided,
height: '22px',
'& input:focus': {
border: 'none !important',
},
}),
singleValue: (provided: any, state: { isDisabled: any }) => {
const opacity = state.isDisabled ? 0.5 : 1;
const transition = 'opacity 300ms';
return {
...provided,
opacity,
transition,
display: 'flex',
alignItems: 'center',
height: '20px',
};
},
};
interface Props {
placeholder?: string;
value: string;
onChange: (value: any, ind: number) => void;
className?: string;
options: any[];
search?: boolean;
showCloseButton?: boolean;
showOrButton?: boolean;
onRemoveValue?: (ind: number) => void;
onAddValue?: (ind: number) => void;
isMultiple?: boolean;
index: number;
onApply: (values: string[]) => void;
onClose: () => void;
values: string[];
}
function FilterValueDropdown(props: Props) {
const {
placeholder = 'Select',
isMultiple = true,
search = false,
options,
onChange,
value,
showCloseButton = true,
showOrButton = true,
index,
onApply,
onClose,
values,
} = props;
const [query, setQuery] = React.useState('');
const filteredOptions = query.length ? options.filter((option) => {
return option.label.toLowerCase().includes(query.toLowerCase());
}) : options
return (
<div className="relative flex items-center w-full">
<div className={stl.wrapper}>
<Select
isSearchable={search}
options={options}
name="issue_type"
value={value ? options.find((item) => item.value === value) : null}
onChange={(value: any) => onChange(value.value, index)}
placeholder={placeholder}
styles={dropdownStyles}
/>
<div className={stl.right}>
{showCloseButton && (
<div onClick={() => props.onRemoveValue?.(index)}>
<Icon name="close" size="12" />
</div>
)}
{showOrButton && (
<div onClick={() => props.onAddValue?.(index)} className="color-teal">
<span className="px-1">or</span>
</div>
)}
</div>
</div>
{!showOrButton && isMultiple && <div className="ml-3">or</div>}
</div>
<AutocompleteModal
values={values}
onClose={onClose}
onApply={onApply}
loadOptions={setQuery}
options={filteredOptions}
/>
);
}
interface MainProps {
placeholder?: string;
value: string[];
onChange: (value: any, ind: number) => void;
onApplyValues: (values: string[]) => void;
className?: string;
options: any[];
search?: boolean;
@ -142,16 +47,17 @@ interface MainProps {
}
function FilterDropdownController(props: MainProps) {
return props.value.map((value, index) => (
<FilterValueDropdown
{...props}
key={index}
value={value}
index={index}
showOrButton={index === props.value.length - 1}
showCloseButton={props.value.length > 1}
/>
))
const mapValues = (value: string) => {
return props.options.find((option) => option.value === value)?.label || value;
}
return <AutoCompleteContainer
value={props.value}
onApplyValues={props.onApplyValues}
modalRenderer={FilterValueDropdown}
modalProps={{ options: props.options }}
mapValues={mapValues}
/>
}
export default FilterDropdownController;

View file

@ -1,25 +1,13 @@
import React from 'react';
import SessionSearchField from 'Shared/SessionSearchField';
import AiSessionSearchField from 'Shared/SessionSearchField/AiSessionSearchField';
import SavedSearch from 'Shared/SavedSearch';
import { Button } from 'antd';
import TagList from './components/TagList';
import SessionFilters from 'Shared/SessionFilters';
import AiSessionSearchField from 'Shared/SessionFilters/AiSessionSearchField';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
interface Props {
}
const MainSearchBar = (props: Props) => {
const MainSearchBar = () => {
const { searchStore, projectsStore } = useStore();
const appliedFilter = searchStore.instance;
const savedSearch = searchStore.savedSearch;
const projectId = projectsStore.siteId;
const currSite = React.useRef(projectId);
const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.length > 0;
const hasSavedSearch = savedSearch && savedSearch.exists();
const hasSearch = hasFilters || hasSavedSearch;
// @ts-ignore
const originStr = window.env.ORIGIN || window.location.origin;
@ -33,24 +21,9 @@ const MainSearchBar = (props: Props) => {
}
}, [projectId]);
return (
<div className="flex items-center flex-wrap">
<div style={{ flex: 3, marginRight: '10px' }}>
{isSaas ? <AiSessionSearchField /> : <SessionSearchField />}
</div>
<div className="flex items-center gap-2 my-2 xl:my-0" style={{ flex: 2 }}>
<TagList />
<SavedSearch />
<Button
// variant={hasSearch ? 'text-primary' : 'text'}
// className="ml-auto font-medium"
type="link"
disabled={!hasSearch}
onClick={() => searchStore.clearSearch()}
className="ml-auto font-medium"
>
Clear Search
</Button>
</div>
<div className={'flex flex-col gap-2 w-full'}>
{isSaas ? <AiSessionSearchField /> : null}
<SessionFilters />
</div>
);
};

View file

@ -1,24 +1,28 @@
import React, { useState } from 'react';
import { Button } from 'UI';
import { Button } from 'antd';
import SaveSearchModal from 'Shared/SaveSearchModal';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
function SaveFilterButton() {
function SaveFilterButton({ disabled }: { disabled?: boolean }) {
const { searchStore } = useStore();
const savedSearch = searchStore.savedSearch;
const [showModal, setshowModal] = useState(false);
return (
<div>
<Button
variant="text-primary"
className="mr-2"
type="link"
disabled={disabled}
onClick={() => setshowModal(true)}
icon="zoom-in">
{savedSearch.exists() ? 'UPDATE SEARCH' : 'SAVE SEARCH'}
>
{savedSearch.exists() ? 'Update Search' : 'Save Search'}
</Button>
{showModal && (<SaveSearchModal show={showModal} closeHandler={() => setshowModal(false)} />)}
{showModal && (
<SaveSearchModal
show={showModal}
closeHandler={() => setshowModal(false)}
/>
)}
</div>
);
}

View file

@ -0,0 +1,53 @@
import React from 'react';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import SaveFilterButton from 'Shared/SaveFilterButton';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import TagList from '../MainSearchBar/components/TagList';
import SavedSearch from '../SavedSearch/SavedSearch';
import { Button } from 'antd';
function SearchActions() {
const { aiFiltersStore, searchStore, customFieldStore } = useStore();
const appliedFilter = searchStore.instance;
const metaLoading = customFieldStore.isLoading;
const hasEvents =
appliedFilter.filters.filter((i: any) => i.isEvent).length > 0;
const hasFilters =
appliedFilter.filters.filter((i: any) => !i.isEvent).length > 0;
const savedSearch = searchStore.savedSearch;
const hasSavedSearch = savedSearch && savedSearch.exists();
const hasSearch = hasFilters || hasSavedSearch;
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
return !metaLoading ? (
<div className={'mb-2'}>
<div className={'flex items-center gap-2 w-full'}>
<TagList />
<SavedSearch />
<div className={'ml-auto'} />
<Button
type="link"
disabled={!hasSearch}
onClick={() => searchStore.clearSearch()}
className="font-medium"
>
Clear Search
</Button>
<SaveFilterButton disabled={!hasEvents && !hasFilters} />
</div>
{showPanel ? (
<>
{aiFiltersStore.isLoading ? (
<div className={'font-semibold flex items-center gap-2 p-4'}>
<AnimatedSVG name={ICONS.LOADER} size={18} />
<span>Translating your query into search steps...</span>
</div>
) : null}
</>
) : null}
</div>
) : null;
}
export default observer(SearchActions);

View file

@ -0,0 +1 @@
export { default } from './SearchActions';

View file

@ -0,0 +1,97 @@
import React, { useEffect } from 'react';
import { debounce } from 'App/utils';
import FilterList from 'Shared/Filters/FilterList';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler';
import { FilterKey } from 'App/types/filter/filterType';
import { addOptionsToFilter } from 'App/types/filter/newFilter';
let debounceFetch: any = () => {};
function SessionFilters() {
const {
searchStore,
projectsStore,
customFieldStore,
tagWatchStore,
} = useStore();
const appliedFilter = searchStore.instance;
const metaLoading = customFieldStore.isLoading;
const saveRequestPayloads =
projectsStore.instance?.saveRequestPayloads ?? false;
useSessionSearchQueryHandler({
appliedFilter,
loading: metaLoading,
onBeforeLoad: async () => {
const tags = await tagWatchStore.getTags();
if (tags) {
addOptionsToFilter(
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({
label: tag.name,
value: tag.tagId.toString(),
}))
);
searchStore.refreshFilterOptions();
}
},
});
useEffect(() => {
debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
}, []);
useEffect(() => {
debounceFetch();
}, [appliedFilter.filters]);
const onAddFilter = (filter: any) => {
searchStore.addFilter(filter);
};
const onUpdateFilter = (filterIndex: any, filter: any) => {
searchStore.updateFilter(filterIndex, filter);
};
const onFilterMove = (newFilters: any) => {
searchStore.updateFilter(0, {
...appliedFilter,
filters: newFilters,
});
debounceFetch();
};
const onRemoveFilter = (filterIndex: any) => {
searchStore.removeFilter(filterIndex);
debounceFetch();
};
const onChangeEventsOrder = (e: any, { value }: any) => {
searchStore.edit({
eventsOrder: value,
});
debounceFetch();
};
return (
<div className="relative">
<FilterList
filter={appliedFilter}
onAddFilter={onAddFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
saveRequestPayloads={saveRequestPayloads}
onFilterMove={onFilterMove}
/>
</div>
);
}
export default observer(SessionFilters);

View file

@ -0,0 +1 @@
export { default } from './SessionFilters';

View file

@ -1,133 +0,0 @@
import React, { useEffect } from 'react';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import FilterList from 'Shared/Filters/FilterList';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SaveFilterButton from 'Shared/SaveFilterButton';
import { FilterKey } from 'Types/filter/filterType';
import { addOptionsToFilter } from 'Types/filter/newFilter';
import { Button, Loader } from 'UI';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import { debounce } from 'App/utils';
import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler';
let debounceFetch: any = () => {
};
function SessionSearch() {
const { tagWatchStore, aiFiltersStore, searchStore, customFieldStore, projectsStore } = useStore();
const appliedFilter = searchStore.instance;
const metaLoading = customFieldStore.isLoading;
const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).length > 0;
const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).length > 0;
const saveRequestPayloads = projectsStore.instance?.saveRequestPayloads ?? false;
useSessionSearchQueryHandler({
appliedFilter,
loading: metaLoading,
onBeforeLoad: async () => {
const tags = await tagWatchStore.getTags();
if (tags) {
addOptionsToFilter(
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({
label: tag.name,
value: tag.tagId.toString()
}))
);
searchStore.refreshFilterOptions();
}
}
});
useEffect(() => {
debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
// void searchStore.fetchSessions(true)
}, []);
useEffect(() => {
debounceFetch();
}, [appliedFilter.filters]);
const onAddFilter = (filter: any) => {
searchStore.addFilter(filter);
};
const onUpdateFilter = (filterIndex: any, filter: any) => {
searchStore.updateFilter(filterIndex, filter);
};
const onFilterMove = (newFilters: any) => {
searchStore.updateFilter(0, {
...appliedFilter,
filters: newFilters
});
debounceFetch();
};
const onRemoveFilter = (filterIndex: any) => {
const newFilters = appliedFilter.filters.filter((_filter: any, i: any) => {
return i !== filterIndex;
});
searchStore.removeFilter(filterIndex);
debounceFetch();
};
const onChangeEventsOrder = (e: any, { value }: any) => {
searchStore.edit({
eventsOrder: value
});
debounceFetch();
};
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
return !metaLoading ? (
<>
{showPanel ? (
<div className="border bg-white rounded-lg mt-4">
<div className="p-5">
{aiFiltersStore.isLoading ? (
<div className={'font-semibold flex items-center gap-2 mb-2'}>
<AnimatedSVG name={ICONS.LOADER} size={18} />
<span>Translating your query into search steps...</span>
</div>
) : null}
{hasEvents || hasFilters ? (
<FilterList
filter={appliedFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
onFilterMove={onFilterMove}
saveRequestPayloads={saveRequestPayloads}
/>
) : null}
</div>
{hasEvents || hasFilters ? (
<div className="border-t px-5 py-1 flex items-center -mx-2">
<div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
<Button variant="text-primary" className="mr-2" icon="plus">
ADD STEP
</Button>
</FilterSelection>
</div>
<div className="ml-auto flex items-center">
<SaveFilterButton />
</div>
</div>
) : null}
</div>
) : (
<></>
)}
</>
) : null;
}
export default observer(SessionSearch);

View file

@ -1 +0,0 @@
export { default } from './SessionSearch';

View file

@ -1,71 +0,0 @@
import React, { useState } from 'react';
import { Input } from 'UI';
import FilterModal from 'Shared/Filters/FilterModal';
import { debounce } from 'App/utils';
import { assist as assistRoute, isRoute } from 'App/routes';
const ASSIST_ROUTE = assistRoute();
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
interface Props {
}
function SessionSearchField(props: Props) {
const { searchStore, searchStoreLive } = useStore();
const isLive =
isRoute(ASSIST_ROUTE, window.location.pathname) ||
window.location.pathname.includes('multiview');
const fetchFilterSearch = isLive
? searchStoreLive.fetchFilterSearch.bind(searchStoreLive)
: searchStore.fetchFilterSearch.bind(searchStore);
const debounceFetchFilterSearch = React.useCallback(
debounce(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);
};
return (
<div className="relative">
<Input
icon="search"
onFocus={() => setShowModal(true)}
onBlur={() => setTimeout(setShowModal, 200, false)}
onChange={onSearchChange}
placeholder={'Search sessions using any captured event (click, input, page, error...)'}
style={{ minWidth: 360 }}
id="search"
type="search"
autoComplete="off"
className="hover:border-gray-medium text-lg placeholder-lg h-9 shadow-sm"
/>
{showModal && (
<div className="absolute left-0 shadow-sm rounded-lg bg-white z-50">
<FilterModal
isMainSearch={true}
onFilterClick={onAddFilter}
isLive={isLive}
/>
</div>
)}
</div>
);
}
export default observer(SessionSearchField);

View file

@ -1 +0,0 @@
export { default } from './SessionSearchField';

View file

@ -436,3 +436,9 @@ p {
display: flex;
align-items: center;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none; /* Remove default styling */
appearance: none;
margin: 0; /* Fix margin if necessary */
}