feat(ui): dynamic fitlers
This commit is contained in:
parent
235364b968
commit
7cf4f50b36
22 changed files with 1389 additions and 928 deletions
|
|
@ -102,7 +102,7 @@ const HIGHLIGHTS_PATH = routes.highlights();
|
|||
const KAI_PATH = routes.kai();
|
||||
|
||||
function PrivateRoutes() {
|
||||
const { projectsStore, userStore, integrationsStore, searchStore } =
|
||||
const { projectsStore, userStore, integrationsStore, searchStore, filterStore } =
|
||||
useStore();
|
||||
const onboarding = userStore.onboarding;
|
||||
const scope = userStore.scopeState;
|
||||
|
|
@ -121,6 +121,7 @@ function PrivateRoutes() {
|
|||
if (siteId && integrationsStore.integrations.siteId !== siteId) {
|
||||
integrationsStore.integrations.setSiteId(siteId);
|
||||
void integrationsStore.integrations.fetchIntegrations(siteId);
|
||||
void filterStore.fetchFilters(siteId)
|
||||
}
|
||||
}, [siteId]);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { PlusIcon } from 'lucide-react';
|
|||
import { Button } from 'antd';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Filter } from '@/mstore/types/filterConstants';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface Props {
|
||||
series: any;
|
||||
|
|
@ -12,8 +14,10 @@ interface Props {
|
|||
|
||||
function AddStepButton({ series, excludeFilterKeys }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { metricStore } = useStore();
|
||||
const { metricStore, filterStore } = useStore();
|
||||
const metric: any = metricStore.instance;
|
||||
const filters: Filter[] = filterStore.getCurrentProjectFilters();
|
||||
// console.log('filters', filters)
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
series.filter.addFilter(filter);
|
||||
|
|
@ -21,9 +25,9 @@ function AddStepButton({ series, excludeFilterKeys }: Props) {
|
|||
};
|
||||
return (
|
||||
<FilterSelection
|
||||
filter={undefined}
|
||||
filters={filters}
|
||||
onFilterClick={onAddFilter}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
mode={'filters'} // excludeFilterKeys={excludeFilterKeys}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
|
|
@ -37,4 +41,4 @@ function AddStepButton({ series, excludeFilterKeys }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default AddStepButton;
|
||||
export default observer(AddStepButton);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
newFFlag,
|
||||
fflag,
|
||||
fflagRead,
|
||||
bookmarks,
|
||||
bookmarks
|
||||
} from 'App/routes';
|
||||
import { withRouter, RouteComponentProps, useLocation } from 'react-router-dom';
|
||||
import FlagView from 'Components/FFlags/FlagView/FlagView';
|
||||
|
|
@ -29,15 +29,18 @@ interface IProps extends RouteComponentProps {
|
|||
};
|
||||
};
|
||||
}
|
||||
|
||||
// TODO should move these routes to the Routes file
|
||||
function Overview({ match: { params } }: IProps) {
|
||||
const { searchStore } = useStore();
|
||||
const { searchStore, filterStore, projectsStore } = useStore();
|
||||
const { siteId, fflagId } = params;
|
||||
const location = useLocation();
|
||||
const tab = location.pathname.split('/')[2];
|
||||
const { activeSiteId } = projectsStore;
|
||||
|
||||
React.useEffect(() => {
|
||||
searchStore.setActiveTab(tab);
|
||||
// void filterStore.fetchFilters(activeSiteId + '');
|
||||
}, [tab]);
|
||||
|
||||
return (
|
||||
|
|
@ -69,5 +72,5 @@ function Overview({ match: { params } }: IProps) {
|
|||
}
|
||||
|
||||
export default withPageTitle('Sessions - OpenReplay')(
|
||||
withRouter(observer(Overview)),
|
||||
withRouter(observer(Overview))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const FilterAutoComplete = observer(
|
|||
onClose,
|
||||
onApply,
|
||||
values,
|
||||
placeholder,
|
||||
placeholder
|
||||
}: {
|
||||
params: any;
|
||||
values: string[];
|
||||
|
|
@ -57,32 +57,32 @@ const FilterAutoComplete = observer(
|
|||
placeholder?: string;
|
||||
}) => {
|
||||
const [options, setOptions] = useState<{ value: string; label: string }[]>(
|
||||
[],
|
||||
[]
|
||||
);
|
||||
const [initialFocus, setInitialFocus] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { filterStore, projectsStore } = useStore();
|
||||
const _params = processKey(params);
|
||||
const filterKey = `${projectsStore.siteId}_${_params.type}${_params.key || ''}`;
|
||||
const filterKey = `${projectsStore.siteId}_${params.id}`;
|
||||
const topValues = filterStore.topValues[filterKey] || [];
|
||||
|
||||
React.useEffect(() => {
|
||||
setOptions([])
|
||||
}, [projectsStore.siteId])
|
||||
setOptions([]);
|
||||
}, [projectsStore.siteId]);
|
||||
|
||||
const loadTopValues = async () => {
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
if (projectsStore.siteId) {
|
||||
await filterStore.fetchTopValues(_params.type, projectsStore.siteId, _params.key);
|
||||
await filterStore.fetchTopValues(params.id, projectsStore.siteId);
|
||||
}
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (topValues.length > 0) {
|
||||
const mappedValues = topValues.map((i) => ({
|
||||
value: i.value,
|
||||
label: i.value,
|
||||
label: i.value
|
||||
}));
|
||||
setOptions(mappedValues);
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ const FilterAutoComplete = observer(
|
|||
if (!inputValue.length) {
|
||||
const mappedValues = topValues.map((i) => ({
|
||||
value: i.value,
|
||||
label: i.value,
|
||||
label: i.value
|
||||
}));
|
||||
setOptions(mappedValues);
|
||||
return;
|
||||
|
|
@ -104,8 +104,8 @@ const FilterAutoComplete = observer(
|
|||
setLoading(true);
|
||||
try {
|
||||
const data = await searchService.fetchAutoCompleteValues({
|
||||
..._params,
|
||||
q: inputValue,
|
||||
type: params.name?.toLowerCase(),
|
||||
q: inputValue
|
||||
});
|
||||
const _options =
|
||||
data.map((i: any) => ({ value: i.value, label: i.value })) || [];
|
||||
|
|
@ -119,7 +119,7 @@ const FilterAutoComplete = observer(
|
|||
|
||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [
|
||||
params,
|
||||
topValues,
|
||||
topValues
|
||||
]);
|
||||
|
||||
const handleInputChange = (newValue: string) => {
|
||||
|
|
@ -146,7 +146,7 @@ const FilterAutoComplete = observer(
|
|||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function AutoCompleteController(props: Props) {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { FilterKey, FilterType } from 'App/types/filter/filterType';
|
||||
import { CircleMinus } from 'lucide-react';
|
||||
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
||||
import { Button, Space, Typography } from 'antd';
|
||||
import { FilterKey } from 'App/types/filter/filterType';
|
||||
import { CircleMinus, Filter as FilterIcon } from 'lucide-react';
|
||||
import cn from 'classnames';
|
||||
import FilterOperator from '../FilterOperator';
|
||||
import FilterSelection from '../FilterSelection';
|
||||
import FilterValue from '../FilterValue';
|
||||
import FilterSource from '../FilterSource';
|
||||
import SubFilterItem from '../SubFilterItem';
|
||||
import { useStore } from '@/mstore';
|
||||
import { getIconForFilter } from 'Shared/Filters/FilterModal/FilterModal';
|
||||
import { Filter, getOperatorsByType } from '@/mstore/types/filterConstants';
|
||||
|
||||
interface Props {
|
||||
filterIndex?: number;
|
||||
filter: any; // event/filter
|
||||
filter: any;
|
||||
onUpdate: (filter: any) => void;
|
||||
onRemoveFilter: () => void;
|
||||
isFilter?: boolean;
|
||||
saveRequestPayloads?: boolean;
|
||||
disableDelete?: boolean;
|
||||
excludeFilterKeys?: Array<string>;
|
||||
excludeCategory?: Array<string>;
|
||||
allowedFilterKeys?: Array<string>;
|
||||
readonly?: boolean;
|
||||
hideIndex?: boolean;
|
||||
hideDelete?: boolean;
|
||||
isConditional?: boolean;
|
||||
isSubItem?: boolean;
|
||||
subFilterIndex?: number;
|
||||
propertyOrder?: string;
|
||||
onToggleOperator?: (newOp: string) => void;
|
||||
}
|
||||
|
||||
function FilterItem(props: Props) {
|
||||
|
|
@ -34,162 +37,290 @@ function FilterItem(props: Props) {
|
|||
saveRequestPayloads,
|
||||
disableDelete = false,
|
||||
hideDelete = false,
|
||||
allowedFilterKeys = [],
|
||||
excludeFilterKeys = [],
|
||||
excludeCategory = [],
|
||||
isConditional,
|
||||
hideIndex = false,
|
||||
onUpdate,
|
||||
onRemoveFilter,
|
||||
readonly,
|
||||
isSubItem = false,
|
||||
subFilterIndex,
|
||||
propertyOrder,
|
||||
onToggleOperator
|
||||
} = props;
|
||||
const canShowValues = !(
|
||||
const [eventFilterOptions, setEventFilterOptions] = useState<Filter[]>([]);
|
||||
|
||||
const { filterStore } = useStore();
|
||||
const allFilters = filterStore.getCurrentProjectFilters();
|
||||
const eventSelections = allFilters.filter((i) => i.isEvent === filter.isEvent);
|
||||
const filterSelections = isSubItem ? eventFilterOptions : eventSelections;
|
||||
|
||||
|
||||
const [eventFiltersLoading, setEventFiltersLoading] = useState(false);
|
||||
const operatorOptions = getOperatorsByType(filter.type);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
async function loadFilters() {
|
||||
try {
|
||||
setEventFiltersLoading(true);
|
||||
const options = await filterStore.getEventFilters(filter.name);
|
||||
setEventFilterOptions(options);
|
||||
} finally {
|
||||
setEventFiltersLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void loadFilters();
|
||||
}, [filter.name]); // Re-fetch when filter name changes
|
||||
|
||||
const canShowValues = useMemo(
|
||||
() =>
|
||||
!(
|
||||
filter.operator === 'isAny' ||
|
||||
filter.operator === 'onAny' ||
|
||||
filter.operator === 'isUndefined'
|
||||
),
|
||||
[filter.operator]
|
||||
);
|
||||
const isSubFilter = filter.type === FilterType.SUB_FILTERS;
|
||||
const replaceFilter = (filter: any) => {
|
||||
props.onUpdate({
|
||||
|
||||
const isReversed = useMemo(() => filter.key === FilterKey.TAGGED_ELEMENT, [filter.key]);
|
||||
|
||||
const replaceFilter = useCallback(
|
||||
(selectedFilter: any) => {
|
||||
onUpdate({
|
||||
...selectedFilter,
|
||||
value: selectedFilter.value,
|
||||
filters: selectedFilter.filters
|
||||
? selectedFilter.filters.map((i: any) => ({ ...i, value: [''] }))
|
||||
: []
|
||||
});
|
||||
},
|
||||
[onUpdate]
|
||||
);
|
||||
|
||||
const handleOperatorChange = useCallback(
|
||||
(e: any, { value }: any) => {
|
||||
onUpdate({ ...filter, operator: value });
|
||||
},
|
||||
[filter, onUpdate]
|
||||
);
|
||||
|
||||
const handleSourceOperatorChange = useCallback(
|
||||
(e: any, { value }: any) => {
|
||||
onUpdate({ ...filter, sourceOperator: value });
|
||||
},
|
||||
[filter, onUpdate]
|
||||
);
|
||||
|
||||
const handleUpdateSubFilter = useCallback(
|
||||
(subFilter: any, index: number) => {
|
||||
onUpdate({
|
||||
...filter,
|
||||
value: filter.value,
|
||||
filters: filter.filters
|
||||
? filter.filters.map((i: any) => ({ ...i, value: [''] }))
|
||||
filters: filter.filters.map((i: any, idx: number) => (idx === index ? subFilter : i))
|
||||
});
|
||||
},
|
||||
[filter, onUpdate]
|
||||
);
|
||||
|
||||
const handleRemoveSubFilter = useCallback(
|
||||
(index: number) => {
|
||||
onUpdate({
|
||||
...filter,
|
||||
filters: filter.filters.filter((_: any, idx: number) => idx !== index)
|
||||
});
|
||||
},
|
||||
[filter, onUpdate]
|
||||
);
|
||||
|
||||
const filteredSubFilters = useMemo(
|
||||
() =>
|
||||
filter.filters
|
||||
? filter.filters.filter(
|
||||
(i: any) =>
|
||||
(i.key !== FilterKey.FETCH_REQUEST_BODY && i.key !== FilterKey.FETCH_RESPONSE_BODY) ||
|
||||
saveRequestPayloads
|
||||
)
|
||||
: [],
|
||||
});
|
||||
};
|
||||
[filter.filters, saveRequestPayloads]
|
||||
);
|
||||
|
||||
const onOperatorChange = (e: any, { value }: any) => {
|
||||
props.onUpdate({ ...filter, operator: value });
|
||||
};
|
||||
|
||||
const onSourceOperatorChange = (e: any, { value }: any) => {
|
||||
props.onUpdate({ ...filter, sourceOperator: value });
|
||||
};
|
||||
|
||||
const onUpdateSubFilter = (subFilter: any, subFilterIndex: any) => {
|
||||
props.onUpdate({
|
||||
const addSubFilter = useCallback(
|
||||
(selectedFilter: any) => {
|
||||
onUpdate({
|
||||
...filter,
|
||||
filters: filter.filters.map((i: any, index: any) => {
|
||||
if (index === subFilterIndex) {
|
||||
return subFilter;
|
||||
}
|
||||
return i;
|
||||
}),
|
||||
filters: [...filteredSubFilters, selectedFilter]
|
||||
});
|
||||
};
|
||||
},
|
||||
[filter, onUpdate]
|
||||
);
|
||||
|
||||
const isReversed = filter.key === FilterKey.TAGGED_ELEMENT;
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex items-center w-full flex-wrap">
|
||||
{!isFilter && !hideIndex && filterIndex >= 0 && (
|
||||
<div className="flex-shrink-0 w-6 h-6 text-xs flex items-center justify-center rounded-full bg-gray-lighter mr-2">
|
||||
<div className="flex items-center flex-grow flex-wrap">
|
||||
{!isFilter && !hideIndex && filterIndex !== undefined && filterIndex >= 0 && (
|
||||
<div
|
||||
className="flex-shrink-0 w-6 h-6 text-xs flex items-center justify-center rounded-full bg-gray-lighter mr-2">
|
||||
<span>{filterIndex + 1}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSubItem && (
|
||||
<div className="w-14 text-right">
|
||||
{subFilterIndex === 0 && (
|
||||
<Typography.Text className="text-neutral-500/90 mr-2">
|
||||
where
|
||||
</Typography.Text>
|
||||
)}
|
||||
{subFilterIndex != 0 && propertyOrder && onToggleOperator && (
|
||||
<Typography.Text
|
||||
className="text-neutral-500/90 mr-2 cursor-pointer"
|
||||
onClick={() =>
|
||||
onToggleOperator(propertyOrder === 'and' ? 'or' : 'and')
|
||||
}
|
||||
>
|
||||
{propertyOrder}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FilterSelection
|
||||
filter={filter}
|
||||
mode={props.isFilter ? 'filters' : 'events'}
|
||||
filters={filterSelections}
|
||||
onFilterClick={replaceFilter}
|
||||
allowedFilterKeys={allowedFilterKeys}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
excludeCategory={excludeCategory}
|
||||
disabled={disableDelete || props.readonly}
|
||||
/>
|
||||
disabled={disableDelete || readonly}
|
||||
>
|
||||
<Space
|
||||
className={cn(
|
||||
'rounded-lg py-1 px-2 cursor-pointer bg-white border border-gray-light text-ellipsis hover:border-neutral-400 btn-select-event',
|
||||
{ 'opacity-50 pointer-events-none': disableDelete || readonly }
|
||||
)}
|
||||
style={{ height: '26px' }}
|
||||
>
|
||||
<div className="text-xs">
|
||||
{filter && getIconForFilter(filter)}
|
||||
</div>
|
||||
<div className="text-neutral-500/90 capitalize">
|
||||
{`${filter?.subCategory ? filter.subCategory : filter?.category}`}
|
||||
</div>
|
||||
<span className="text-neutral-500/90">•</span>
|
||||
<div
|
||||
className="rounded-lg overflow-hidden whitespace-nowrap text-ellipsis mr-auto truncate"
|
||||
style={{ textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{filter.displayName || filter.name}
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
</FilterSelection>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center flex-wrap',
|
||||
isReversed ? 'flex-row-reverse ml-2' : 'flex-row',
|
||||
isReversed ? 'flex-row-reverse ml-2' : 'flex-row'
|
||||
)}
|
||||
>
|
||||
{/* Filter with Source */}
|
||||
{filter.hasSource && (
|
||||
<>
|
||||
<FilterOperator
|
||||
options={filter.sourceOperatorOptions}
|
||||
onChange={onSourceOperatorChange}
|
||||
onChange={handleSourceOperatorChange}
|
||||
className="mx-2 flex-shrink-0 btn-event-operator"
|
||||
value={filter.sourceOperator}
|
||||
isDisabled={filter.operatorDisabled || props.readonly}
|
||||
isDisabled={filter.operatorDisabled || readonly}
|
||||
/>
|
||||
<FilterSource filter={filter} onUpdate={props.onUpdate} />
|
||||
<FilterSource filter={filter} onUpdate={onUpdate} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Filter values */}
|
||||
{!isSubFilter && filter.operatorOptions && (
|
||||
{operatorOptions.length && (
|
||||
<>
|
||||
<FilterOperator
|
||||
options={filter.operatorOptions}
|
||||
onChange={onOperatorChange}
|
||||
options={operatorOptions}
|
||||
onChange={handleOperatorChange}
|
||||
className="mx-2 flex-shrink-0 btn-sub-event-operator"
|
||||
value={filter.operator}
|
||||
isDisabled={filter.operatorDisabled || props.readonly}
|
||||
isDisabled={filter.operatorDisabled || readonly}
|
||||
/>
|
||||
{canShowValues && (
|
||||
<>
|
||||
{props.readonly ? (
|
||||
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip hover:border-neutral-400">
|
||||
{canShowValues &&
|
||||
(readonly ? (
|
||||
<div
|
||||
className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip hover:border-neutral-400">
|
||||
{filter.value
|
||||
.map((val: string) =>
|
||||
filter.options && filter.options.length
|
||||
? (filter.options[
|
||||
filter.options.findIndex(
|
||||
(i: any) => i.value === val,
|
||||
)
|
||||
]?.label ?? val)
|
||||
: val,
|
||||
? filter.options[
|
||||
filter.options.findIndex((i: any) => i.value === val)
|
||||
]?.label ?? val
|
||||
: val
|
||||
)
|
||||
.join(', ')}
|
||||
</div>
|
||||
) : (
|
||||
<FilterValue
|
||||
isConditional={isConditional}
|
||||
filter={filter}
|
||||
onUpdate={props.onUpdate}
|
||||
<FilterValue isConditional={isConditional} filter={filter} onUpdate={onUpdate} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!readonly && !hideDelete && (
|
||||
<div className="flex flex-shrink-0 gap-2">
|
||||
{filter.isEvent && !isSubItem && (
|
||||
<FilterSelection
|
||||
filters={eventFilterOptions}
|
||||
onFilterClick={addSubFilter}
|
||||
disabled={disableDelete || readonly}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FilterIcon size={13} />}
|
||||
size="small"
|
||||
aria-label="Add filter"
|
||||
title="Filter"
|
||||
/>
|
||||
</FilterSelection>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CircleMinus size={13} />}
|
||||
disabled={disableDelete}
|
||||
onClick={onRemoveFilter}
|
||||
size="small"
|
||||
aria-label="Remove filter"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* filters */}
|
||||
{isSubFilter && (
|
||||
<div className="grid grid-col ml-3 w-full">
|
||||
{filter.filters
|
||||
.filter(
|
||||
(i: any) =>
|
||||
(i.key !== FilterKey.FETCH_REQUEST_BODY &&
|
||||
i.key !== FilterKey.FETCH_RESPONSE_BODY) ||
|
||||
saveRequestPayloads,
|
||||
)
|
||||
.map((subFilter: any, subFilterIndex: any) => (
|
||||
<SubFilterItem
|
||||
filterIndex={subFilterIndex}
|
||||
{filter.filters?.length > 0 && (
|
||||
<div className="pl-8 w-full">
|
||||
{filteredSubFilters.map((subFilter: any, index: number) => (
|
||||
<FilterItem
|
||||
key={`subfilter-${index}`}
|
||||
filter={subFilter}
|
||||
onUpdate={(f) => onUpdateSubFilter(f, subFilterIndex)}
|
||||
onRemoveFilter={props.onRemoveFilter}
|
||||
subFilterIndex={index}
|
||||
onUpdate={(updatedSubFilter) => handleUpdateSubFilter(updatedSubFilter, index)}
|
||||
onRemoveFilter={() => handleRemoveSubFilter(index)}
|
||||
isFilter={isFilter}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
disableDelete={disableDelete}
|
||||
readonly={readonly}
|
||||
hideIndex={hideIndex}
|
||||
hideDelete={hideDelete}
|
||||
isConditional={isConditional}
|
||||
isSubItem={true}
|
||||
propertyOrder={filter.propertyOrder || 'and'}
|
||||
onToggleOperator={(newOp) =>
|
||||
onUpdate({ ...filter, propertyOrder: newOp })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{props.readonly || props.hideDelete ? null : (
|
||||
<div className="flex flex-shrink-0 self-start ml-auto">
|
||||
<Button
|
||||
disabled={disableDelete}
|
||||
type="text"
|
||||
onClick={props.onRemoveFilter}
|
||||
size="small"
|
||||
className="btn-remove-step mt-2"
|
||||
>
|
||||
<CircleMinus size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterItem;
|
||||
export default React.memo(FilterItem);
|
||||
|
|
|
|||
|
|
@ -4,34 +4,34 @@ import { Dropdown, Button, Tooltip } from 'antd';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const EventsOrder = observer(
|
||||
(props: { onChange: (e: any, v: any) => void; filter: any }) => {
|
||||
const { filter, onChange } = props;
|
||||
const { eventsOrderSupport } = filter;
|
||||
(props: { onChange: (e: any, v: any) => void; orderProps: any }) => {
|
||||
const { onChange, orderProps: { eventsOrder, eventsOrderSupport } } = props;
|
||||
// const { eventsOrderSupport } = filter;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: 'then',
|
||||
label: t('THEN'),
|
||||
disabled: eventsOrderSupport && !eventsOrderSupport.includes('then'),
|
||||
disabled: eventsOrderSupport && !eventsOrderSupport.includes('then')
|
||||
},
|
||||
{
|
||||
key: 'and',
|
||||
label: t('AND'),
|
||||
disabled: eventsOrderSupport && !eventsOrderSupport.includes('and'),
|
||||
disabled: eventsOrderSupport && !eventsOrderSupport.includes('and')
|
||||
},
|
||||
{
|
||||
key: 'or',
|
||||
label: t('OR'),
|
||||
disabled: eventsOrderSupport && !eventsOrderSupport.includes('or'),
|
||||
},
|
||||
disabled: eventsOrderSupport && !eventsOrderSupport.includes('or')
|
||||
}
|
||||
];
|
||||
const onClick = ({ key }: any) => {
|
||||
onChange(null, { name: 'eventsOrder', value: key, key });
|
||||
};
|
||||
|
||||
const selected = menuItems.find(
|
||||
(item) => item.key === filter.eventsOrder,
|
||||
(item) => item.key === eventsOrder
|
||||
)?.label;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -57,7 +57,7 @@ const EventsOrder = observer(
|
|||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default EventsOrder;
|
||||
|
|
|
|||
|
|
@ -33,14 +33,15 @@ interface Props {
|
|||
export const FilterList = observer((props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
observeChanges = () => {},
|
||||
observeChanges = () => {
|
||||
},
|
||||
filter,
|
||||
excludeFilterKeys = [],
|
||||
isConditional,
|
||||
onAddFilter,
|
||||
readonly,
|
||||
borderless,
|
||||
excludeCategory,
|
||||
excludeCategory
|
||||
} = props;
|
||||
|
||||
const { filters } = filter;
|
||||
|
|
@ -53,13 +54,13 @@ export const FilterList = observer((props: Props) => {
|
|||
<div
|
||||
className={cn(
|
||||
'bg-white',
|
||||
borderless ? '' : 'pb-2 px-4 rounded-xl border border-gray-lighter',
|
||||
borderless ? '' : 'pb-2 px-4 rounded-xl border border-gray-lighter'
|
||||
)}
|
||||
style={{
|
||||
borderBottomLeftRadius: props.mergeDown ? 0 : undefined,
|
||||
borderBottomRightRadius: props.mergeDown ? 0 : undefined,
|
||||
borderTopLeftRadius: props.mergeUp ? 0 : undefined,
|
||||
borderTopRightRadius: props.mergeUp ? 0 : undefined,
|
||||
borderTopRightRadius: props.mergeUp ? 0 : undefined
|
||||
}}
|
||||
>
|
||||
<div className={'flex items-center pt-2'} style={{ gap: '0.65rem' }}>
|
||||
|
|
@ -91,7 +92,7 @@ export const FilterList = observer((props: Props) => {
|
|||
className="hover:bg-active-blue px-5 "
|
||||
style={{
|
||||
marginLeft: '-1rem',
|
||||
width: 'calc(100% + 2rem)',
|
||||
width: 'calc(100% + 2rem)'
|
||||
}}
|
||||
>
|
||||
<FilterItem
|
||||
|
|
@ -106,7 +107,7 @@ export const FilterList = observer((props: Props) => {
|
|||
isConditional={isConditional}
|
||||
/>
|
||||
</div>
|
||||
) : null,
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -115,7 +116,8 @@ export const FilterList = observer((props: Props) => {
|
|||
export const EventsList = observer((props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
observeChanges = () => {},
|
||||
observeChanges = () => {
|
||||
},
|
||||
filter,
|
||||
hideEventsOrder = false,
|
||||
saveRequestPayloads,
|
||||
|
|
@ -126,7 +128,7 @@ export const EventsList = observer((props: Props) => {
|
|||
onAddFilter,
|
||||
cannotAdd,
|
||||
excludeCategory,
|
||||
borderless,
|
||||
borderless
|
||||
} = props;
|
||||
|
||||
const { filters } = filter;
|
||||
|
|
@ -143,7 +145,7 @@ export const EventsList = observer((props: Props) => {
|
|||
|
||||
const [hoveredItem, setHoveredItem] = React.useState<Record<string, any>>({
|
||||
i: null,
|
||||
position: null,
|
||||
position: null
|
||||
});
|
||||
const [draggedInd, setDraggedItem] = React.useState<number | null>(null);
|
||||
|
||||
|
|
@ -164,7 +166,7 @@ export const EventsList = observer((props: Props) => {
|
|||
}
|
||||
return draggedInd < hoveredIndex ? hoveredIndex - 1 : hoveredIndex;
|
||||
},
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDragStart = React.useCallback(
|
||||
|
|
@ -176,7 +178,7 @@ export const EventsList = observer((props: Props) => {
|
|||
ev.dataTransfer.setDragImage(el, 0, 0);
|
||||
}
|
||||
},
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDrop = React.useCallback(
|
||||
|
|
@ -187,7 +189,7 @@ export const EventsList = observer((props: Props) => {
|
|||
const newPosition = calculateNewPosition(
|
||||
draggedInd,
|
||||
hoveredItem.i,
|
||||
hoveredItem.position,
|
||||
hoveredItem.position
|
||||
);
|
||||
|
||||
const reorderedItem = newItems.splice(draggedInd, 1)[0];
|
||||
|
|
@ -205,8 +207,8 @@ export const EventsList = observer((props: Props) => {
|
|||
hoveredItem.position,
|
||||
props,
|
||||
setHoveredItem,
|
||||
setDraggedItem,
|
||||
],
|
||||
setDraggedItem
|
||||
]
|
||||
);
|
||||
|
||||
const eventsNum = filters.filter((i: any) => i.isEvent).length;
|
||||
|
|
@ -222,7 +224,7 @@ export const EventsList = observer((props: Props) => {
|
|||
borderBottomRightRadius: props.mergeDown ? 0 : undefined,
|
||||
borderTopLeftRadius: props.mergeUp ? 0 : undefined,
|
||||
borderTopRightRadius: props.mergeUp ? 0 : undefined,
|
||||
marginBottom: props.mergeDown ? '-1px' : undefined,
|
||||
marginBottom: props.mergeDown ? '-1px' : undefined
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center mb-2 gap-2">
|
||||
|
|
@ -232,8 +234,8 @@ export const EventsList = observer((props: Props) => {
|
|||
mode="events"
|
||||
filter={undefined}
|
||||
onFilterClick={onAddFilter}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
excludeCategory={excludeCategory}
|
||||
// excludeFilterKeys={excludeFilterKeys}
|
||||
// excludeCategory={excludeCategory}
|
||||
>
|
||||
<Button
|
||||
type="default"
|
||||
|
|
@ -263,8 +265,8 @@ export const EventsList = observer((props: Props) => {
|
|||
className={cn(
|
||||
'hover:bg-active-blue px-5 pe-3 gap-2 items-center flex',
|
||||
{
|
||||
'bg-[#f6f6f6]': hoveredItem.i === filterIndex,
|
||||
},
|
||||
'bg-[#f6f6f6]': hoveredItem.i === filterIndex
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
pointerEvents: 'unset',
|
||||
|
|
@ -290,7 +292,7 @@ export const EventsList = observer((props: Props) => {
|
|||
hoveredItem.i === filterIndex &&
|
||||
hoveredItem.position === 'bottom'
|
||||
? '1px dashed #888'
|
||||
: undefined,
|
||||
: undefined
|
||||
}}
|
||||
id={`${filter.key}-${filterIndex}`}
|
||||
onDragOver={(e) => handleDragOverEv(e, filterIndex)}
|
||||
|
|
@ -305,7 +307,7 @@ export const EventsList = observer((props: Props) => {
|
|||
handleDragStart(
|
||||
e,
|
||||
filterIndex,
|
||||
`${filter.key}-${filterIndex}`,
|
||||
`${filter.key}-${filterIndex}`
|
||||
)
|
||||
}
|
||||
onDragEnd={() => {
|
||||
|
|
@ -313,7 +315,7 @@ export const EventsList = observer((props: Props) => {
|
|||
setDraggedItem(null);
|
||||
}}
|
||||
style={{
|
||||
cursor: draggedInd !== null ? 'grabbing' : 'grab',
|
||||
cursor: draggedInd !== null ? 'grabbing' : 'grab'
|
||||
}}
|
||||
>
|
||||
<GripVertical size={16} />
|
||||
|
|
@ -326,13 +328,13 @@ export const EventsList = observer((props: Props) => {
|
|||
onRemoveFilter={() => onRemoveFilter(filterIndex)}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
disableDelete={cannotDeleteFilter}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
// excludeFilterKeys={excludeFilterKeys}
|
||||
readonly={props.readonly}
|
||||
isConditional={isConditional}
|
||||
excludeCategory={excludeCategory}
|
||||
// excludeCategory={excludeCategory}
|
||||
/>
|
||||
</div>
|
||||
) : null,
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { Space, Typography } from '.store/antd-virtual-9dbfadb7f6/package';
|
||||
import EventsOrder from 'Shared/Filters/FilterList/EventsOrder';
|
||||
|
||||
interface FilterListHeaderProps {
|
||||
title: string;
|
||||
filterSelection?: ReactNode;
|
||||
showEventsOrder?: boolean;
|
||||
orderProps?: any;
|
||||
onChangeOrder?: (e: any, data: any) => void;
|
||||
actions?: ReactNode[];
|
||||
}
|
||||
|
||||
const FilterListHeader = ({
|
||||
title,
|
||||
filterSelection,
|
||||
showEventsOrder = false,
|
||||
orderProps = {},
|
||||
onChangeOrder,
|
||||
actions = []
|
||||
}: FilterListHeaderProps) => {
|
||||
return (
|
||||
<div className="flex items-center mb-2 gap-2">
|
||||
<Space>
|
||||
<div className="font-medium">{title}</div>
|
||||
<Typography.Text>{filterSelection}</Typography.Text>
|
||||
</Space>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{showEventsOrder && onChangeOrder && (
|
||||
<EventsOrder
|
||||
orderProps={orderProps}
|
||||
onChange={onChangeOrder}
|
||||
/>
|
||||
)}
|
||||
{actions.map((action, index) => (
|
||||
<div key={index}>{action}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterListHeader;
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { GripVertical } from 'lucide-react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import cn from 'classnames';
|
||||
import FilterItem from '../FilterItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Filter } from '@/mstore/types/filterConstants';
|
||||
|
||||
interface UnifiedFilterListProps {
|
||||
title: string;
|
||||
filters: any[];
|
||||
header?: React.ReactNode;
|
||||
filterSelection?: React.ReactNode;
|
||||
handleRemove: (key: string) => void;
|
||||
handleUpdate: (key: string, updatedFilter: any) => void;
|
||||
handleAdd: (newFilter: Filter) => void;
|
||||
handleMove: (draggedIndex: number, newPosition: number) => void;
|
||||
isDraggable?: boolean;
|
||||
showIndices?: boolean;
|
||||
readonly?: boolean;
|
||||
isConditional?: boolean;
|
||||
showEventsOrder?: boolean;
|
||||
saveRequestPayloads?: boolean;
|
||||
supportsEmpty?: boolean;
|
||||
mergeDown?: boolean;
|
||||
mergeUp?: boolean;
|
||||
borderless?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
actions?: React.ReactNode[];
|
||||
orderProps?: any;
|
||||
}
|
||||
|
||||
const UnifiedFilterList = (props: UnifiedFilterListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
filters,
|
||||
handleRemove,
|
||||
handleUpdate,
|
||||
handleMove,
|
||||
isDraggable = false,
|
||||
showIndices = true,
|
||||
readonly = false,
|
||||
isConditional = false,
|
||||
showEventsOrder = false,
|
||||
saveRequestPayloads = false,
|
||||
supportsEmpty = true,
|
||||
mergeDown = false,
|
||||
mergeUp = false,
|
||||
style
|
||||
} = props;
|
||||
|
||||
const [hoveredItem, setHoveredItem] = useState<{ i: number | null; position: string | null }>({
|
||||
i: null,
|
||||
position: null
|
||||
});
|
||||
const [draggedInd, setDraggedItem] = useState<number | null>(null);
|
||||
|
||||
const cannotDelete = !supportsEmpty && filters.length <= 1;
|
||||
|
||||
const updateFilter = useCallback((key: string, updatedFilter: any) => {
|
||||
handleUpdate(key, updatedFilter);
|
||||
}, [handleUpdate]);
|
||||
|
||||
const removeFilter = useCallback((key: string) => {
|
||||
handleRemove(key);
|
||||
}, [handleRemove]);
|
||||
|
||||
const calculateNewPosition = useCallback(
|
||||
(dragInd: number, hoverIndex: number, hoverPosition: string) => {
|
||||
return hoverPosition === 'bottom' ? (dragInd < hoverIndex ? hoverIndex - 1 : hoverIndex) : hoverIndex;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(ev: React.DragEvent, index: number, elId: string) => {
|
||||
ev.dataTransfer.setData('text/plain', index.toString());
|
||||
setDraggedItem(index);
|
||||
const el = document.getElementById(elId);
|
||||
if (el) {
|
||||
ev.dataTransfer.setDragImage(el, 0, 0);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDragOver = useCallback((event: React.DragEvent, i: number) => {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget.getBoundingClientRect();
|
||||
const hoverMiddleY = (target.bottom - target.top) / 2;
|
||||
const hoverClientY = event.clientY - target.top;
|
||||
const position = hoverClientY < hoverMiddleY ? 'top' : 'bottom';
|
||||
setHoveredItem({ position, i });
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
if (draggedInd === null || hoveredItem.i === null) return;
|
||||
const newPosition = calculateNewPosition(
|
||||
draggedInd,
|
||||
hoveredItem.i,
|
||||
hoveredItem.position || 'bottom'
|
||||
);
|
||||
handleMove(draggedInd, newPosition);
|
||||
setHoveredItem({ i: null, position: null });
|
||||
setDraggedItem(null);
|
||||
},
|
||||
[draggedInd, calculateNewPosition, handleMove, hoveredItem.i, hoveredItem.position]
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
setHoveredItem({ i: null, position: null });
|
||||
setDraggedItem(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col" style={style}>
|
||||
{filters.map((filterItem: any, filterIndex: number) => (
|
||||
<div
|
||||
key={`filter-${filterIndex}`}
|
||||
className={cn('hover:bg-active-blue px-5 pe-3 gap-2 items-center flex', {
|
||||
'bg-[#f6f6f6]': hoveredItem.i === filterIndex
|
||||
})}
|
||||
style={{
|
||||
marginLeft: '-1rem',
|
||||
width: 'calc(100% + 2rem)',
|
||||
alignItems: 'start',
|
||||
borderTop: hoveredItem.i === filterIndex && hoveredItem.position === 'top' ? '1px dashed #888' : undefined,
|
||||
borderBottom: hoveredItem.i === filterIndex && hoveredItem.position === 'bottom' ? '1px dashed #888' : undefined
|
||||
}}
|
||||
id={`filter-${filterItem.key}`}
|
||||
onDragOver={isDraggable ? (e) => handleDragOver(e, filterIndex) : undefined}
|
||||
onDrop={isDraggable ? handleDrop : undefined}
|
||||
>
|
||||
{isDraggable && filters.length > 1 && (
|
||||
<div
|
||||
className="cursor-grab text-neutral-500/90 hover:bg-white px-1 mt-2.5 rounded-lg"
|
||||
draggable={true}
|
||||
onDragStart={(e) => handleDragStart(e, filterIndex, `filter-${filterIndex}`)}
|
||||
onDragEnd={handleDragEnd}
|
||||
style={{ cursor: draggedInd !== null ? 'grabbing' : 'grab' }}
|
||||
>
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FilterItem
|
||||
filterIndex={showIndices ? filterIndex : undefined}
|
||||
filter={filterItem}
|
||||
onUpdate={(updatedFilter) => updateFilter(filterItem.key, updatedFilter)}
|
||||
onRemoveFilter={() => removeFilter(filterItem.key)}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
disableDelete={cannotDelete}
|
||||
readonly={readonly}
|
||||
isConditional={isConditional}
|
||||
hideIndex={!showIndices}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedFilterList;
|
||||
|
|
@ -1,279 +1,200 @@
|
|||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import cn from 'classnames';
|
||||
import {
|
||||
AppWindow,
|
||||
ArrowUpDown,
|
||||
Chrome,
|
||||
CircleAlert,
|
||||
Clock2,
|
||||
Code,
|
||||
ContactRound,
|
||||
CornerDownRight,
|
||||
Cpu,
|
||||
Earth,
|
||||
FileStack,
|
||||
Layers,
|
||||
MapPin,
|
||||
Megaphone,
|
||||
MemoryStick,
|
||||
MonitorSmartphone,
|
||||
Navigation,
|
||||
Network,
|
||||
OctagonAlert,
|
||||
Pin,
|
||||
Pointer,
|
||||
RectangleEllipsis,
|
||||
SquareMousePointer,
|
||||
SquareUser,
|
||||
Timer,
|
||||
VenetianMask,
|
||||
Workflow,
|
||||
Flag,
|
||||
ChevronRight,
|
||||
Info,
|
||||
SquareArrowOutUpRight,
|
||||
} from 'lucide-react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Icon, Loader } from 'UI';
|
||||
import { Pointer, ChevronRight, MousePointerClick } from 'lucide-react';
|
||||
import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
|
||||
import { Loader } from 'UI';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { Input, Button } from 'antd';
|
||||
|
||||
import { FilterCategory, FilterKey, FilterType } from 'Types/filter/filterType';
|
||||
import { Input, Space, Typography } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import stl from './FilterModal.module.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Filter } from '@/mstore/types/filterConstants';
|
||||
|
||||
export const IconMap = {
|
||||
[FilterKey.CLICK]: <Pointer size={14} />,
|
||||
[FilterKey.LOCATION]: <Navigation size={14} />,
|
||||
[FilterKey.INPUT]: <RectangleEllipsis size={14} />,
|
||||
[FilterKey.CUSTOM]: <Code size={14} />,
|
||||
[FilterKey.FETCH]: <ArrowUpDown size={14} />,
|
||||
[FilterKey.GRAPHQL]: <Network size={14} />,
|
||||
[FilterKey.STATEACTION]: <RectangleEllipsis size={14} />,
|
||||
[FilterKey.ERROR]: <OctagonAlert size={14} />,
|
||||
[FilterKey.ISSUE]: <CircleAlert size={14} />,
|
||||
[FilterKey.FETCH_FAILED]: <Code size={14} />,
|
||||
[FilterKey.DOM_COMPLETE]: <ArrowUpDown size={14} />,
|
||||
[FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: <Network size={14} />,
|
||||
[FilterKey.TTFB]: <Timer size={14} />,
|
||||
[FilterKey.AVG_CPU_LOAD]: <Cpu size={14} />,
|
||||
[FilterKey.AVG_MEMORY_USAGE]: <MemoryStick size={14} />,
|
||||
[FilterKey.USERID]: <SquareUser size={14} />,
|
||||
[FilterKey.USERANONYMOUSID]: <VenetianMask size={14} />,
|
||||
[FilterKey.USER_CITY]: <Pin size={14} />,
|
||||
[FilterKey.USER_STATE]: <MapPin size={14} />,
|
||||
[FilterKey.USER_COUNTRY]: <Earth size={14} />,
|
||||
[FilterKey.USER_DEVICE]: <Code size={14} />,
|
||||
[FilterKey.USER_OS]: <AppWindow size={14} />,
|
||||
[FilterKey.USER_BROWSER]: <Chrome size={14} />,
|
||||
[FilterKey.PLATFORM]: <MonitorSmartphone size={14} />,
|
||||
[FilterKey.REVID]: <FileStack size={14} />,
|
||||
[FilterKey.REFERRER]: <Workflow size={14} />,
|
||||
[FilterKey.DURATION]: <Clock2 size={14} />,
|
||||
[FilterKey.TAGGED_ELEMENT]: <SquareMousePointer size={14} />,
|
||||
[FilterKey.METADATA]: <ContactRound size={14} />,
|
||||
[FilterKey.UTM_SOURCE]: <CornerDownRight size={14} />,
|
||||
[FilterKey.UTM_MEDIUM]: <Layers size={14} />,
|
||||
[FilterKey.UTM_CAMPAIGN]: <Megaphone size={14} />,
|
||||
[FilterKey.FEATURE_FLAG]: <Flag size={14} />,
|
||||
export const getIconForFilter = (filter: Filter) => <MousePointerClick size={14} />;
|
||||
|
||||
// Helper function for grouping filters
|
||||
const groupFiltersByCategory = (filters: Filter[]) => {
|
||||
if (!filters?.length) return {};
|
||||
|
||||
return filters.reduce((acc, filter) => {
|
||||
const category = filter.category
|
||||
? filter.category.charAt(0).toUpperCase() + filter.category.slice(1)
|
||||
: 'Unknown';
|
||||
|
||||
if (!acc[category]) acc[category] = [];
|
||||
acc[category].push(filter);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
function filterJson(
|
||||
jsonObj: Record<string, any>,
|
||||
excludeKeys: string[] = [],
|
||||
excludeCategory: string[] = [],
|
||||
allowedFilterKeys: string[] = [],
|
||||
mode: 'filters' | 'events',
|
||||
): Record<string, any> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(jsonObj)
|
||||
.map(([key, value]) => {
|
||||
const arr = value.filter(
|
||||
(i: { key: string; isEvent: boolean; category: string }) => {
|
||||
if (excludeCategory.includes(i.category)) return false;
|
||||
if (excludeKeys.includes(i.key)) return false;
|
||||
if (mode === 'events' && !i.isEvent) return false;
|
||||
if (mode === 'filters' && i.isEvent) return false;
|
||||
return !(
|
||||
allowedFilterKeys.length > 0 && !allowedFilterKeys.includes(i.key)
|
||||
);
|
||||
},
|
||||
);
|
||||
return [key, arr];
|
||||
})
|
||||
.filter(([_, arr]) => arr.length > 0),
|
||||
);
|
||||
}
|
||||
// Optimized filtering function with early returns
|
||||
const getFilteredEntries = (query: string, filters: Filter[]) => {
|
||||
const trimmedQuery = query.trim().toLowerCase();
|
||||
|
||||
export const getMatchingEntries = (
|
||||
searchQuery: string,
|
||||
filters: Record<string, any>,
|
||||
) => {
|
||||
const matchingCategories: string[] = [];
|
||||
const matchingFilters: Record<string, any> = {};
|
||||
const lowerCaseQuery = searchQuery.toLowerCase();
|
||||
if (!filters || Object.keys(filters).length === 0) {
|
||||
return { matchingCategories: ['All'], matchingFilters: {} };
|
||||
}
|
||||
|
||||
if (lowerCaseQuery.length === 0) {
|
||||
if (!trimmedQuery) {
|
||||
return {
|
||||
matchingCategories: ['All', ...Object.keys(filters)],
|
||||
matchingFilters: filters,
|
||||
matchingFilters: filters
|
||||
};
|
||||
}
|
||||
|
||||
Object.keys(filters).forEach((name) => {
|
||||
if (name.toLocaleLowerCase().includes(lowerCaseQuery)) {
|
||||
const matchingCategories = ['All'];
|
||||
const matchingFilters = {};
|
||||
|
||||
// Single pass through the data with optimized conditionals
|
||||
Object.entries(filters).forEach(([name, categoryFilters]) => {
|
||||
const categoryMatch = name.toLowerCase().includes(trimmedQuery);
|
||||
|
||||
if (categoryMatch) {
|
||||
matchingCategories.push(name);
|
||||
matchingFilters[name] = filters[name];
|
||||
} else {
|
||||
const filtersQuery = filters[name].filter((filterOption: any) =>
|
||||
filterOption.label.toLocaleLowerCase().includes(lowerCaseQuery),
|
||||
matchingFilters[name] = categoryFilters;
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = categoryFilters.filter(
|
||||
(filter: Filter) =>
|
||||
filter.displayName?.toLowerCase().includes(trimmedQuery) ||
|
||||
filter.name?.toLowerCase().includes(trimmedQuery)
|
||||
);
|
||||
|
||||
if (filtersQuery.length > 0) matchingFilters[name] = filtersQuery;
|
||||
filtersQuery.length > 0 && matchingCategories.push(name);
|
||||
if (filtered.length) {
|
||||
matchingCategories.push(name);
|
||||
matchingFilters[name] = filtered;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
matchingCategories: ['All', ...matchingCategories],
|
||||
matchingFilters,
|
||||
};
|
||||
return { matchingCategories, matchingFilters };
|
||||
};
|
||||
|
||||
interface Props {
|
||||
isLive?: boolean;
|
||||
conditionalFilters: any;
|
||||
mobileConditionalFilters: any;
|
||||
onFilterClick?: (filter: any) => void;
|
||||
isMainSearch?: boolean;
|
||||
searchQuery?: string;
|
||||
excludeFilterKeys?: Array<string>;
|
||||
excludeCategory?: Array<string>;
|
||||
allowedFilterKeys?: Array<string>;
|
||||
isConditional?: boolean;
|
||||
isMobile?: boolean;
|
||||
mode: 'filters' | 'events';
|
||||
}
|
||||
// Custom debounce hook to optimize search
|
||||
const useDebounce = (value: any, delay = 300) => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
export const getNewIcon = (filter: Record<string, any>) => {
|
||||
if (filter.icon?.includes('metadata')) {
|
||||
return IconMap[FilterKey.METADATA];
|
||||
}
|
||||
// @ts-ignore
|
||||
if (IconMap[filter.key]) {
|
||||
// @ts-ignore
|
||||
return IconMap[filter.key];
|
||||
}
|
||||
return <Icon name={filter.icon} size={16} />;
|
||||
};
|
||||
|
||||
function FilterModal(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
isLive,
|
||||
onFilterClick = () => null,
|
||||
isMainSearch = false,
|
||||
excludeFilterKeys = [],
|
||||
excludeCategory = [],
|
||||
allowedFilterKeys = [],
|
||||
isConditional,
|
||||
mode,
|
||||
} = props;
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [category, setCategory] = React.useState('All');
|
||||
const { searchStore, searchStoreLive, projectsStore } = useStore();
|
||||
const isMobile = projectsStore.active?.platform === 'ios'; // TODO - should be using mobile once the app is changed
|
||||
const filters = isLive
|
||||
? searchStoreLive.filterListLive
|
||||
: isMobile
|
||||
? searchStore.filterListMobile
|
||||
: searchStoreLive.filterList;
|
||||
const conditionalFilters = searchStore.filterListConditional;
|
||||
const mobileConditionalFilters = searchStore.filterListMobileConditional;
|
||||
const showSearchList = isMainSearch && searchQuery.length > 0;
|
||||
const filterSearchList = isLive
|
||||
? searchStoreLive.filterSearchList
|
||||
: searchStore.filterSearchList;
|
||||
const fetchingFilterSearchList = isLive
|
||||
? searchStoreLive.loadingFilterSearch
|
||||
: searchStore.loadingFilterSearch;
|
||||
|
||||
const parseAndAdd = (filter) => {
|
||||
if (
|
||||
filter.category === FilterCategory.EVENTS &&
|
||||
filter.key.startsWith('_')
|
||||
) {
|
||||
filter.value = [filter.key.substring(1)];
|
||||
filter.key = FilterKey.CUSTOM;
|
||||
filter.label = 'Custom Events';
|
||||
}
|
||||
if (
|
||||
filter.type === FilterType.ISSUE &&
|
||||
filter.key.startsWith(`${FilterKey.ISSUE}_`)
|
||||
) {
|
||||
filter.key = FilterKey.ISSUE;
|
||||
}
|
||||
onFilterClick(filter);
|
||||
};
|
||||
const onFilterSearchClick = (filter: any) => {
|
||||
const _filter = { ...filtersMap[filter.type] };
|
||||
_filter.value = [filter.value];
|
||||
parseAndAdd(_filter);
|
||||
};
|
||||
|
||||
const filterJsonObj = isConditional
|
||||
? isMobile
|
||||
? mobileConditionalFilters
|
||||
: conditionalFilters
|
||||
: filters;
|
||||
const filterObj = filterJson(
|
||||
filterJsonObj,
|
||||
excludeFilterKeys,
|
||||
excludeCategory,
|
||||
allowedFilterKeys,
|
||||
mode,
|
||||
);
|
||||
const showMetaCTA =
|
||||
mode === 'filters' &&
|
||||
!filterObj.Metadata &&
|
||||
(allowedFilterKeys?.length
|
||||
? allowedFilterKeys.includes(FilterKey.METADATA)
|
||||
: true) &&
|
||||
(excludeCategory?.length
|
||||
? !excludeCategory.includes(FilterCategory.METADATA)
|
||||
: true) &&
|
||||
(excludeFilterKeys?.length
|
||||
? !excludeFilterKeys.includes(FilterKey.METADATA)
|
||||
: true);
|
||||
|
||||
const { matchingCategories, matchingFilters } = getMatchingEntries(
|
||||
searchQuery,
|
||||
filterObj,
|
||||
);
|
||||
|
||||
const isResultEmpty =
|
||||
(!filterSearchList || Object.keys(filterSearchList).length === 0) &&
|
||||
matchingCategories.length === 0 &&
|
||||
Object.keys(matchingFilters).length === 0;
|
||||
|
||||
const inputRef = useRef<any>(null);
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => clearTimeout(handler);
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
};
|
||||
|
||||
// Memoized filter item component
|
||||
const FilterItem = React.memo(({ filter, onClick, showCategory }: {
|
||||
filter: Filter;
|
||||
onClick: (filter: Filter) => void;
|
||||
showCategory?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
className="flex items-center flex-shrink-0 p-2 cursor-pointer gap-1 rounded-lg hover:bg-active-blue"
|
||||
onClick={() => onClick(filter)}
|
||||
>
|
||||
{showCategory && filter.category && (
|
||||
<div style={{ width: 100 }} className="text-neutral-500/90 flex justify-between items-center">
|
||||
<span className="capitalize">{filter.subCategory || filter.category}</span>
|
||||
<ChevronRight size={14} />
|
||||
</div>
|
||||
)}
|
||||
<Space className="flex-1 min-w-0">
|
||||
<span className="text-neutral-500/90 text-xs">{getIconForFilter(filter)}</span>
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: true }}
|
||||
className="max-w-full"
|
||||
style={{ display: 'block' }}
|
||||
>
|
||||
{filter.displayName || filter.name}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
));
|
||||
|
||||
// Memoized category list component
|
||||
const CategoryList = React.memo(({ categories, activeCategory, onSelect }: {
|
||||
categories: string[];
|
||||
activeCategory: string;
|
||||
onSelect: (category: string) => void;
|
||||
}) => (
|
||||
<>
|
||||
{categories.map((key) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => onSelect(key)}
|
||||
className={cn(
|
||||
'rounded-xl px-4 py-2 hover:bg-active-blue capitalize cursor-pointer font-medium',
|
||||
key === activeCategory && 'bg-active-blue text-teal'
|
||||
)}
|
||||
>
|
||||
{key}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
));
|
||||
|
||||
function FilterModal({ onFilterClick = () => null, filters = [], isMainSearch = false }) {
|
||||
const { t } = useTranslation();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const debouncedQuery = useDebounce(searchQuery);
|
||||
const [category, setCategory] = useState('All');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
// Memoize expensive computations
|
||||
const groupedFilters = useMemo(() =>
|
||||
groupFiltersByCategory(filters),
|
||||
[filters]
|
||||
);
|
||||
|
||||
const { matchingCategories, matchingFilters } = useMemo(
|
||||
() => getFilteredEntries(debouncedQuery, groupedFilters),
|
||||
[debouncedQuery, groupedFilters]
|
||||
);
|
||||
|
||||
const displayedFilters = useMemo(() => {
|
||||
if (category === 'All') {
|
||||
return Object.entries(matchingFilters).flatMap(([cat, filters]) =>
|
||||
filters.map((filter) => ({ ...filter, category: cat }))
|
||||
);
|
||||
}
|
||||
return matchingFilters[category] || [];
|
||||
}, [category, matchingFilters]);
|
||||
|
||||
const isResultEmpty = useMemo(
|
||||
() => matchingCategories.length <= 1 && Object.keys(matchingFilters).length === 0,
|
||||
[matchingCategories.length, matchingFilters]
|
||||
);
|
||||
|
||||
// Memoize handlers
|
||||
const handleFilterClick = useCallback(
|
||||
(filter: Filter) => onFilterClick({ ...filter, operator: 'is' }),
|
||||
[onFilterClick]
|
||||
);
|
||||
|
||||
const handleCategoryClick = useCallback(
|
||||
(cat: string) => setCategory(cat),
|
||||
[]
|
||||
);
|
||||
|
||||
// Focus input only when necessary
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, [category]);
|
||||
|
||||
const displayedFilters =
|
||||
category === 'All'
|
||||
? Object.entries(matchingFilters).flatMap(([category, filters]) =>
|
||||
filters.map((f: any) => ({ ...f, category })),
|
||||
)
|
||||
: matchingFilters[category];
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{ width: '490px', maxHeight: '380px' }}>
|
||||
<div className="flex items-center justify-center h-60">
|
||||
<Loader loading />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={stl.wrapper} style={{ width: '460px', maxHeight: '380px' }}>
|
||||
<div style={{ width: '490px', maxHeight: '380px' }}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className="mb-4 rounded-xl text-lg font-medium placeholder:text-lg placeholder:font-medium placeholder:text-neutral-300"
|
||||
|
|
@ -282,149 +203,41 @@ function FilterModal(props: Props) {
|
|||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex gap-2 items-start">
|
||||
<div className="flex flex-col gap-1">
|
||||
{matchingCategories.map((key) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => setCategory(key)}
|
||||
className={cn(
|
||||
'rounded-xl px-4 py-2 hover:bg-active-blue capitalize cursor-pointer font-medium',
|
||||
key === category ? 'bg-active-blue text-teal' : '',
|
||||
)}
|
||||
>
|
||||
{key}
|
||||
</div>
|
||||
))}
|
||||
{showMetaCTA ? (
|
||||
<div
|
||||
key="META_CTA"
|
||||
onClick={() => setCategory('META_CTA')}
|
||||
className={cn(
|
||||
'rounded-xl px-4 py-2 hover:bg-active-blue capitalize cursor-pointer font-medium',
|
||||
category === 'META_CTA' ? 'bg-active-blue text-teal' : '',
|
||||
)}
|
||||
>
|
||||
{t('Metadata')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-col gap-1 overflow-y-auto w-full h-full"
|
||||
style={{ maxHeight: 300, flex: 2 }}
|
||||
>
|
||||
{displayedFilters && displayedFilters.length
|
||||
? displayedFilters.map((filter: Record<string, any>) => (
|
||||
<div
|
||||
key={filter.label}
|
||||
className={cn(
|
||||
'flex items-center p-2 cursor-pointer gap-1 rounded-lg hover:bg-active-blue',
|
||||
)}
|
||||
onClick={() => parseAndAdd({ ...filter })}
|
||||
>
|
||||
{filter.category ? (
|
||||
<div
|
||||
style={{ width: 100 }}
|
||||
className="text-neutral-500/90 w-full flex justify-between items-center"
|
||||
>
|
||||
<span>
|
||||
{filter.subCategory
|
||||
? filter.subCategory
|
||||
: filter.category}
|
||||
</span>
|
||||
<ChevronRight size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-neutral-500/90 text-xs">
|
||||
{getNewIcon(filter)}
|
||||
</span>
|
||||
<span>{filter.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
{category === 'META_CTA' && showMetaCTA ? (
|
||||
<div
|
||||
style={{
|
||||
height: 300,
|
||||
}}
|
||||
className="mx-auto flex flex-col items-center justify-center gap-3 w-2/3 text-center"
|
||||
>
|
||||
<div className="font-semibold flex gap-2 items-center">
|
||||
<Info size={16} />
|
||||
<span>{t('No Metadata Available')}</span>
|
||||
</div>
|
||||
<div className="text-secondary">
|
||||
{t('Identify sessions & data easily by linking user-specific metadata.')}
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
className="text-teal"
|
||||
onClick={() => {
|
||||
const docs = 'https://docs.openreplay.com/en/en/session-replay/metadata/';
|
||||
window.open(docs, '_blank');
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="">{t('Learn how')}</span>
|
||||
<SquareArrowOutUpRight size={14} />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{showSearchList && (
|
||||
<Loader loading={fetchingFilterSearchList}>
|
||||
<div className="-mx-6 px-6">
|
||||
{isResultEmpty && !fetchingFilterSearchList ? (
|
||||
<div className="flex items-center flex-col">
|
||||
|
||||
{isResultEmpty ? (
|
||||
<div className="flex items-center flex-col justify-center h-60">
|
||||
<AnimatedSVG name={ICONS.NO_SEARCH_RESULTS} size={30} />
|
||||
<div className="font-medium px-3 mt-4">
|
||||
{' '}
|
||||
{t('No matching filters.')}
|
||||
</div>
|
||||
<div className="font-medium px-3 mt-4">{t('No matching filters.')}</div>
|
||||
</div>
|
||||
) : (
|
||||
Object.keys(filterSearchList).map((key, index) => {
|
||||
const filter = filterSearchList[key];
|
||||
const option = filtersMap[key];
|
||||
return option ? (
|
||||
<div key={index} className={cn('mb-3')}>
|
||||
<div className="font-medium uppercase color-gray-medium mb-2">
|
||||
{option.label}
|
||||
</div>
|
||||
<div>
|
||||
{filter.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
stl.filterSearchItem,
|
||||
'cursor-pointer px-3 py-1 flex items-center gap-2',
|
||||
)}
|
||||
onClick={() =>
|
||||
onFilterSearchClick({ type: key, value: f.value })
|
||||
}
|
||||
>
|
||||
{getNewIcon(option)}
|
||||
<div className="whitespace-nowrap text-ellipsis overflow-hidden">
|
||||
{f.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2 items-start">
|
||||
<div className="flex flex-col gap-1 min-w-40">
|
||||
<CategoryList
|
||||
categories={matchingCategories}
|
||||
activeCategory={category}
|
||||
onSelect={handleCategoryClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 overflow-y-auto w-full" style={{ maxHeight: 300, flex: 2 }}>
|
||||
{displayedFilters.length > 0 ? (
|
||||
displayedFilters.map((filter: Filter, index: number) => (
|
||||
<FilterItem
|
||||
key={`${filter.name}-${index}`}
|
||||
filter={filter}
|
||||
onClick={handleFilterClick}
|
||||
showCategory={category === 'All'}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
})
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<div className="text-neutral-500">{t('No filters in this category')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Loader>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(FilterModal);
|
||||
export default React.memo(observer(FilterModal));
|
||||
|
|
|
|||
|
|
@ -1,121 +1,64 @@
|
|||
import React, { useState } from 'react';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import { assist as assistRoute, isRoute } from 'App/routes';
|
||||
import cn from 'classnames';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import FilterModal from '../FilterModal';
|
||||
import { getNewIcon } from '../FilterModal/FilterModal';
|
||||
import FilterModal from '../FilterModal/FilterModal';
|
||||
import { Filter } from '@/mstore/types/filterConstants';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
|
||||
interface Props {
|
||||
filter?: any;
|
||||
onFilterClick: (filter: any) => void;
|
||||
children?: any;
|
||||
excludeFilterKeys?: Array<string>;
|
||||
excludeCategory?: Array<string>;
|
||||
allowedFilterKeys?: Array<string>;
|
||||
interface FilterSelectionProps {
|
||||
filters: Filter[];
|
||||
onFilterClick: (filter: Filter) => void;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
isConditional?: boolean;
|
||||
isMobile?: boolean;
|
||||
mode: 'filters' | 'events';
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
function FilterSelection(props: Props) {
|
||||
const {
|
||||
filter,
|
||||
const FilterSelection: React.FC<FilterSelectionProps> = observer(({
|
||||
filters,
|
||||
onFilterClick,
|
||||
children,
|
||||
excludeFilterKeys = [],
|
||||
excludeCategory = [],
|
||||
allowedFilterKeys = [],
|
||||
disabled = false,
|
||||
isConditional,
|
||||
isMobile,
|
||||
mode,
|
||||
isLive,
|
||||
} = props;
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const modalRef = React.useRef<HTMLDivElement>(null);
|
||||
isLive
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
onFilterClick(filter);
|
||||
setShowModal(false);
|
||||
};
|
||||
const handleFilterClick = useCallback((selectedFilter: Filter) => {
|
||||
onFilterClick(selectedFilter);
|
||||
setOpen(false);
|
||||
}, [onFilterClick]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showModal && modalRef.current) {
|
||||
const modalRect = modalRef.current.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
if (modalRect.right > viewportWidth) {
|
||||
modalRef.current.style.left = 'unset';
|
||||
modalRef.current.style.right = '-280px';
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
if (!disabled) {
|
||||
setOpen(newOpen);
|
||||
}
|
||||
}
|
||||
}, [showModal]);
|
||||
}, [disabled]);
|
||||
|
||||
const label = filter?.category === 'Issue' ? 'Issue' : filter?.label;
|
||||
return (
|
||||
<div className="relative flex-shrink-0 my-1.5">
|
||||
<OutsideClickDetectingDiv
|
||||
className="relative"
|
||||
onClickOutside={() => {
|
||||
setTimeout(() => {
|
||||
setShowModal(false);
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
{children ? (
|
||||
React.cloneElement(children, {
|
||||
onClick: (e) => {
|
||||
setShowModal(true);
|
||||
},
|
||||
disabled,
|
||||
})
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg py-1 px-2 flex items-center gap-1 cursor-pointer bg-white border border-gray-light text-ellipsis hover:border-neutral-400 btn-select-event',
|
||||
{ 'opacity-50 pointer-events-none': disabled },
|
||||
)}
|
||||
style={{
|
||||
height: '26px',
|
||||
}}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<div className="text-xs text-neutral-500/90 hover:border-neutral-400">
|
||||
{getNewIcon(filter)}
|
||||
</div>
|
||||
<div className="text-neutral-500/90 flex gap-2 hover:border-neutral-400 ">{`${filter.subCategory ? filter.subCategory : filter.category} •`}</div>
|
||||
<div
|
||||
className="rounded-lg overflow-hidden whitespace-nowrap text-ellipsis mr-auto truncate "
|
||||
style={{ textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showModal && (
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="absolute mt-2 left-0 rounded-2xl shadow-lg bg-white z-50"
|
||||
>
|
||||
const content = (
|
||||
<FilterModal
|
||||
isLive={isRoute(ASSIST_ROUTE, window.location.pathname) || isLive}
|
||||
onFilterClick={onAddFilter}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
allowedFilterKeys={allowedFilterKeys}
|
||||
excludeCategory={excludeCategory}
|
||||
isConditional={isConditional}
|
||||
isMobile={isMobile}
|
||||
mode={mode}
|
||||
onFilterClick={handleFilterClick}
|
||||
filters={filters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</OutsideClickDetectingDiv>
|
||||
);
|
||||
|
||||
const triggerElement = React.isValidElement(children)
|
||||
? React.cloneElement(children, { disabled })
|
||||
: children;
|
||||
|
||||
return (
|
||||
<div className="relative flex-shrink-0">
|
||||
<Popover
|
||||
content={content}
|
||||
trigger="click"
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
placement="bottomLeft"
|
||||
overlayClassName="filter-selection-popover rounded-lg border border-gray-200 shadow-sm shadow-gray-200"
|
||||
destroyTooltipOnHide
|
||||
arrow={false}
|
||||
>
|
||||
{triggerElement}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default observer(FilterSelection);
|
||||
export default FilterSelection;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ interface Props {
|
|||
onUpdate: (filter: any) => void;
|
||||
isConditional?: boolean;
|
||||
}
|
||||
|
||||
function FilterValue(props: Props) {
|
||||
const { filter } = props;
|
||||
const isAutoOpen = filter.autoOpen;
|
||||
|
|
@ -29,7 +30,7 @@ function FilterValue(props: Props) {
|
|||
}, [isAutoOpen]);
|
||||
const [durationValues, setDurationValues] = useState({
|
||||
minDuration: filter.value?.[0],
|
||||
maxDuration: filter.value.length > 1 ? filter.value[1] : filter.value[0],
|
||||
maxDuration: filter.value.length > 1 ? filter.value[1] : filter.value[0]
|
||||
});
|
||||
const showCloseButton = filter.value.length > 1;
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ function FilterValue(props: Props) {
|
|||
|
||||
const onRemoveValue = (valueIndex: any) => {
|
||||
const newValue = filter.value.filter(
|
||||
(_: any, index: any) => index !== valueIndex,
|
||||
(_: any, index: any) => index !== valueIndex
|
||||
);
|
||||
props.onUpdate({ ...filter, value: newValue });
|
||||
};
|
||||
|
|
@ -60,7 +61,7 @@ function FilterValue(props: Props) {
|
|||
};
|
||||
|
||||
const debounceOnSelect = React.useCallback(debounce(onChange, 500), [
|
||||
onChange,
|
||||
onChange
|
||||
]);
|
||||
|
||||
const onDurationChange = (newValues: any) => {
|
||||
|
|
@ -77,14 +78,19 @@ function FilterValue(props: Props) {
|
|||
) {
|
||||
props.onUpdate({
|
||||
...filter,
|
||||
value: [durationValues.minDuration, durationValues.maxDuration],
|
||||
value: [durationValues.minDuration, durationValues.maxDuration]
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getParms = (key: any) => {
|
||||
let params: any = { type: filter.key };
|
||||
const getParams = (key: any) => {
|
||||
let params: any = {
|
||||
type: filter.key,
|
||||
name: filter.name,
|
||||
isEvent: filter.isEvent,
|
||||
id: filter.id
|
||||
};
|
||||
switch (filter.category) {
|
||||
case FilterCategory.METADATA:
|
||||
params = { type: FilterKey.METADATA, key };
|
||||
|
|
@ -99,6 +105,7 @@ function FilterValue(props: Props) {
|
|||
|
||||
const renderValueFiled = (value: any[]) => {
|
||||
const showOrButton = filter.value.length > 1;
|
||||
|
||||
function BaseFilterLocalAutoComplete(props) {
|
||||
return (
|
||||
<FilterAutoCompleteLocal
|
||||
|
|
@ -115,6 +122,7 @@ function FilterValue(props: Props) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BaseDropDown(props) {
|
||||
return (
|
||||
<FilterValueDropdown
|
||||
|
|
@ -127,6 +135,7 @@ function FilterValue(props: Props) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
switch (filter.type) {
|
||||
case FilterType.NUMBER_MULTIPLE:
|
||||
return (
|
||||
|
|
@ -145,7 +154,23 @@ function FilterValue(props: Props) {
|
|||
/>
|
||||
);
|
||||
case FilterType.STRING:
|
||||
return <BaseFilterLocalAutoComplete placeholder={filter.placeholder} />;
|
||||
// return <BaseFilterLocalAutoComplete placeholder={filter.placeholder} />;
|
||||
return <FilterAutoComplete
|
||||
value={value}
|
||||
isAutoOpen={isAutoOpen}
|
||||
showCloseButton={showCloseButton}
|
||||
showOrButton={showOrButton}
|
||||
onApplyValues={onApplyValues}
|
||||
onRemoveValue={(index) => onRemoveValue(index)}
|
||||
method="GET"
|
||||
endpoint="/PROJECT_ID/events/search"
|
||||
params={getParams(filter.key)}
|
||||
headerText=""
|
||||
placeholder={filter.placeholder}
|
||||
onSelect={(e, item, index) => onChange(e, item, index)}
|
||||
icon={filter.icon}
|
||||
modalProps={{ placeholder: 'Search' }}
|
||||
/>;
|
||||
case FilterType.DROPDOWN:
|
||||
return <BaseDropDown />;
|
||||
case FilterType.ISSUE:
|
||||
|
|
@ -181,7 +206,7 @@ function FilterValue(props: Props) {
|
|||
onRemoveValue={(index) => onRemoveValue(index)}
|
||||
method="GET"
|
||||
endpoint="/PROJECT_ID/events/search"
|
||||
params={getParms(filter.key)}
|
||||
params={getParams(filter.key)}
|
||||
headerText=""
|
||||
placeholder={filter.placeholder}
|
||||
onSelect={(e, item, index) => onChange(e, item, index)}
|
||||
|
|
@ -196,7 +221,7 @@ function FilterValue(props: Props) {
|
|||
<div
|
||||
id="ignore-outside"
|
||||
className={cn('grid gap-3 w-fit flex-wrap my-1.5', {
|
||||
'grid-cols-2': filter.hasSource,
|
||||
'grid-cols-2': filter.hasSource
|
||||
})}
|
||||
>
|
||||
{renderValueFiled(filter.value)}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ interface Props {
|
|||
onRemoveFilter: () => void;
|
||||
isFilter?: boolean;
|
||||
}
|
||||
|
||||
export default function SubFilterItem(props: Props) {
|
||||
const { isFilter = false, filterIndex, filter } = props;
|
||||
const canShowValues = !(
|
||||
|
|
|
|||
|
|
@ -1,110 +1,128 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { debounce } from 'App/utils';
|
||||
import { FilterList, EventsList } 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';
|
||||
import UnifiedFilterList from 'Shared/Filters/FilterList/UnifiedFilterList';
|
||||
import FilterSelection from 'Shared/Filters/FilterSelection';
|
||||
import { Button, Divider } from 'antd';
|
||||
import { Plus } from 'lucide-react';
|
||||
import cn from 'classnames';
|
||||
import { Filter } from '@/mstore/types/filterConstants';
|
||||
import FilterListHeader from 'Shared/Filters/FilterList/FilterListHeader';
|
||||
|
||||
let debounceFetch: any = () => {
|
||||
};
|
||||
|
||||
let debounceFetch: any = () => {};
|
||||
function SessionFilters() {
|
||||
const { searchStore, projectsStore, customFieldStore, tagWatchStore } =
|
||||
const { searchStore, projectsStore, filterStore } =
|
||||
useStore();
|
||||
|
||||
const appliedFilter = searchStore.instance;
|
||||
const metaLoading = customFieldStore.isLoading;
|
||||
const searchInstance = searchStore.instance;
|
||||
const saveRequestPayloads =
|
||||
projectsStore.instance?.saveRequestPayloads ?? false;
|
||||
const activeProject = projectsStore.active;
|
||||
|
||||
const reloadTags = 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(() => {
|
||||
// Add default location/screen filter if no filters are present
|
||||
if (searchStore.instance.filters.length === 0) {
|
||||
searchStore.addFilterByKeyAndValue(
|
||||
activeProject?.platform === 'web'
|
||||
? FilterKey.LOCATION
|
||||
: FilterKey.VIEW_MOBILE,
|
||||
'',
|
||||
'isAny',
|
||||
);
|
||||
}
|
||||
void reloadTags();
|
||||
}, [projectsStore.activeSiteId, activeProject]);
|
||||
|
||||
useSessionSearchQueryHandler({
|
||||
appliedFilter,
|
||||
loading: metaLoading,
|
||||
onBeforeLoad: async () => {
|
||||
await reloadTags();
|
||||
},
|
||||
});
|
||||
const allFilterOptions: Filter[] = filterStore.getCurrentProjectFilters();
|
||||
const eventOptions = allFilterOptions.filter(i => i.isEvent);
|
||||
const propertyOptions = allFilterOptions.filter(i => !i.isEvent);
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
filter.autoOpen = true;
|
||||
searchStore.addFilter(filter);
|
||||
};
|
||||
|
||||
const onUpdateFilter = (filterIndex: any, filter: any) => {
|
||||
searchStore.updateFilter(filterIndex, filter);
|
||||
};
|
||||
|
||||
const onFilterMove = (newFilters: any) => {
|
||||
searchStore.updateSearch({ ...appliedFilter, filters: newFilters});
|
||||
// debounceFetch();
|
||||
};
|
||||
|
||||
const onRemoveFilter = (filterIndex: any) => {
|
||||
searchStore.removeFilter(filterIndex);
|
||||
|
||||
// debounceFetch();
|
||||
};
|
||||
|
||||
const onChangeEventsOrder = (e: any, { value }: any) => {
|
||||
searchStore.edit({
|
||||
eventsOrder: value,
|
||||
eventsOrder: value
|
||||
});
|
||||
|
||||
// debounceFetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<EventsList
|
||||
filter={appliedFilter}
|
||||
onAddFilter={onAddFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
onFilterMove={onFilterMove}
|
||||
mergeDown
|
||||
<div
|
||||
className={cn(
|
||||
'bg-white',
|
||||
'py-2 px-4 rounded-xl border border-gray-lighter'
|
||||
)}
|
||||
>
|
||||
<FilterListHeader
|
||||
title={'Events'}
|
||||
showEventsOrder={true}
|
||||
orderProps={searchInstance}
|
||||
onChangeOrder={onChangeEventsOrder}
|
||||
filterSelection={
|
||||
<FilterSelection
|
||||
filters={eventOptions}
|
||||
onFilterClick={(newFilter) => {
|
||||
console.log('newFilter', newFilter);
|
||||
onAddFilter(newFilter);
|
||||
}}>
|
||||
<Button type="default" size="small">
|
||||
<div className="flex items-center gap-1">
|
||||
<Plus size={16} strokeWidth={1} />
|
||||
<span>Add Event</span>
|
||||
</div>
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
}
|
||||
/>
|
||||
<FilterList
|
||||
mergeUp
|
||||
filter={appliedFilter}
|
||||
onAddFilter={onAddFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
onFilterMove={onFilterMove}
|
||||
|
||||
<UnifiedFilterList
|
||||
title="Events"
|
||||
filters={searchInstance.filters.filter(i => i.isEvent)}
|
||||
isDraggable={true}
|
||||
showIndices={true}
|
||||
handleRemove={function(key: string): void {
|
||||
searchStore.removeFilter(key);
|
||||
}}
|
||||
handleUpdate={function(key: string, updatedFilter: any): void {
|
||||
searchStore.updateFilter(key, updatedFilter);
|
||||
}}
|
||||
handleAdd={function(newFilter: Filter): void {
|
||||
searchStore.addFilter(newFilter);
|
||||
}}
|
||||
handleMove={function(draggedIndex: number, newPosition: number): void {
|
||||
searchStore.moveFilter(draggedIndex, newPosition);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Divider className="my-3" />
|
||||
|
||||
<FilterListHeader
|
||||
title={'Filters'}
|
||||
filterSelection={
|
||||
<FilterSelection
|
||||
filters={propertyOptions}
|
||||
onFilterClick={(newFilter) => {
|
||||
onAddFilter(newFilter);
|
||||
}}
|
||||
>
|
||||
<Button type="default" size="small">
|
||||
<div className="flex items-center gap-1">
|
||||
<Plus size={16} strokeWidth={1} />
|
||||
<span>Filter</span>
|
||||
</div>
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
} />
|
||||
|
||||
<UnifiedFilterList
|
||||
title="Filters"
|
||||
filters={searchInstance.filters.filter(i => !i.isEvent)}
|
||||
isDraggable={false}
|
||||
showIndices={false}
|
||||
handleRemove={function(key: string): void {
|
||||
searchStore.removeFilter(key);
|
||||
}}
|
||||
handleUpdate={function(key: string, updatedFilter: any): void {
|
||||
searchStore.updateFilter(key, updatedFilter);
|
||||
}}
|
||||
handleAdd={function(newFilter: Filter): void {
|
||||
searchStore.addFilter(newFilter);
|
||||
}}
|
||||
handleMove={function(draggedIndex: number, newPosition: number): void {
|
||||
searchStore.moveFilter(draggedIndex, newPosition);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { makeAutoObservable } from 'mobx';
|
||||
import { makeAutoObservable, runInAction } from 'mobx';
|
||||
import { makePersistable } from 'mobx-persist-store';
|
||||
import { filterService } from 'App/services';
|
||||
import { Filter, Operator, COMMON_FILTERS, getOperatorsByType } from './types/filterConstants';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { projectStore } from '@/mstore/index';
|
||||
|
||||
interface TopValue {
|
||||
rowCount?: number;
|
||||
|
|
@ -11,17 +15,40 @@ interface TopValues {
|
|||
[key: string]: TopValue[];
|
||||
}
|
||||
|
||||
interface ProjectFilters {
|
||||
[projectId: string]: Filter[];
|
||||
}
|
||||
|
||||
export default class FilterStore {
|
||||
topValues: TopValues = {};
|
||||
filters: ProjectFilters = {};
|
||||
commonFilters: Filter[] = [];
|
||||
isLoadingFilters: boolean = true;
|
||||
|
||||
filterCache: Record<string, Filter[]> = {};
|
||||
private pendingFetches: Record<string, Promise<Filter[]>> = {};
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
|
||||
// Set up persistence with 10-minute expiration
|
||||
/*void makePersistable(this, {
|
||||
name: 'FilterStore',
|
||||
// properties: ['filters', 'commonFilters'],
|
||||
properties: ['filters'],
|
||||
storage: window.localStorage,
|
||||
expireIn: 10 * 60 * 1000, // 10 minutes in milliseconds
|
||||
removeOnExpiration: true
|
||||
});*/
|
||||
|
||||
// Initialize common static filters
|
||||
this.initCommonFilters();
|
||||
}
|
||||
|
||||
setTopValues = (key: string, values: Record<string, any> | TopValue[]) => {
|
||||
const vals = Array.isArray(values) ? values : values.data;
|
||||
this.topValues[key] = vals?.filter(
|
||||
(value) => value !== null && value.value !== '',
|
||||
(value: any) => value !== null && value.value !== ''
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -29,13 +56,147 @@ export default class FilterStore {
|
|||
this.topValues = {};
|
||||
};
|
||||
|
||||
fetchTopValues = async (key: string, siteId: string, source?: string) => {
|
||||
const valKey = `${siteId}_${key}${source || ''}`
|
||||
fetchTopValues = async (id: string, siteId: string, source?: string) => {
|
||||
const valKey = `${siteId}_${id}${source || ''}`;
|
||||
|
||||
if (this.topValues[valKey] && this.topValues[valKey].length) {
|
||||
return Promise.resolve(this.topValues[valKey]);
|
||||
}
|
||||
return filterService.fetchTopValues(key, source).then((response: []) => {
|
||||
const filter = this.filters[siteId]?.find(i => i.id === id);
|
||||
if (!filter) {
|
||||
console.error('Filter not found in store:', id);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return filterService.fetchTopValues(filter.name?.toLowerCase(), source).then((response: []) => {
|
||||
this.setTopValues(valKey, response);
|
||||
});
|
||||
};
|
||||
|
||||
setFilters = (projectId: string, filters: Filter[]) => {
|
||||
this.filters[projectId] = filters;
|
||||
};
|
||||
|
||||
getFilters = (projectId: string): Filter[] => {
|
||||
const filters = this.filters[projectId] || [];
|
||||
return this.addOperatorsToFilters(filters);
|
||||
};
|
||||
|
||||
setIsLoadingFilters = (loading: boolean) => {
|
||||
this.isLoadingFilters = loading;
|
||||
};
|
||||
|
||||
resetFilters = () => {
|
||||
this.filters = {};
|
||||
};
|
||||
|
||||
processFilters = (filters: Filter[], category?: string): Filter[] => {
|
||||
return filters.map(filter => ({
|
||||
...filter,
|
||||
possibleTypes: filter.possibleTypes?.map(type => type.toLowerCase()) || [],
|
||||
type: filter.possibleTypes?.[0].toLowerCase() || 'string',
|
||||
category: category || 'custom',
|
||||
subCategory: category === 'events' ? (filter.autoCaptured ? 'auto' : 'user') : category,
|
||||
displayName: filter.displayName || filter.name,
|
||||
icon: FilterKey.LOCATION, // TODO - use actual icons
|
||||
isEvent: category === 'events',
|
||||
value: filter.value || [],
|
||||
propertyOrder: 'and'
|
||||
}));
|
||||
};
|
||||
|
||||
addOperatorsToFilters = (filters: Filter[]): Filter[] => {
|
||||
return filters.map(filter => ({
|
||||
...filter
|
||||
// operators: filter.operators?.length ? filter.operators : getOperatorsByType(filter.possibleTypes || [])
|
||||
}));
|
||||
};
|
||||
|
||||
// Modified to not add operators in cache
|
||||
fetchFilters = async (projectId: string): Promise<Filter[]> => {
|
||||
// Return cached filters with operators if available
|
||||
if (this.filters[projectId] && this.filters[projectId].length) {
|
||||
return Promise.resolve(this.getFilters(projectId));
|
||||
}
|
||||
|
||||
this.setIsLoadingFilters(true);
|
||||
|
||||
try {
|
||||
const response = await filterService.fetchFilters(projectId);
|
||||
|
||||
const processedFilters: Filter[] = [];
|
||||
|
||||
Object.keys(response.data).forEach((category: string) => {
|
||||
const { list, total } = response.data[category] || { list: [], total: 0 };
|
||||
const filters = this.processFilters(list, category);
|
||||
processedFilters.push(...filters);
|
||||
});
|
||||
|
||||
this.setFilters(projectId, processedFilters);
|
||||
|
||||
return this.getFilters(projectId);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch filters:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.setIsLoadingFilters(false);
|
||||
}
|
||||
};
|
||||
|
||||
initCommonFilters = () => {
|
||||
this.commonFilters = [...COMMON_FILTERS];
|
||||
};
|
||||
|
||||
getAllFilters = (projectId: string): Filter[] => {
|
||||
const projectFilters = this.filters[projectId] || [];
|
||||
// return this.addOperatorsToFilters([...this.commonFilters, ...projectFilters]);
|
||||
return this.addOperatorsToFilters([...projectFilters]);
|
||||
};
|
||||
|
||||
getCurrentProjectFilters = (): Filter[] => {
|
||||
return this.getAllFilters(projectStore.activeSiteId + '');
|
||||
};
|
||||
|
||||
// getEventFilters = (eventName: string): Filter[] => {
|
||||
// const filters = await filterService.fetchProperties(eventName)
|
||||
// return filters;
|
||||
// // const filters = this.getAllFilters(projectStore.activeSiteId + '');
|
||||
// // return filters.filter(i => !i.isEvent); // TODO fetch from the API for this event and cache them
|
||||
// };
|
||||
|
||||
getEventFilters = async (eventName: string): Promise<Filter[]> => {
|
||||
if (this.filterCache[eventName]) {
|
||||
return this.filterCache[eventName];
|
||||
}
|
||||
|
||||
if (await this.pendingFetches[eventName]) {
|
||||
return this.pendingFetches[eventName];
|
||||
}
|
||||
|
||||
try {
|
||||
this.pendingFetches[eventName] = this.fetchAndProcessPropertyFilters(eventName);
|
||||
const filters = await this.pendingFetches[eventName];
|
||||
|
||||
runInAction(() => {
|
||||
this.filterCache[eventName] = filters;
|
||||
});
|
||||
|
||||
delete this.pendingFetches[eventName];
|
||||
return filters;
|
||||
} catch (error) {
|
||||
delete this.pendingFetches[eventName];
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
private fetchAndProcessPropertyFilters = async (eventName: string): Promise<Filter[]> => {
|
||||
const resp = await filterService.fetchProperties(eventName);
|
||||
const names = resp.data.map((i: any) => i['allProperties.PropertyName']);
|
||||
|
||||
const activeSiteId = projectStore.activeSiteId + '';
|
||||
return this.filters[activeSiteId]?.filter((i: any) => names.includes(i.name)) || [];
|
||||
};
|
||||
|
||||
setCommonFilters = (filters: Filter[]) => {
|
||||
this.commonFilters = filters;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const filterMap = ({
|
|||
filters,
|
||||
sort,
|
||||
order
|
||||
}: any) => ({
|
||||
}: any) => ({
|
||||
value: checkValues(key, value),
|
||||
custom,
|
||||
type: category === FilterCategory.METADATA ? FilterKey.METADATA : key,
|
||||
|
|
@ -60,37 +60,22 @@ export const TAB_MAP: any = {
|
|||
|
||||
class SearchStore {
|
||||
list: SavedSearch[] = [];
|
||||
|
||||
latestRequestTime: number | null = null;
|
||||
|
||||
latestList = List();
|
||||
|
||||
alertMetricId: number | null = null;
|
||||
|
||||
instance = new Search();
|
||||
|
||||
savedSearch: ISavedSearch = new SavedSearch();
|
||||
|
||||
filterSearchList: any = {};
|
||||
|
||||
currentPage = 1;
|
||||
|
||||
pageSize = PER_PAGE;
|
||||
|
||||
activeTab = { name: 'All', type: 'all' };
|
||||
|
||||
scrollY = 0;
|
||||
|
||||
sessions = List();
|
||||
|
||||
total: number = 0;
|
||||
latestSessionCount: number = 0;
|
||||
loadingFilterSearch = false;
|
||||
|
||||
isSaving: boolean = false;
|
||||
|
||||
activeTags: any[] = [];
|
||||
|
||||
urlParsed: boolean = false;
|
||||
searchInProgress = false;
|
||||
|
||||
|
|
@ -146,7 +131,7 @@ class SearchStore {
|
|||
|
||||
editSavedSearch(instance: Partial<SavedSearch>) {
|
||||
this.savedSearch = new SavedSearch(
|
||||
Object.assign(this.savedSearch.toData(), instance),
|
||||
Object.assign(this.savedSearch.toData(), instance)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -172,14 +157,14 @@ class SearchStore {
|
|||
this.filterSearchList = response.reduce(
|
||||
(
|
||||
acc: Record<string, { projectId: number; value: string }[]>,
|
||||
item: any,
|
||||
item: any
|
||||
) => {
|
||||
const { projectId, type, value } = item;
|
||||
if (!acc[type]) acc[type] = [];
|
||||
acc[type].push({ projectId, value });
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
{}
|
||||
);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
|
|
@ -207,7 +192,7 @@ class SearchStore {
|
|||
|
||||
resetTags = () => {
|
||||
this.activeTags = ['all'];
|
||||
}
|
||||
};
|
||||
|
||||
toggleTag(tag?: iTag) {
|
||||
if (!tag) {
|
||||
|
|
@ -302,6 +287,7 @@ class SearchStore {
|
|||
(i: FilterItem) => i.key === filter.key
|
||||
);
|
||||
|
||||
// new random key
|
||||
filter.value = checkFilterValue(filter.value);
|
||||
filter.filters = filter.filters
|
||||
? filter.filters.map((subFilter: any) => ({
|
||||
|
|
@ -319,6 +305,7 @@ class SearchStore {
|
|||
oldFilter.merge(updatedFilter);
|
||||
this.updateFilter(index, updatedFilter);
|
||||
} else {
|
||||
filter.key = Math.random().toString(36).substring(7);
|
||||
this.instance.filters.push(filter);
|
||||
this.instance = new Search({
|
||||
...this.instance.toData()
|
||||
|
|
@ -332,12 +319,23 @@ class SearchStore {
|
|||
}
|
||||
}
|
||||
|
||||
moveFilter(draggedIndex: number, newPosition: number) {
|
||||
const newFilters = this.instance.filters.slice();
|
||||
const [removed] = newFilters.splice(draggedIndex, 1);
|
||||
newFilters.splice(newPosition, 0, removed);
|
||||
|
||||
this.instance = new Search({
|
||||
...this.instance.toData(),
|
||||
filters: newFilters
|
||||
});
|
||||
}
|
||||
|
||||
addFilterByKeyAndValue(
|
||||
key: any,
|
||||
value: any,
|
||||
operator?: string,
|
||||
sourceOperator?: string,
|
||||
source?: string,
|
||||
source?: string
|
||||
) {
|
||||
const defaultFilter = { ...filtersMap[key] };
|
||||
defaultFilter.value = value;
|
||||
|
|
@ -353,20 +351,19 @@ class SearchStore {
|
|||
this.addFilter(defaultFilter);
|
||||
}
|
||||
|
||||
refreshFilterOptions() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
updateSearch = (search: Partial<Search>) => {
|
||||
this.instance = Object.assign(this.instance, search);
|
||||
};
|
||||
|
||||
updateFilter = (index: number, search: Partial<FilterItem>) => {
|
||||
const newFilters = this.instance.filters.map((_filter: any, i: any) => {
|
||||
if (i === index) {
|
||||
return search;
|
||||
updateFilter = (key: string, search: Partial<FilterItem>) => {
|
||||
const newFilters = this.instance.filters.map((f: any) => {
|
||||
if (f.key === key) {
|
||||
return {
|
||||
...f,
|
||||
...search
|
||||
};
|
||||
}
|
||||
return _filter;
|
||||
return f;
|
||||
});
|
||||
|
||||
this.instance = new Search({
|
||||
|
|
@ -375,9 +372,9 @@ class SearchStore {
|
|||
});
|
||||
};
|
||||
|
||||
removeFilter = (index: number) => {
|
||||
removeFilter = (key: string) => {
|
||||
const newFilters = this.instance.filters.filter(
|
||||
(_filter: any, i: any) => i !== index,
|
||||
(f: any) => f.key !== key
|
||||
);
|
||||
|
||||
this.instance = new Search({
|
||||
|
|
@ -390,13 +387,9 @@ class SearchStore {
|
|||
this.scrollY = y;
|
||||
};
|
||||
|
||||
async fetchAutoplaySessions(page: number): Promise<void> {
|
||||
// TODO
|
||||
}
|
||||
|
||||
fetchSessions = async (
|
||||
async fetchSessions(
|
||||
force: boolean = false,
|
||||
bookmarked: boolean = false,
|
||||
bookmarked: boolean = false
|
||||
): Promise<void> => {
|
||||
if (this.searchInProgress) return;
|
||||
const filter = this.instance.toSearch();
|
||||
|
|
|
|||
|
|
@ -61,33 +61,22 @@ export default class Filter implements IFilter {
|
|||
}
|
||||
|
||||
filterId: string = '';
|
||||
|
||||
name: string = '';
|
||||
|
||||
autoOpen = false;
|
||||
|
||||
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,
|
||||
private readonly isMobile = false
|
||||
) {
|
||||
makeAutoObservable(this, {
|
||||
filters: observable,
|
||||
|
|
@ -101,7 +90,7 @@ export default class Filter implements IFilter {
|
|||
merge: action,
|
||||
addExcludeFilter: action,
|
||||
updateFilter: action,
|
||||
replaceFilters: action,
|
||||
replaceFilters: action
|
||||
});
|
||||
this.filters = filters.map((i) => new FilterItem(i));
|
||||
}
|
||||
|
|
@ -146,8 +135,8 @@ export default class Filter implements IFilter {
|
|||
new FilterItem(undefined, this.isConditional, this.isMobile).fromJson(
|
||||
i,
|
||||
undefined,
|
||||
isHeatmap,
|
||||
),
|
||||
isHeatmap
|
||||
)
|
||||
);
|
||||
this.eventsOrder = json.eventsOrder;
|
||||
return this;
|
||||
|
|
@ -156,7 +145,7 @@ export default class Filter implements IFilter {
|
|||
fromData(data: any) {
|
||||
this.name = data.name;
|
||||
this.filters = data.filters.map((i: Record<string, any>) =>
|
||||
new FilterItem(undefined, this.isConditional, this.isMobile).fromData(i),
|
||||
new FilterItem(undefined, this.isConditional, this.isMobile).fromData(i)
|
||||
);
|
||||
this.eventsOrder = data.eventsOrder;
|
||||
return this;
|
||||
|
|
@ -168,7 +157,7 @@ export default class Filter implements IFilter {
|
|||
filters: this.filters.map((i) => i.toJson()),
|
||||
eventsOrder: this.eventsOrder,
|
||||
startTimestamp: this.startTimestamp,
|
||||
endTimestamp: this.endTimestamp,
|
||||
endTimestamp: this.endTimestamp
|
||||
};
|
||||
return json;
|
||||
}
|
||||
|
|
@ -182,7 +171,7 @@ export default class Filter implements IFilter {
|
|||
const json = {
|
||||
name: this.name,
|
||||
filters: this.filters.map((i) => i.toJson()),
|
||||
eventsOrder: this.eventsOrder,
|
||||
eventsOrder: this.eventsOrder
|
||||
};
|
||||
return json;
|
||||
}
|
||||
|
|
@ -204,12 +193,12 @@ export default class Filter implements IFilter {
|
|||
this.addFilter({
|
||||
...filtersMap[FilterKey.LOCATION],
|
||||
value: [''],
|
||||
operator: 'isAny',
|
||||
operator: 'isAny'
|
||||
});
|
||||
this.addFilter({
|
||||
...filtersMap[FilterKey.CLICK],
|
||||
value: [''],
|
||||
operator: 'onAny',
|
||||
operator: 'onAny'
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +206,7 @@ export default class Filter implements IFilter {
|
|||
return {
|
||||
name: this.name,
|
||||
filters: this.filters.map((i) => i.toJson()),
|
||||
eventsOrder: this.eventsOrder,
|
||||
eventsOrder: this.eventsOrder
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +226,7 @@ export default class Filter implements IFilter {
|
|||
value: any,
|
||||
operator: undefined,
|
||||
sourceOperator: undefined,
|
||||
source: undefined,
|
||||
source: undefined
|
||||
) {
|
||||
let defaultFilter = { ...filtersMap[key] };
|
||||
if (defaultFilter) {
|
||||
|
|
|
|||
194
frontend/app/mstore/types/filterConstants.ts
Normal file
194
frontend/app/mstore/types/filterConstants.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
export interface Operator {
|
||||
value: string;
|
||||
label: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface FilterProperty {
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
type: string; // 'number' | 'string' | 'boolean' | etc.
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
id?: string;
|
||||
name: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
possibleTypes?: string[];
|
||||
autoCaptured: boolean;
|
||||
metadataName: string;
|
||||
category: string; // 'event' | 'filter' | 'action' | etc.
|
||||
subCategory?: string;
|
||||
type?: string; // 'number' | 'string' | 'boolean' | etc.
|
||||
icon?: string;
|
||||
properties?: FilterProperty[];
|
||||
operator?: string;
|
||||
operators?: Operator[];
|
||||
isEvent?: boolean;
|
||||
value?: string[];
|
||||
propertyOrder?: string;
|
||||
}
|
||||
|
||||
export const OPERATORS = {
|
||||
string: [
|
||||
{ value: 'is', label: 'is', displayName: 'Is', description: 'Exact match' },
|
||||
{ value: 'isNot', label: 'isNot', displayName: 'Is not', description: 'Not an exact match' },
|
||||
{ value: 'contains', label: 'contains', displayName: 'Contains', description: 'Contains the string' },
|
||||
{
|
||||
value: 'doesNotContain',
|
||||
label: 'doesNotContain',
|
||||
displayName: 'Does not contain',
|
||||
description: 'Does not contain the string'
|
||||
},
|
||||
{ value: 'startsWith', label: 'startsWith', displayName: 'Starts with', description: 'Starts with the string' },
|
||||
{ value: 'endsWith', label: 'endsWith', displayName: 'Ends with', description: 'Ends with the string' },
|
||||
{ value: 'isBlank', label: 'isBlank', displayName: 'Is blank', description: 'Is empty or null' },
|
||||
{ value: 'isNotBlank', label: 'isNotBlank', displayName: 'Is not blank', description: 'Is not empty or null' }
|
||||
],
|
||||
|
||||
number: [
|
||||
{ value: 'equals', label: 'equals', displayName: 'Equals', description: 'Exactly equals the value' },
|
||||
{
|
||||
value: 'doesNotEqual',
|
||||
label: 'doesNotEqual',
|
||||
displayName: 'Does not equal',
|
||||
description: 'Does not equal the value'
|
||||
},
|
||||
{ value: 'greaterThan', label: 'greaterThan', displayName: 'Greater than', description: 'Greater than the value' },
|
||||
{ value: 'lessThan', label: 'lessThan', displayName: 'Less than', description: 'Less than the value' },
|
||||
{
|
||||
value: 'greaterThanOrEquals',
|
||||
label: 'greaterThanOrEquals',
|
||||
displayName: 'Greater than or equals',
|
||||
description: 'Greater than or equal to the value'
|
||||
},
|
||||
{
|
||||
value: 'lessThanOrEquals',
|
||||
label: 'lessThanOrEquals',
|
||||
displayName: 'Less than or equals',
|
||||
description: 'Less than or equal to the value'
|
||||
},
|
||||
{ value: 'isBlank', label: 'isBlank', displayName: 'Is blank', description: 'Is empty or null' },
|
||||
{ value: 'isNotBlank', label: 'isNotBlank', displayName: 'Is not blank', description: 'Is not empty or null' }
|
||||
],
|
||||
|
||||
boolean: [
|
||||
{ value: 'isTrue', label: 'isTrue', displayName: 'Is true', description: 'Value is true' },
|
||||
{ value: 'isFalse', label: 'isFalse', displayName: 'Is false', description: 'Value is false' },
|
||||
{ value: 'isBlank', label: 'isBlank', displayName: 'Is blank', description: 'Is null' },
|
||||
{ value: 'isNotBlank', label: 'isNotBlank', displayName: 'Is not blank', description: 'Is not null' }
|
||||
],
|
||||
|
||||
date: [
|
||||
{ value: 'on', label: 'on', displayName: 'On', description: 'On the exact date' },
|
||||
{ value: 'notOn', label: 'notOn', displayName: 'Not on', description: 'Not on the exact date' },
|
||||
{ value: 'before', label: 'before', displayName: 'Before', description: 'Before the date' },
|
||||
{ value: 'after', label: 'after', displayName: 'After', description: 'After the date' },
|
||||
{ value: 'onOrBefore', label: 'onOrBefore', displayName: 'On or before', description: 'On or before the date' },
|
||||
{ value: 'onOrAfter', label: 'onOrAfter', displayName: 'On or after', description: 'On or after the date' },
|
||||
{ value: 'isBlank', label: 'isBlank', displayName: 'Is blank', description: 'Is empty or null' },
|
||||
{ value: 'isNotBlank', label: 'isNotBlank', displayName: 'Is not blank', description: 'Is not empty or null' }
|
||||
],
|
||||
|
||||
array: [
|
||||
{ value: 'contains', label: 'contains', displayName: 'Contains', description: 'Array contains the value' },
|
||||
{
|
||||
value: 'doesNotContain',
|
||||
label: 'doesNotContain',
|
||||
displayName: 'Does not contain',
|
||||
description: 'Array does not contain the value'
|
||||
},
|
||||
{ value: 'hasAny', label: 'hasAny', displayName: 'Has any', description: 'Array has any of the values' },
|
||||
{ value: 'hasAll', label: 'hasAll', displayName: 'Has all', description: 'Array has all of the values' },
|
||||
{ value: 'isEmpty', label: 'isEmpty', displayName: 'Is empty', description: 'Array is empty' },
|
||||
{ value: 'isNotEmpty', label: 'isNotEmpty', displayName: 'Is not empty', description: 'Array is not empty' }
|
||||
]
|
||||
};
|
||||
|
||||
export const COMMON_FILTERS: Filter[] = [];
|
||||
|
||||
export const getOperatorsByType = (type: string): Operator[] => {
|
||||
let operators: Operator[] = [];
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
case 'string':
|
||||
operators = OPERATORS.string;
|
||||
break;
|
||||
case 'number':
|
||||
case 'integer':
|
||||
case 'float':
|
||||
case 'decimal':
|
||||
operators = OPERATORS.number;
|
||||
break;
|
||||
case 'boolean':
|
||||
operators = OPERATORS.boolean;
|
||||
break;
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
case 'timestamp':
|
||||
operators = OPERATORS.date;
|
||||
break;
|
||||
case 'array':
|
||||
case 'list':
|
||||
operators = OPERATORS.array;
|
||||
break;
|
||||
default:
|
||||
// Default to string operators if type is unknown
|
||||
operators = OPERATORS.string;
|
||||
break;
|
||||
}
|
||||
|
||||
return operators;
|
||||
};
|
||||
|
||||
// export const getOperatorsByType = (types: string[]): Operator[] => {
|
||||
// const operatorSet = new Set<Operator>();
|
||||
//
|
||||
// if (!types || types.length === 0) {
|
||||
// return [...OPERATORS.string];
|
||||
// }
|
||||
//
|
||||
// // Process each type in the array
|
||||
// types.forEach(type => {
|
||||
// let operators: Operator[] = [];
|
||||
//
|
||||
// switch (type.toLowerCase()) {
|
||||
// case 'string':
|
||||
// operators = OPERATORS.string;
|
||||
// break;
|
||||
// case 'number':
|
||||
// case 'integer':
|
||||
// case 'float':
|
||||
// case 'decimal':
|
||||
// operators = OPERATORS.number;
|
||||
// break;
|
||||
// case 'boolean':
|
||||
// operators = OPERATORS.boolean;
|
||||
// break;
|
||||
// case 'date':
|
||||
// case 'datetime':
|
||||
// case 'timestamp':
|
||||
// operators = OPERATORS.date;
|
||||
// break;
|
||||
// case 'array':
|
||||
// case 'list':
|
||||
// operators = OPERATORS.array;
|
||||
// break;
|
||||
// default:
|
||||
// // Default to string operators if type is unknown
|
||||
// operators = OPERATORS.string;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// // Add operators to the set
|
||||
// operators.forEach(operator => {
|
||||
// operatorSet.add(operator);
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// // Convert Set back to Array and return
|
||||
// return Array.from(operatorSet);
|
||||
// };
|
||||
|
|
@ -2,59 +2,42 @@ import { FilterCategory, FilterKey, FilterType } from 'Types/filter/filterType';
|
|||
import {
|
||||
conditionalFiltersMap,
|
||||
filtersMap,
|
||||
mobileConditionalFiltersMap,
|
||||
mobileConditionalFiltersMap
|
||||
} from 'Types/filter/newFilter';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
import { pageUrlOperators } from '../../constants/filterOptions';
|
||||
import { pageUrlOperators } from '@/constants/filterOptions';
|
||||
|
||||
export default class FilterItem {
|
||||
type: string = '';
|
||||
|
||||
category: FilterCategory = FilterCategory.METADATA;
|
||||
|
||||
subCategory: string = '';
|
||||
|
||||
key: string = '';
|
||||
|
||||
label: string = '';
|
||||
|
||||
value: any = [''];
|
||||
|
||||
isEvent: boolean = false;
|
||||
|
||||
operator: string = '';
|
||||
|
||||
hasSource: boolean = false;
|
||||
|
||||
source: string = '';
|
||||
|
||||
sourceOperator: string = '';
|
||||
|
||||
sourceOperatorOptions: any = [];
|
||||
|
||||
filters: FilterItem[] = [];
|
||||
|
||||
operatorOptions: any[] = [];
|
||||
|
||||
options: any[] = [];
|
||||
|
||||
isActive: boolean = true;
|
||||
|
||||
completed: number = 0;
|
||||
|
||||
dropped: number = 0;
|
||||
|
||||
constructor(
|
||||
data: any = {},
|
||||
private readonly isConditional?: boolean,
|
||||
private readonly isMobile?: boolean,
|
||||
private readonly isMobile?: boolean
|
||||
) {
|
||||
makeAutoObservable(this);
|
||||
|
||||
if (Array.isArray(data.filters)) {
|
||||
data.filters = data.filters.map(
|
||||
(i: Record<string, any>) => new FilterItem(i),
|
||||
(i: Record<string, any>) => new FilterItem(i)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -163,7 +146,7 @@ export default class FilterItem {
|
|||
sourceOperator: this.sourceOperator,
|
||||
filters: Array.isArray(this.filters)
|
||||
? this.filters.map((i) => i.toJson())
|
||||
: [],
|
||||
: []
|
||||
};
|
||||
if (this.type === FilterKey.DURATION) {
|
||||
json.value = this.value.map((i: any) => (!i ? 0 : i));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
CUSTOM_RANGE,
|
||||
DATE_RANGE_VALUES,
|
||||
getDateRangeFromValue,
|
||||
getDateRangeFromValue
|
||||
} from 'App/dateRange';
|
||||
import Filter, { IFilter } from 'App/mstore/types/filter';
|
||||
import FilterItem from 'App/mstore/types/filterItem';
|
||||
|
|
@ -25,7 +25,7 @@ interface ISearch {
|
|||
userDevice?: string;
|
||||
fid0?: string;
|
||||
events: Event[];
|
||||
filters: IFilter[];
|
||||
filters: FilterItem[];
|
||||
minDuration?: number;
|
||||
maxDuration?: number;
|
||||
custom: Record<string, any>;
|
||||
|
|
@ -46,62 +46,36 @@ interface ISearch {
|
|||
|
||||
export default class Search {
|
||||
name: string;
|
||||
|
||||
searchId?: number;
|
||||
|
||||
referrer?: string;
|
||||
|
||||
userBrowser?: string;
|
||||
|
||||
userOs?: string;
|
||||
|
||||
userCountry?: string;
|
||||
|
||||
userDevice?: string;
|
||||
|
||||
fid0?: string;
|
||||
|
||||
events: Event[];
|
||||
|
||||
filters: FilterItem[];
|
||||
|
||||
minDuration?: number;
|
||||
|
||||
maxDuration?: number;
|
||||
|
||||
custom: Record<string, any>;
|
||||
|
||||
rangeValue: string;
|
||||
|
||||
startDate: number;
|
||||
|
||||
endDate: number;
|
||||
|
||||
groupByUser: boolean;
|
||||
|
||||
sort: string;
|
||||
|
||||
order: string;
|
||||
|
||||
viewed?: boolean;
|
||||
|
||||
consoleLogCount?: number;
|
||||
|
||||
eventsCount?: number;
|
||||
|
||||
suspicious?: boolean;
|
||||
|
||||
consoleLevel?: string;
|
||||
|
||||
strict: boolean;
|
||||
|
||||
eventsOrder: string;
|
||||
|
||||
limit: number;
|
||||
|
||||
constructor(initialData?: Partial<ISearch>) {
|
||||
makeAutoObservable(this, {
|
||||
filters: observable,
|
||||
filters: observable
|
||||
});
|
||||
Object.assign(this, {
|
||||
name: '',
|
||||
|
|
@ -131,7 +105,7 @@ export default class Search {
|
|||
strict: false,
|
||||
eventsOrder: 'then',
|
||||
limit: 10,
|
||||
...initialData,
|
||||
...initialData
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +145,7 @@ export default class Search {
|
|||
toSearch() {
|
||||
const js: any = { ...this };
|
||||
js.filters = this.filters.map((filter: any) =>
|
||||
new FilterItem(filter).toJson(),
|
||||
new FilterItem(filter).toJson()
|
||||
);
|
||||
|
||||
const { startDate, endDate } = this.getDateRange(
|
||||
|
|
@ -191,7 +165,7 @@ export default class Search {
|
|||
private getDateRange(
|
||||
rangeName: string,
|
||||
customStartDate: number,
|
||||
customEndDate: number,
|
||||
customEndDate: number
|
||||
roundMinutes?: number,
|
||||
): { startDate: number; endDate: number } {
|
||||
let endDate = new Date().getTime();
|
||||
|
|
@ -207,7 +181,9 @@ export default class Search {
|
|||
break;
|
||||
case CUSTOM_RANGE:
|
||||
if (!customStartDate || !customEndDate) {
|
||||
throw new Error('Start date and end date must be provided for CUSTOM_RANGE.');
|
||||
throw new Error(
|
||||
'Start date and end date must be provided for CUSTOM_RANGE.'
|
||||
);
|
||||
}
|
||||
startDate = customStartDate;
|
||||
endDate = customEndDate;
|
||||
|
|
@ -244,16 +220,17 @@ export default class Search {
|
|||
eventsOrder,
|
||||
startDate,
|
||||
endDate,
|
||||
filters,
|
||||
// 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;
|
||||
}),
|
||||
// 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;
|
||||
// })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,27 @@ export default class FilterService {
|
|||
this.client = client || new APIClient();
|
||||
}
|
||||
|
||||
fetchTopValues = async (key: string, source?: string) => {
|
||||
let path = `/PROJECT_ID/events/search?type=${key}`;
|
||||
fetchTopValues = async (name: string, source?: string) => {
|
||||
// const r = await this.client.get('/PROJECT_ID/events/search', params);
|
||||
// https://foss.openreplay.com/api/65/events/search?name=user_device_type&isEvent=false&q=sd
|
||||
|
||||
// let path = `/PROJECT_ID/events/search?type=${key}`;
|
||||
let path = `/PROJECT_ID/events/search?type=${name}`;
|
||||
if (source) {
|
||||
path += `&source=${source}`;
|
||||
}
|
||||
const response = await this.client.get(path);
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
fetchProperties = async (name: string) => {
|
||||
let path = `/pa/PROJECT_ID/properties/search?event_name=${name}`;
|
||||
const response = await this.client.get(path);
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
fetchFilters = async (projectId: string) => {
|
||||
const response = await this.client.get(`/pa/${projectId}/filters`);
|
||||
return await response.json();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ export enum IssueCategory {
|
|||
}
|
||||
|
||||
export enum FilterType {
|
||||
STRING = 'STRING',
|
||||
STRING = 'string',
|
||||
ISSUE = 'ISSUE',
|
||||
BOOLEAN = 'BOOLEAN',
|
||||
NUMBER = 'NUMBER',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue