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();
|
const KAI_PATH = routes.kai();
|
||||||
|
|
||||||
function PrivateRoutes() {
|
function PrivateRoutes() {
|
||||||
const { projectsStore, userStore, integrationsStore, searchStore } =
|
const { projectsStore, userStore, integrationsStore, searchStore, filterStore } =
|
||||||
useStore();
|
useStore();
|
||||||
const onboarding = userStore.onboarding;
|
const onboarding = userStore.onboarding;
|
||||||
const scope = userStore.scopeState;
|
const scope = userStore.scopeState;
|
||||||
|
|
@ -121,6 +121,7 @@ function PrivateRoutes() {
|
||||||
if (siteId && integrationsStore.integrations.siteId !== siteId) {
|
if (siteId && integrationsStore.integrations.siteId !== siteId) {
|
||||||
integrationsStore.integrations.setSiteId(siteId);
|
integrationsStore.integrations.setSiteId(siteId);
|
||||||
void integrationsStore.integrations.fetchIntegrations(siteId);
|
void integrationsStore.integrations.fetchIntegrations(siteId);
|
||||||
|
void filterStore.fetchFilters(siteId)
|
||||||
}
|
}
|
||||||
}, [siteId]);
|
}, [siteId]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { PlusIcon } from 'lucide-react';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Filter } from '@/mstore/types/filterConstants';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
series: any;
|
series: any;
|
||||||
|
|
@ -12,8 +14,10 @@ interface Props {
|
||||||
|
|
||||||
function AddStepButton({ series, excludeFilterKeys }: Props) {
|
function AddStepButton({ series, excludeFilterKeys }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { metricStore } = useStore();
|
const { metricStore, filterStore } = useStore();
|
||||||
const metric: any = metricStore.instance;
|
const metric: any = metricStore.instance;
|
||||||
|
const filters: Filter[] = filterStore.getCurrentProjectFilters();
|
||||||
|
// console.log('filters', filters)
|
||||||
|
|
||||||
const onAddFilter = (filter: any) => {
|
const onAddFilter = (filter: any) => {
|
||||||
series.filter.addFilter(filter);
|
series.filter.addFilter(filter);
|
||||||
|
|
@ -21,9 +25,9 @@ function AddStepButton({ series, excludeFilterKeys }: Props) {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filter={undefined}
|
filters={filters}
|
||||||
onFilterClick={onAddFilter}
|
onFilterClick={onAddFilter}
|
||||||
excludeFilterKeys={excludeFilterKeys}
|
mode={'filters'} // excludeFilterKeys={excludeFilterKeys}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -37,4 +41,4 @@ function AddStepButton({ series, excludeFilterKeys }: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddStepButton;
|
export default observer(AddStepButton);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
newFFlag,
|
newFFlag,
|
||||||
fflag,
|
fflag,
|
||||||
fflagRead,
|
fflagRead,
|
||||||
bookmarks,
|
bookmarks
|
||||||
} from 'App/routes';
|
} from 'App/routes';
|
||||||
import { withRouter, RouteComponentProps, useLocation } from 'react-router-dom';
|
import { withRouter, RouteComponentProps, useLocation } from 'react-router-dom';
|
||||||
import FlagView from 'Components/FFlags/FlagView/FlagView';
|
import FlagView from 'Components/FFlags/FlagView/FlagView';
|
||||||
|
|
@ -29,15 +29,18 @@ interface IProps extends RouteComponentProps {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO should move these routes to the Routes file
|
// TODO should move these routes to the Routes file
|
||||||
function Overview({ match: { params } }: IProps) {
|
function Overview({ match: { params } }: IProps) {
|
||||||
const { searchStore } = useStore();
|
const { searchStore, filterStore, projectsStore } = useStore();
|
||||||
const { siteId, fflagId } = params;
|
const { siteId, fflagId } = params;
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const tab = location.pathname.split('/')[2];
|
const tab = location.pathname.split('/')[2];
|
||||||
|
const { activeSiteId } = projectsStore;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
searchStore.setActiveTab(tab);
|
searchStore.setActiveTab(tab);
|
||||||
|
// void filterStore.fetchFilters(activeSiteId + '');
|
||||||
}, [tab]);
|
}, [tab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -69,5 +72,5 @@ function Overview({ match: { params } }: IProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withPageTitle('Sessions - OpenReplay')(
|
export default withPageTitle('Sessions - OpenReplay')(
|
||||||
withRouter(observer(Overview)),
|
withRouter(observer(Overview))
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,12 @@ interface Props {
|
||||||
|
|
||||||
const FilterAutoComplete = observer(
|
const FilterAutoComplete = observer(
|
||||||
({
|
({
|
||||||
params = {},
|
params = {},
|
||||||
onClose,
|
onClose,
|
||||||
onApply,
|
onApply,
|
||||||
values,
|
values,
|
||||||
placeholder,
|
placeholder
|
||||||
}: {
|
}: {
|
||||||
params: any;
|
params: any;
|
||||||
values: string[];
|
values: string[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -57,32 +57,32 @@ const FilterAutoComplete = observer(
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [options, setOptions] = useState<{ value: string; label: string }[]>(
|
const [options, setOptions] = useState<{ value: string; label: string }[]>(
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
const [initialFocus, setInitialFocus] = useState(false);
|
const [initialFocus, setInitialFocus] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { filterStore, projectsStore } = useStore();
|
const { filterStore, projectsStore } = useStore();
|
||||||
const _params = processKey(params);
|
const _params = processKey(params);
|
||||||
const filterKey = `${projectsStore.siteId}_${_params.type}${_params.key || ''}`;
|
const filterKey = `${projectsStore.siteId}_${params.id}`;
|
||||||
const topValues = filterStore.topValues[filterKey] || [];
|
const topValues = filterStore.topValues[filterKey] || [];
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setOptions([])
|
setOptions([]);
|
||||||
}, [projectsStore.siteId])
|
}, [projectsStore.siteId]);
|
||||||
|
|
||||||
const loadTopValues = async () => {
|
const loadTopValues = async () => {
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
if (projectsStore.siteId) {
|
if (projectsStore.siteId) {
|
||||||
await filterStore.fetchTopValues(_params.type, projectsStore.siteId, _params.key);
|
await filterStore.fetchTopValues(params.id, projectsStore.siteId);
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (topValues.length > 0) {
|
if (topValues.length > 0) {
|
||||||
const mappedValues = topValues.map((i) => ({
|
const mappedValues = topValues.map((i) => ({
|
||||||
value: i.value,
|
value: i.value,
|
||||||
label: i.value,
|
label: i.value
|
||||||
}));
|
}));
|
||||||
setOptions(mappedValues);
|
setOptions(mappedValues);
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +96,7 @@ const FilterAutoComplete = observer(
|
||||||
if (!inputValue.length) {
|
if (!inputValue.length) {
|
||||||
const mappedValues = topValues.map((i) => ({
|
const mappedValues = topValues.map((i) => ({
|
||||||
value: i.value,
|
value: i.value,
|
||||||
label: i.value,
|
label: i.value
|
||||||
}));
|
}));
|
||||||
setOptions(mappedValues);
|
setOptions(mappedValues);
|
||||||
return;
|
return;
|
||||||
|
|
@ -104,8 +104,8 @@ const FilterAutoComplete = observer(
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await searchService.fetchAutoCompleteValues({
|
const data = await searchService.fetchAutoCompleteValues({
|
||||||
..._params,
|
type: params.name?.toLowerCase(),
|
||||||
q: inputValue,
|
q: inputValue
|
||||||
});
|
});
|
||||||
const _options =
|
const _options =
|
||||||
data.map((i: any) => ({ value: i.value, label: i.value })) || [];
|
data.map((i: any) => ({ value: i.value, label: i.value })) || [];
|
||||||
|
|
@ -119,7 +119,7 @@ const FilterAutoComplete = observer(
|
||||||
|
|
||||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [
|
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [
|
||||||
params,
|
params,
|
||||||
topValues,
|
topValues
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleInputChange = (newValue: string) => {
|
const handleInputChange = (newValue: string) => {
|
||||||
|
|
@ -146,7 +146,7 @@ const FilterAutoComplete = observer(
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function AutoCompleteController(props: Props) {
|
function AutoCompleteController(props: Props) {
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,32 @@
|
||||||
import React from 'react';
|
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
||||||
import { Button } from 'antd';
|
import { Button, Space, Typography } from 'antd';
|
||||||
import { FilterKey, FilterType } from 'App/types/filter/filterType';
|
import { FilterKey } from 'App/types/filter/filterType';
|
||||||
import { CircleMinus } from 'lucide-react';
|
import { CircleMinus, Filter as FilterIcon } from 'lucide-react';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import FilterOperator from '../FilterOperator';
|
import FilterOperator from '../FilterOperator';
|
||||||
import FilterSelection from '../FilterSelection';
|
import FilterSelection from '../FilterSelection';
|
||||||
import FilterValue from '../FilterValue';
|
import FilterValue from '../FilterValue';
|
||||||
import FilterSource from '../FilterSource';
|
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 {
|
interface Props {
|
||||||
filterIndex?: number;
|
filterIndex?: number;
|
||||||
filter: any; // event/filter
|
filter: any;
|
||||||
onUpdate: (filter: any) => void;
|
onUpdate: (filter: any) => void;
|
||||||
onRemoveFilter: () => void;
|
onRemoveFilter: () => void;
|
||||||
isFilter?: boolean;
|
isFilter?: boolean;
|
||||||
saveRequestPayloads?: boolean;
|
saveRequestPayloads?: boolean;
|
||||||
disableDelete?: boolean;
|
disableDelete?: boolean;
|
||||||
excludeFilterKeys?: Array<string>;
|
|
||||||
excludeCategory?: Array<string>;
|
|
||||||
allowedFilterKeys?: Array<string>;
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
hideIndex?: boolean;
|
hideIndex?: boolean;
|
||||||
hideDelete?: boolean;
|
hideDelete?: boolean;
|
||||||
isConditional?: boolean;
|
isConditional?: boolean;
|
||||||
|
isSubItem?: boolean;
|
||||||
|
subFilterIndex?: number;
|
||||||
|
propertyOrder?: string;
|
||||||
|
onToggleOperator?: (newOp: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FilterItem(props: Props) {
|
function FilterItem(props: Props) {
|
||||||
|
|
@ -34,162 +37,290 @@ function FilterItem(props: Props) {
|
||||||
saveRequestPayloads,
|
saveRequestPayloads,
|
||||||
disableDelete = false,
|
disableDelete = false,
|
||||||
hideDelete = false,
|
hideDelete = false,
|
||||||
allowedFilterKeys = [],
|
|
||||||
excludeFilterKeys = [],
|
|
||||||
excludeCategory = [],
|
|
||||||
isConditional,
|
isConditional,
|
||||||
hideIndex = false,
|
hideIndex = false,
|
||||||
|
onUpdate,
|
||||||
|
onRemoveFilter,
|
||||||
|
readonly,
|
||||||
|
isSubItem = false,
|
||||||
|
subFilterIndex,
|
||||||
|
propertyOrder,
|
||||||
|
onToggleOperator
|
||||||
} = props;
|
} = props;
|
||||||
const canShowValues = !(
|
const [eventFilterOptions, setEventFilterOptions] = useState<Filter[]>([]);
|
||||||
filter.operator === 'isAny' ||
|
|
||||||
filter.operator === 'onAny' ||
|
const { filterStore } = useStore();
|
||||||
filter.operator === 'isUndefined'
|
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) => {
|
const isReversed = useMemo(() => filter.key === FilterKey.TAGGED_ELEMENT, [filter.key]);
|
||||||
props.onUpdate({
|
|
||||||
...filter,
|
const replaceFilter = useCallback(
|
||||||
value: filter.value,
|
(selectedFilter: any) => {
|
||||||
filters: filter.filters
|
onUpdate({
|
||||||
? filter.filters.map((i: any) => ({ ...i, value: [''] }))
|
...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,
|
||||||
|
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) => {
|
const addSubFilter = useCallback(
|
||||||
props.onUpdate({ ...filter, operator: value });
|
(selectedFilter: any) => {
|
||||||
};
|
onUpdate({
|
||||||
|
...filter,
|
||||||
|
filters: [...filteredSubFilters, selectedFilter]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[filter, onUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
const onSourceOperatorChange = (e: any, { value }: any) => {
|
|
||||||
props.onUpdate({ ...filter, sourceOperator: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUpdateSubFilter = (subFilter: any, subFilterIndex: any) => {
|
|
||||||
props.onUpdate({
|
|
||||||
...filter,
|
|
||||||
filters: filter.filters.map((i: any, index: any) => {
|
|
||||||
if (index === subFilterIndex) {
|
|
||||||
return subFilter;
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const isReversed = filter.key === FilterKey.TAGGED_ELEMENT;
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center w-full">
|
<div className="w-full">
|
||||||
<div className="flex items-center w-full flex-wrap">
|
<div className="flex items-center w-full">
|
||||||
{!isFilter && !hideIndex && filterIndex >= 0 && (
|
<div className="flex items-center flex-grow flex-wrap">
|
||||||
<div className="flex-shrink-0 w-6 h-6 text-xs flex items-center justify-center rounded-full bg-gray-lighter mr-2">
|
{!isFilter && !hideIndex && filterIndex !== undefined && filterIndex >= 0 && (
|
||||||
<span>{filterIndex + 1}</span>
|
<div
|
||||||
</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>
|
||||||
<FilterSelection
|
</div>
|
||||||
filter={filter}
|
|
||||||
mode={props.isFilter ? 'filters' : 'events'}
|
|
||||||
onFilterClick={replaceFilter}
|
|
||||||
allowedFilterKeys={allowedFilterKeys}
|
|
||||||
excludeFilterKeys={excludeFilterKeys}
|
|
||||||
excludeCategory={excludeCategory}
|
|
||||||
disabled={disableDelete || props.readonly}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex items-center flex-wrap',
|
|
||||||
isReversed ? 'flex-row-reverse ml-2' : 'flex-row',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Filter with Source */}
|
|
||||||
{filter.hasSource && (
|
|
||||||
<>
|
|
||||||
<FilterOperator
|
|
||||||
options={filter.sourceOperatorOptions}
|
|
||||||
onChange={onSourceOperatorChange}
|
|
||||||
className="mx-2 flex-shrink-0 btn-event-operator"
|
|
||||||
value={filter.sourceOperator}
|
|
||||||
isDisabled={filter.operatorDisabled || props.readonly}
|
|
||||||
/>
|
|
||||||
<FilterSource filter={filter} onUpdate={props.onUpdate} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filter values */}
|
{isSubItem && (
|
||||||
{!isSubFilter && filter.operatorOptions && (
|
<div className="w-14 text-right">
|
||||||
<>
|
{subFilterIndex === 0 && (
|
||||||
<FilterOperator
|
<Typography.Text className="text-neutral-500/90 mr-2">
|
||||||
options={filter.operatorOptions}
|
where
|
||||||
onChange={onOperatorChange}
|
</Typography.Text>
|
||||||
className="mx-2 flex-shrink-0 btn-sub-event-operator"
|
)}
|
||||||
value={filter.operator}
|
{subFilterIndex != 0 && propertyOrder && onToggleOperator && (
|
||||||
isDisabled={filter.operatorDisabled || props.readonly}
|
<Typography.Text
|
||||||
/>
|
className="text-neutral-500/90 mr-2 cursor-pointer"
|
||||||
{canShowValues && (
|
onClick={() =>
|
||||||
<>
|
onToggleOperator(propertyOrder === 'and' ? 'or' : 'and')
|
||||||
{props.readonly ? (
|
}
|
||||||
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip hover:border-neutral-400">
|
>
|
||||||
|
{propertyOrder}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FilterSelection
|
||||||
|
filters={filterSelections}
|
||||||
|
onFilterClick={replaceFilter}
|
||||||
|
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'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{filter.hasSource && (
|
||||||
|
<>
|
||||||
|
<FilterOperator
|
||||||
|
options={filter.sourceOperatorOptions}
|
||||||
|
onChange={handleSourceOperatorChange}
|
||||||
|
className="mx-2 flex-shrink-0 btn-event-operator"
|
||||||
|
value={filter.sourceOperator}
|
||||||
|
isDisabled={filter.operatorDisabled || readonly}
|
||||||
|
/>
|
||||||
|
<FilterSource filter={filter} onUpdate={onUpdate} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{operatorOptions.length && (
|
||||||
|
<>
|
||||||
|
<FilterOperator
|
||||||
|
options={operatorOptions}
|
||||||
|
onChange={handleOperatorChange}
|
||||||
|
className="mx-2 flex-shrink-0 btn-sub-event-operator"
|
||||||
|
value={filter.operator}
|
||||||
|
isDisabled={filter.operatorDisabled || readonly}
|
||||||
|
/>
|
||||||
|
{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
|
{filter.value
|
||||||
.map((val: string) =>
|
.map((val: string) =>
|
||||||
filter.options && filter.options.length
|
filter.options && filter.options.length
|
||||||
? (filter.options[
|
? filter.options[
|
||||||
filter.options.findIndex(
|
filter.options.findIndex((i: any) => i.value === val)
|
||||||
(i: any) => i.value === val,
|
]?.label ?? val
|
||||||
)
|
: val
|
||||||
]?.label ?? val)
|
|
||||||
: val,
|
|
||||||
)
|
)
|
||||||
.join(', ')}
|
.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<FilterValue
|
<FilterValue isConditional={isConditional} filter={filter} onUpdate={onUpdate} />
|
||||||
isConditional={isConditional}
|
))}
|
||||||
filter={filter}
|
</>
|
||||||
onUpdate={props.onUpdate}
|
)}
|
||||||
/>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* filters */}
|
{!readonly && !hideDelete && (
|
||||||
{isSubFilter && (
|
<div className="flex flex-shrink-0 gap-2">
|
||||||
<div className="grid grid-col ml-3 w-full">
|
{filter.isEvent && !isSubItem && (
|
||||||
{filter.filters
|
<FilterSelection
|
||||||
.filter(
|
filters={eventFilterOptions}
|
||||||
(i: any) =>
|
onFilterClick={addSubFilter}
|
||||||
(i.key !== FilterKey.FETCH_REQUEST_BODY &&
|
disabled={disableDelete || readonly}
|
||||||
i.key !== FilterKey.FETCH_RESPONSE_BODY) ||
|
>
|
||||||
saveRequestPayloads,
|
<Button
|
||||||
)
|
type="text"
|
||||||
.map((subFilter: any, subFilterIndex: any) => (
|
icon={<FilterIcon size={13} />}
|
||||||
<SubFilterItem
|
size="small"
|
||||||
filterIndex={subFilterIndex}
|
aria-label="Add filter"
|
||||||
filter={subFilter}
|
title="Filter"
|
||||||
onUpdate={(f) => onUpdateSubFilter(f, subFilterIndex)}
|
|
||||||
onRemoveFilter={props.onRemoveFilter}
|
|
||||||
/>
|
/>
|
||||||
))}
|
</FilterSelection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<CircleMinus size={13} />}
|
||||||
|
disabled={disableDelete}
|
||||||
|
onClick={onRemoveFilter}
|
||||||
|
size="small"
|
||||||
|
aria-label="Remove filter"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{props.readonly || props.hideDelete ? null : (
|
|
||||||
<div className="flex flex-shrink-0 self-start ml-auto">
|
{filter.filters?.length > 0 && (
|
||||||
<Button
|
<div className="pl-8 w-full">
|
||||||
disabled={disableDelete}
|
{filteredSubFilters.map((subFilter: any, index: number) => (
|
||||||
type="text"
|
<FilterItem
|
||||||
onClick={props.onRemoveFilter}
|
key={`subfilter-${index}`}
|
||||||
size="small"
|
filter={subFilter}
|
||||||
className="btn-remove-step mt-2"
|
subFilterIndex={index}
|
||||||
>
|
onUpdate={(updatedSubFilter) => handleUpdateSubFilter(updatedSubFilter, index)}
|
||||||
<CircleMinus size={14} />
|
onRemoveFilter={() => handleRemoveSubFilter(index)}
|
||||||
</Button>
|
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>
|
||||||
)}
|
)}
|
||||||
</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';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const EventsOrder = observer(
|
const EventsOrder = observer(
|
||||||
(props: { onChange: (e: any, v: any) => void; filter: any }) => {
|
(props: { onChange: (e: any, v: any) => void; orderProps: any }) => {
|
||||||
const { filter, onChange } = props;
|
const { onChange, orderProps: { eventsOrder, eventsOrderSupport } } = props;
|
||||||
const { eventsOrderSupport } = filter;
|
// const { eventsOrderSupport } = filter;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
key: 'then',
|
key: 'then',
|
||||||
label: t('THEN'),
|
label: t('THEN'),
|
||||||
disabled: eventsOrderSupport && !eventsOrderSupport.includes('then'),
|
disabled: eventsOrderSupport && !eventsOrderSupport.includes('then')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'and',
|
key: 'and',
|
||||||
label: t('AND'),
|
label: t('AND'),
|
||||||
disabled: eventsOrderSupport && !eventsOrderSupport.includes('and'),
|
disabled: eventsOrderSupport && !eventsOrderSupport.includes('and')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'or',
|
key: 'or',
|
||||||
label: t('OR'),
|
label: t('OR'),
|
||||||
disabled: eventsOrderSupport && !eventsOrderSupport.includes('or'),
|
disabled: eventsOrderSupport && !eventsOrderSupport.includes('or')
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
const onClick = ({ key }: any) => {
|
const onClick = ({ key }: any) => {
|
||||||
onChange(null, { name: 'eventsOrder', value: key, key });
|
onChange(null, { name: 'eventsOrder', value: key, key });
|
||||||
};
|
};
|
||||||
|
|
||||||
const selected = menuItems.find(
|
const selected = menuItems.find(
|
||||||
(item) => item.key === filter.eventsOrder,
|
(item) => item.key === eventsOrder
|
||||||
)?.label;
|
)?.label;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -57,7 +57,7 @@ const EventsOrder = observer(
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default EventsOrder;
|
export default EventsOrder;
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,15 @@ interface Props {
|
||||||
export const FilterList = observer((props: Props) => {
|
export const FilterList = observer((props: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
observeChanges = () => {},
|
observeChanges = () => {
|
||||||
|
},
|
||||||
filter,
|
filter,
|
||||||
excludeFilterKeys = [],
|
excludeFilterKeys = [],
|
||||||
isConditional,
|
isConditional,
|
||||||
onAddFilter,
|
onAddFilter,
|
||||||
readonly,
|
readonly,
|
||||||
borderless,
|
borderless,
|
||||||
excludeCategory,
|
excludeCategory
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { filters } = filter;
|
const { filters } = filter;
|
||||||
|
|
@ -53,13 +54,13 @@ export const FilterList = observer((props: Props) => {
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-white',
|
'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={{
|
style={{
|
||||||
borderBottomLeftRadius: props.mergeDown ? 0 : undefined,
|
borderBottomLeftRadius: props.mergeDown ? 0 : undefined,
|
||||||
borderBottomRightRadius: props.mergeDown ? 0 : undefined,
|
borderBottomRightRadius: props.mergeDown ? 0 : undefined,
|
||||||
borderTopLeftRadius: props.mergeUp ? 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' }}>
|
<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 "
|
className="hover:bg-active-blue px-5 "
|
||||||
style={{
|
style={{
|
||||||
marginLeft: '-1rem',
|
marginLeft: '-1rem',
|
||||||
width: 'calc(100% + 2rem)',
|
width: 'calc(100% + 2rem)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FilterItem
|
<FilterItem
|
||||||
|
|
@ -106,7 +107,7 @@ export const FilterList = observer((props: Props) => {
|
||||||
isConditional={isConditional}
|
isConditional={isConditional}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null,
|
) : null
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -115,7 +116,8 @@ export const FilterList = observer((props: Props) => {
|
||||||
export const EventsList = observer((props: Props) => {
|
export const EventsList = observer((props: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
observeChanges = () => {},
|
observeChanges = () => {
|
||||||
|
},
|
||||||
filter,
|
filter,
|
||||||
hideEventsOrder = false,
|
hideEventsOrder = false,
|
||||||
saveRequestPayloads,
|
saveRequestPayloads,
|
||||||
|
|
@ -126,7 +128,7 @@ export const EventsList = observer((props: Props) => {
|
||||||
onAddFilter,
|
onAddFilter,
|
||||||
cannotAdd,
|
cannotAdd,
|
||||||
excludeCategory,
|
excludeCategory,
|
||||||
borderless,
|
borderless
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { filters } = filter;
|
const { filters } = filter;
|
||||||
|
|
@ -143,7 +145,7 @@ export const EventsList = observer((props: Props) => {
|
||||||
|
|
||||||
const [hoveredItem, setHoveredItem] = React.useState<Record<string, any>>({
|
const [hoveredItem, setHoveredItem] = React.useState<Record<string, any>>({
|
||||||
i: null,
|
i: null,
|
||||||
position: null,
|
position: null
|
||||||
});
|
});
|
||||||
const [draggedInd, setDraggedItem] = React.useState<number | null>(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;
|
return draggedInd < hoveredIndex ? hoveredIndex - 1 : hoveredIndex;
|
||||||
},
|
},
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDragStart = React.useCallback(
|
const handleDragStart = React.useCallback(
|
||||||
|
|
@ -176,7 +178,7 @@ export const EventsList = observer((props: Props) => {
|
||||||
ev.dataTransfer.setDragImage(el, 0, 0);
|
ev.dataTransfer.setDragImage(el, 0, 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDrop = React.useCallback(
|
const handleDrop = React.useCallback(
|
||||||
|
|
@ -187,7 +189,7 @@ export const EventsList = observer((props: Props) => {
|
||||||
const newPosition = calculateNewPosition(
|
const newPosition = calculateNewPosition(
|
||||||
draggedInd,
|
draggedInd,
|
||||||
hoveredItem.i,
|
hoveredItem.i,
|
||||||
hoveredItem.position,
|
hoveredItem.position
|
||||||
);
|
);
|
||||||
|
|
||||||
const reorderedItem = newItems.splice(draggedInd, 1)[0];
|
const reorderedItem = newItems.splice(draggedInd, 1)[0];
|
||||||
|
|
@ -205,15 +207,15 @@ export const EventsList = observer((props: Props) => {
|
||||||
hoveredItem.position,
|
hoveredItem.position,
|
||||||
props,
|
props,
|
||||||
setHoveredItem,
|
setHoveredItem,
|
||||||
setDraggedItem,
|
setDraggedItem
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventsNum = filters.filter((i: any) => i.isEvent).length;
|
const eventsNum = filters.filter((i: any) => i.isEvent).length;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-white',
|
'bg-white',
|
||||||
borderless ? '' : 'py-2 px-4 rounded-xl border border-gray-lighter'
|
borderless ? '' : 'py-2 px-4 rounded-xl border border-gray-lighter'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -222,7 +224,7 @@ export const EventsList = observer((props: Props) => {
|
||||||
borderBottomRightRadius: props.mergeDown ? 0 : undefined,
|
borderBottomRightRadius: props.mergeDown ? 0 : undefined,
|
||||||
borderTopLeftRadius: props.mergeUp ? 0 : undefined,
|
borderTopLeftRadius: props.mergeUp ? 0 : undefined,
|
||||||
borderTopRightRadius: 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">
|
<div className="flex items-center mb-2 gap-2">
|
||||||
|
|
@ -232,8 +234,8 @@ export const EventsList = observer((props: Props) => {
|
||||||
mode="events"
|
mode="events"
|
||||||
filter={undefined}
|
filter={undefined}
|
||||||
onFilterClick={onAddFilter}
|
onFilterClick={onAddFilter}
|
||||||
excludeFilterKeys={excludeFilterKeys}
|
// excludeFilterKeys={excludeFilterKeys}
|
||||||
excludeCategory={excludeCategory}
|
// excludeCategory={excludeCategory}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
|
|
@ -263,8 +265,8 @@ export const EventsList = observer((props: Props) => {
|
||||||
className={cn(
|
className={cn(
|
||||||
'hover:bg-active-blue px-5 pe-3 gap-2 items-center flex',
|
'hover:bg-active-blue px-5 pe-3 gap-2 items-center flex',
|
||||||
{
|
{
|
||||||
'bg-[#f6f6f6]': hoveredItem.i === filterIndex,
|
'bg-[#f6f6f6]': hoveredItem.i === filterIndex
|
||||||
},
|
}
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'unset',
|
pointerEvents: 'unset',
|
||||||
|
|
@ -290,7 +292,7 @@ export const EventsList = observer((props: Props) => {
|
||||||
hoveredItem.i === filterIndex &&
|
hoveredItem.i === filterIndex &&
|
||||||
hoveredItem.position === 'bottom'
|
hoveredItem.position === 'bottom'
|
||||||
? '1px dashed #888'
|
? '1px dashed #888'
|
||||||
: undefined,
|
: undefined
|
||||||
}}
|
}}
|
||||||
id={`${filter.key}-${filterIndex}`}
|
id={`${filter.key}-${filterIndex}`}
|
||||||
onDragOver={(e) => handleDragOverEv(e, filterIndex)}
|
onDragOver={(e) => handleDragOverEv(e, filterIndex)}
|
||||||
|
|
@ -305,7 +307,7 @@ export const EventsList = observer((props: Props) => {
|
||||||
handleDragStart(
|
handleDragStart(
|
||||||
e,
|
e,
|
||||||
filterIndex,
|
filterIndex,
|
||||||
`${filter.key}-${filterIndex}`,
|
`${filter.key}-${filterIndex}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onDragEnd={() => {
|
onDragEnd={() => {
|
||||||
|
|
@ -313,7 +315,7 @@ export const EventsList = observer((props: Props) => {
|
||||||
setDraggedItem(null);
|
setDraggedItem(null);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
cursor: draggedInd !== null ? 'grabbing' : 'grab',
|
cursor: draggedInd !== null ? 'grabbing' : 'grab'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GripVertical size={16} />
|
<GripVertical size={16} />
|
||||||
|
|
@ -326,13 +328,13 @@ export const EventsList = observer((props: Props) => {
|
||||||
onRemoveFilter={() => onRemoveFilter(filterIndex)}
|
onRemoveFilter={() => onRemoveFilter(filterIndex)}
|
||||||
saveRequestPayloads={saveRequestPayloads}
|
saveRequestPayloads={saveRequestPayloads}
|
||||||
disableDelete={cannotDeleteFilter}
|
disableDelete={cannotDeleteFilter}
|
||||||
excludeFilterKeys={excludeFilterKeys}
|
// excludeFilterKeys={excludeFilterKeys}
|
||||||
readonly={props.readonly}
|
readonly={props.readonly}
|
||||||
isConditional={isConditional}
|
isConditional={isConditional}
|
||||||
excludeCategory={excludeCategory}
|
// excludeCategory={excludeCategory}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null,
|
) : null
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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 cn from 'classnames';
|
||||||
import {
|
import { Pointer, ChevronRight, MousePointerClick } from 'lucide-react';
|
||||||
AppWindow,
|
import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
|
||||||
ArrowUpDown,
|
import { Loader } from 'UI';
|
||||||
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 AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||||
import { Input, Button } from 'antd';
|
import { Input, Space, Typography } from 'antd';
|
||||||
|
|
||||||
import { FilterCategory, FilterKey, FilterType } from 'Types/filter/filterType';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useStore } from 'App/mstore';
|
|
||||||
import stl from './FilterModal.module.css';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Filter } from '@/mstore/types/filterConstants';
|
||||||
|
|
||||||
export const IconMap = {
|
export const getIconForFilter = (filter: Filter) => <MousePointerClick size={14} />;
|
||||||
[FilterKey.CLICK]: <Pointer size={14} />,
|
|
||||||
[FilterKey.LOCATION]: <Navigation size={14} />,
|
// Helper function for grouping filters
|
||||||
[FilterKey.INPUT]: <RectangleEllipsis size={14} />,
|
const groupFiltersByCategory = (filters: Filter[]) => {
|
||||||
[FilterKey.CUSTOM]: <Code size={14} />,
|
if (!filters?.length) return {};
|
||||||
[FilterKey.FETCH]: <ArrowUpDown size={14} />,
|
|
||||||
[FilterKey.GRAPHQL]: <Network size={14} />,
|
return filters.reduce((acc, filter) => {
|
||||||
[FilterKey.STATEACTION]: <RectangleEllipsis size={14} />,
|
const category = filter.category
|
||||||
[FilterKey.ERROR]: <OctagonAlert size={14} />,
|
? filter.category.charAt(0).toUpperCase() + filter.category.slice(1)
|
||||||
[FilterKey.ISSUE]: <CircleAlert size={14} />,
|
: 'Unknown';
|
||||||
[FilterKey.FETCH_FAILED]: <Code size={14} />,
|
|
||||||
[FilterKey.DOM_COMPLETE]: <ArrowUpDown size={14} />,
|
if (!acc[category]) acc[category] = [];
|
||||||
[FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: <Network size={14} />,
|
acc[category].push(filter);
|
||||||
[FilterKey.TTFB]: <Timer size={14} />,
|
return acc;
|
||||||
[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} />,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function filterJson(
|
// Optimized filtering function with early returns
|
||||||
jsonObj: Record<string, any>,
|
const getFilteredEntries = (query: string, filters: Filter[]) => {
|
||||||
excludeKeys: string[] = [],
|
const trimmedQuery = query.trim().toLowerCase();
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMatchingEntries = (
|
if (!filters || Object.keys(filters).length === 0) {
|
||||||
searchQuery: string,
|
return { matchingCategories: ['All'], matchingFilters: {} };
|
||||||
filters: Record<string, any>,
|
}
|
||||||
) => {
|
|
||||||
const matchingCategories: string[] = [];
|
|
||||||
const matchingFilters: Record<string, any> = {};
|
|
||||||
const lowerCaseQuery = searchQuery.toLowerCase();
|
|
||||||
|
|
||||||
if (lowerCaseQuery.length === 0) {
|
if (!trimmedQuery) {
|
||||||
return {
|
return {
|
||||||
matchingCategories: ['All', ...Object.keys(filters)],
|
matchingCategories: ['All', ...Object.keys(filters)],
|
||||||
matchingFilters: filters,
|
matchingFilters: filters
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(filters).forEach((name) => {
|
const matchingCategories = ['All'];
|
||||||
if (name.toLocaleLowerCase().includes(lowerCaseQuery)) {
|
const matchingFilters = {};
|
||||||
matchingCategories.push(name);
|
|
||||||
matchingFilters[name] = filters[name];
|
|
||||||
} else {
|
|
||||||
const filtersQuery = filters[name].filter((filterOption: any) =>
|
|
||||||
filterOption.label.toLocaleLowerCase().includes(lowerCaseQuery),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (filtersQuery.length > 0) matchingFilters[name] = filtersQuery;
|
// Single pass through the data with optimized conditionals
|
||||||
filtersQuery.length > 0 && matchingCategories.push(name);
|
Object.entries(filters).forEach(([name, categoryFilters]) => {
|
||||||
|
const categoryMatch = name.toLowerCase().includes(trimmedQuery);
|
||||||
|
|
||||||
|
if (categoryMatch) {
|
||||||
|
matchingCategories.push(name);
|
||||||
|
matchingFilters[name] = categoryFilters;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = categoryFilters.filter(
|
||||||
|
(filter: Filter) =>
|
||||||
|
filter.displayName?.toLowerCase().includes(trimmedQuery) ||
|
||||||
|
filter.name?.toLowerCase().includes(trimmedQuery)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filtered.length) {
|
||||||
|
matchingCategories.push(name);
|
||||||
|
matchingFilters[name] = filtered;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { matchingCategories, matchingFilters };
|
||||||
matchingCategories: ['All', ...matchingCategories],
|
|
||||||
matchingFilters,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
// Custom debounce hook to optimize search
|
||||||
isLive?: boolean;
|
const useDebounce = (value: any, delay = 300) => {
|
||||||
conditionalFilters: any;
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
const handler = setTimeout(() => {
|
||||||
inputRef.current.focus();
|
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]);
|
}, [category]);
|
||||||
|
|
||||||
const displayedFilters =
|
if (isLoading) {
|
||||||
category === 'All'
|
return (
|
||||||
? Object.entries(matchingFilters).flatMap(([category, filters]) =>
|
<div style={{ width: '490px', maxHeight: '380px' }}>
|
||||||
filters.map((f: any) => ({ ...f, category })),
|
<div className="flex items-center justify-center h-60">
|
||||||
)
|
<Loader loading />
|
||||||
: matchingFilters[category];
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={stl.wrapper} style={{ width: '460px', maxHeight: '380px' }}>
|
<div style={{ width: '490px', maxHeight: '380px' }}>
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="mb-4 rounded-xl text-lg font-medium placeholder:text-lg placeholder:font-medium placeholder:text-neutral-300"
|
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)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2 items-start">
|
|
||||||
<div className="flex flex-col gap-1">
|
{isResultEmpty ? (
|
||||||
{matchingCategories.map((key) => (
|
<div className="flex items-center flex-col justify-center h-60">
|
||||||
<div
|
<AnimatedSVG name={ICONS.NO_SEARCH_RESULTS} size={30} />
|
||||||
key={key}
|
<div className="font-medium px-3 mt-4">{t('No matching filters.')}</div>
|
||||||
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>
|
||||||
<div
|
) : (
|
||||||
className="flex flex-col gap-1 overflow-y-auto w-full h-full"
|
<div className="flex gap-2 items-start">
|
||||||
style={{ maxHeight: 300, flex: 2 }}
|
<div className="flex flex-col gap-1 min-w-40">
|
||||||
>
|
<CategoryList
|
||||||
{displayedFilters && displayedFilters.length
|
categories={matchingCategories}
|
||||||
? displayedFilters.map((filter: Record<string, any>) => (
|
activeCategory={category}
|
||||||
<div
|
onSelect={handleCategoryClick}
|
||||||
key={filter.label}
|
/>
|
||||||
className={cn(
|
</div>
|
||||||
'flex items-center p-2 cursor-pointer gap-1 rounded-lg hover:bg-active-blue',
|
<div className="flex flex-col gap-1 overflow-y-auto w-full" style={{ maxHeight: 300, flex: 2 }}>
|
||||||
)}
|
{displayedFilters.length > 0 ? (
|
||||||
onClick={() => parseAndAdd({ ...filter })}
|
displayedFilters.map((filter: Filter, index: number) => (
|
||||||
>
|
<FilterItem
|
||||||
{filter.category ? (
|
key={`${filter.name}-${index}`}
|
||||||
<div
|
filter={filter}
|
||||||
style={{ width: 100 }}
|
onClick={handleFilterClick}
|
||||||
className="text-neutral-500/90 w-full flex justify-between items-center"
|
showCategory={category === 'All'}
|
||||||
>
|
/>
|
||||||
<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">
|
|
||||||
<AnimatedSVG name={ICONS.NO_SEARCH_RESULTS} size={30} />
|
|
||||||
<div className="font-medium px-3 mt-4">
|
|
||||||
{' '}
|
|
||||||
{t('No matching filters.')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
Object.keys(filterSearchList).map((key, index) => {
|
<div className="flex items-center justify-center h-40">
|
||||||
const filter = filterSearchList[key];
|
<div className="text-neutral-500">{t('No filters in this category')}</div>
|
||||||
const option = filtersMap[key];
|
</div>
|
||||||
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>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Loader>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(FilterModal);
|
export default React.memo(observer(FilterModal));
|
||||||
|
|
|
||||||
|
|
@ -1,121 +1,64 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
import { Popover } from 'antd';
|
||||||
import { assist as assistRoute, isRoute } from 'App/routes';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import FilterModal from '../FilterModal';
|
import FilterModal from '../FilterModal/FilterModal';
|
||||||
import { getNewIcon } from '../FilterModal/FilterModal';
|
import { Filter } from '@/mstore/types/filterConstants';
|
||||||
|
|
||||||
const ASSIST_ROUTE = assistRoute();
|
interface FilterSelectionProps {
|
||||||
|
filters: Filter[];
|
||||||
interface Props {
|
onFilterClick: (filter: Filter) => void;
|
||||||
filter?: any;
|
children?: React.ReactNode;
|
||||||
onFilterClick: (filter: any) => void;
|
|
||||||
children?: any;
|
|
||||||
excludeFilterKeys?: Array<string>;
|
|
||||||
excludeCategory?: Array<string>;
|
|
||||||
allowedFilterKeys?: Array<string>;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isConditional?: boolean;
|
|
||||||
isMobile?: boolean;
|
|
||||||
mode: 'filters' | 'events';
|
|
||||||
isLive?: boolean;
|
isLive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FilterSelection(props: Props) {
|
const FilterSelection: React.FC<FilterSelectionProps> = observer(({
|
||||||
const {
|
filters,
|
||||||
filter,
|
onFilterClick,
|
||||||
onFilterClick,
|
children,
|
||||||
children,
|
disabled = false,
|
||||||
excludeFilterKeys = [],
|
isLive
|
||||||
excludeCategory = [],
|
}) => {
|
||||||
allowedFilterKeys = [],
|
const [open, setOpen] = useState(false);
|
||||||
disabled = false,
|
|
||||||
isConditional,
|
|
||||||
isMobile,
|
|
||||||
mode,
|
|
||||||
isLive,
|
|
||||||
} = props;
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
const modalRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const onAddFilter = (filter: any) => {
|
const handleFilterClick = useCallback((selectedFilter: Filter) => {
|
||||||
onFilterClick(filter);
|
onFilterClick(selectedFilter);
|
||||||
setShowModal(false);
|
setOpen(false);
|
||||||
};
|
}, [onFilterClick]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||||
if (showModal && modalRef.current) {
|
if (!disabled) {
|
||||||
const modalRect = modalRef.current.getBoundingClientRect();
|
setOpen(newOpen);
|
||||||
const viewportWidth = window.innerWidth;
|
|
||||||
if (modalRect.right > viewportWidth) {
|
|
||||||
modalRef.current.style.left = 'unset';
|
|
||||||
modalRef.current.style.right = '-280px';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [showModal]);
|
}, [disabled]);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<FilterModal
|
||||||
|
onFilterClick={handleFilterClick}
|
||||||
|
filters={filters}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const triggerElement = React.isValidElement(children)
|
||||||
|
? React.cloneElement(children, { disabled })
|
||||||
|
: children;
|
||||||
|
|
||||||
const label = filter?.category === 'Issue' ? 'Issue' : filter?.label;
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-shrink-0 my-1.5">
|
<div className="relative flex-shrink-0">
|
||||||
<OutsideClickDetectingDiv
|
<Popover
|
||||||
className="relative"
|
content={content}
|
||||||
onClickOutside={() => {
|
trigger="click"
|
||||||
setTimeout(() => {
|
open={open}
|
||||||
setShowModal(false);
|
onOpenChange={handleOpenChange}
|
||||||
}, 0);
|
placement="bottomLeft"
|
||||||
}}
|
overlayClassName="filter-selection-popover rounded-lg border border-gray-200 shadow-sm shadow-gray-200"
|
||||||
|
destroyTooltipOnHide
|
||||||
|
arrow={false}
|
||||||
>
|
>
|
||||||
{children ? (
|
{triggerElement}
|
||||||
React.cloneElement(children, {
|
</Popover>
|
||||||
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"
|
|
||||||
>
|
|
||||||
<FilterModal
|
|
||||||
isLive={isRoute(ASSIST_ROUTE, window.location.pathname) || isLive}
|
|
||||||
onFilterClick={onAddFilter}
|
|
||||||
excludeFilterKeys={excludeFilterKeys}
|
|
||||||
allowedFilterKeys={allowedFilterKeys}
|
|
||||||
excludeCategory={excludeCategory}
|
|
||||||
isConditional={isConditional}
|
|
||||||
isMobile={isMobile}
|
|
||||||
mode={mode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</OutsideClickDetectingDiv>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default observer(FilterSelection);
|
export default FilterSelection;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ interface Props {
|
||||||
onUpdate: (filter: any) => void;
|
onUpdate: (filter: any) => void;
|
||||||
isConditional?: boolean;
|
isConditional?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FilterValue(props: Props) {
|
function FilterValue(props: Props) {
|
||||||
const { filter } = props;
|
const { filter } = props;
|
||||||
const isAutoOpen = filter.autoOpen;
|
const isAutoOpen = filter.autoOpen;
|
||||||
|
|
@ -29,7 +30,7 @@ function FilterValue(props: Props) {
|
||||||
}, [isAutoOpen]);
|
}, [isAutoOpen]);
|
||||||
const [durationValues, setDurationValues] = useState({
|
const [durationValues, setDurationValues] = useState({
|
||||||
minDuration: filter.value?.[0],
|
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;
|
const showCloseButton = filter.value.length > 1;
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ function FilterValue(props: Props) {
|
||||||
|
|
||||||
const onRemoveValue = (valueIndex: any) => {
|
const onRemoveValue = (valueIndex: any) => {
|
||||||
const newValue = filter.value.filter(
|
const newValue = filter.value.filter(
|
||||||
(_: any, index: any) => index !== valueIndex,
|
(_: any, index: any) => index !== valueIndex
|
||||||
);
|
);
|
||||||
props.onUpdate({ ...filter, value: newValue });
|
props.onUpdate({ ...filter, value: newValue });
|
||||||
};
|
};
|
||||||
|
|
@ -60,7 +61,7 @@ function FilterValue(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const debounceOnSelect = React.useCallback(debounce(onChange, 500), [
|
const debounceOnSelect = React.useCallback(debounce(onChange, 500), [
|
||||||
onChange,
|
onChange
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onDurationChange = (newValues: any) => {
|
const onDurationChange = (newValues: any) => {
|
||||||
|
|
@ -77,14 +78,19 @@ function FilterValue(props: Props) {
|
||||||
) {
|
) {
|
||||||
props.onUpdate({
|
props.onUpdate({
|
||||||
...filter,
|
...filter,
|
||||||
value: [durationValues.minDuration, durationValues.maxDuration],
|
value: [durationValues.minDuration, durationValues.maxDuration]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getParms = (key: any) => {
|
const getParams = (key: any) => {
|
||||||
let params: any = { type: filter.key };
|
let params: any = {
|
||||||
|
type: filter.key,
|
||||||
|
name: filter.name,
|
||||||
|
isEvent: filter.isEvent,
|
||||||
|
id: filter.id
|
||||||
|
};
|
||||||
switch (filter.category) {
|
switch (filter.category) {
|
||||||
case FilterCategory.METADATA:
|
case FilterCategory.METADATA:
|
||||||
params = { type: FilterKey.METADATA, key };
|
params = { type: FilterKey.METADATA, key };
|
||||||
|
|
@ -99,6 +105,7 @@ function FilterValue(props: Props) {
|
||||||
|
|
||||||
const renderValueFiled = (value: any[]) => {
|
const renderValueFiled = (value: any[]) => {
|
||||||
const showOrButton = filter.value.length > 1;
|
const showOrButton = filter.value.length > 1;
|
||||||
|
|
||||||
function BaseFilterLocalAutoComplete(props) {
|
function BaseFilterLocalAutoComplete(props) {
|
||||||
return (
|
return (
|
||||||
<FilterAutoCompleteLocal
|
<FilterAutoCompleteLocal
|
||||||
|
|
@ -115,6 +122,7 @@ function FilterValue(props: Props) {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BaseDropDown(props) {
|
function BaseDropDown(props) {
|
||||||
return (
|
return (
|
||||||
<FilterValueDropdown
|
<FilterValueDropdown
|
||||||
|
|
@ -127,6 +135,7 @@ function FilterValue(props: Props) {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (filter.type) {
|
switch (filter.type) {
|
||||||
case FilterType.NUMBER_MULTIPLE:
|
case FilterType.NUMBER_MULTIPLE:
|
||||||
return (
|
return (
|
||||||
|
|
@ -145,7 +154,23 @@ function FilterValue(props: Props) {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case FilterType.STRING:
|
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:
|
case FilterType.DROPDOWN:
|
||||||
return <BaseDropDown />;
|
return <BaseDropDown />;
|
||||||
case FilterType.ISSUE:
|
case FilterType.ISSUE:
|
||||||
|
|
@ -181,7 +206,7 @@ function FilterValue(props: Props) {
|
||||||
onRemoveValue={(index) => onRemoveValue(index)}
|
onRemoveValue={(index) => onRemoveValue(index)}
|
||||||
method="GET"
|
method="GET"
|
||||||
endpoint="/PROJECT_ID/events/search"
|
endpoint="/PROJECT_ID/events/search"
|
||||||
params={getParms(filter.key)}
|
params={getParams(filter.key)}
|
||||||
headerText=""
|
headerText=""
|
||||||
placeholder={filter.placeholder}
|
placeholder={filter.placeholder}
|
||||||
onSelect={(e, item, index) => onChange(e, item, index)}
|
onSelect={(e, item, index) => onChange(e, item, index)}
|
||||||
|
|
@ -196,7 +221,7 @@ function FilterValue(props: Props) {
|
||||||
<div
|
<div
|
||||||
id="ignore-outside"
|
id="ignore-outside"
|
||||||
className={cn('grid gap-3 w-fit flex-wrap my-1.5', {
|
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)}
|
{renderValueFiled(filter.value)}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ interface Props {
|
||||||
onRemoveFilter: () => void;
|
onRemoveFilter: () => void;
|
||||||
isFilter?: boolean;
|
isFilter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SubFilterItem(props: Props) {
|
export default function SubFilterItem(props: Props) {
|
||||||
const { isFilter = false, filterIndex, filter } = props;
|
const { isFilter = false, filterIndex, filter } = props;
|
||||||
const canShowValues = !(
|
const canShowValues = !(
|
||||||
|
|
|
||||||
|
|
@ -1,110 +1,128 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { debounce } from 'App/utils';
|
|
||||||
import { FilterList, EventsList } from 'Shared/Filters/FilterList';
|
|
||||||
|
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler';
|
import UnifiedFilterList from 'Shared/Filters/FilterList/UnifiedFilterList';
|
||||||
import { FilterKey } from 'App/types/filter/filterType';
|
import FilterSelection from 'Shared/Filters/FilterSelection';
|
||||||
import { addOptionsToFilter } from 'App/types/filter/newFilter';
|
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() {
|
function SessionFilters() {
|
||||||
const { searchStore, projectsStore, customFieldStore, tagWatchStore } =
|
const { searchStore, projectsStore, filterStore } =
|
||||||
useStore();
|
useStore();
|
||||||
|
|
||||||
const appliedFilter = searchStore.instance;
|
const searchInstance = searchStore.instance;
|
||||||
const metaLoading = customFieldStore.isLoading;
|
|
||||||
const saveRequestPayloads =
|
const saveRequestPayloads =
|
||||||
projectsStore.instance?.saveRequestPayloads ?? false;
|
projectsStore.instance?.saveRequestPayloads ?? false;
|
||||||
const activeProject = projectsStore.active;
|
|
||||||
|
|
||||||
const reloadTags = async () => {
|
const allFilterOptions: Filter[] = filterStore.getCurrentProjectFilters();
|
||||||
const tags = await tagWatchStore.getTags();
|
const eventOptions = allFilterOptions.filter(i => i.isEvent);
|
||||||
if (tags) {
|
const propertyOptions = allFilterOptions.filter(i => !i.isEvent);
|
||||||
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 onAddFilter = (filter: any) => {
|
const onAddFilter = (filter: any) => {
|
||||||
filter.autoOpen = true;
|
filter.autoOpen = true;
|
||||||
searchStore.addFilter(filter);
|
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) => {
|
const onChangeEventsOrder = (e: any, { value }: any) => {
|
||||||
searchStore.edit({
|
searchStore.edit({
|
||||||
eventsOrder: value,
|
eventsOrder: value
|
||||||
});
|
});
|
||||||
|
|
||||||
// debounceFetch();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<EventsList
|
<div
|
||||||
filter={appliedFilter}
|
className={cn(
|
||||||
onAddFilter={onAddFilter}
|
'bg-white',
|
||||||
onUpdateFilter={onUpdateFilter}
|
'py-2 px-4 rounded-xl border border-gray-lighter'
|
||||||
onRemoveFilter={onRemoveFilter}
|
)}
|
||||||
onChangeEventsOrder={onChangeEventsOrder}
|
>
|
||||||
saveRequestPayloads={saveRequestPayloads}
|
<FilterListHeader
|
||||||
onFilterMove={onFilterMove}
|
title={'Events'}
|
||||||
mergeDown
|
showEventsOrder={true}
|
||||||
/>
|
orderProps={searchInstance}
|
||||||
<FilterList
|
onChangeOrder={onChangeEventsOrder}
|
||||||
mergeUp
|
filterSelection={
|
||||||
filter={appliedFilter}
|
<FilterSelection
|
||||||
onAddFilter={onAddFilter}
|
filters={eventOptions}
|
||||||
onUpdateFilter={onUpdateFilter}
|
onFilterClick={(newFilter) => {
|
||||||
onRemoveFilter={onRemoveFilter}
|
console.log('newFilter', newFilter);
|
||||||
onChangeEventsOrder={onChangeEventsOrder}
|
onAddFilter(newFilter);
|
||||||
saveRequestPayloads={saveRequestPayloads}
|
}}>
|
||||||
onFilterMove={onFilterMove}
|
<Button type="default" size="small">
|
||||||
/>
|
<div className="flex items-center gap-1">
|
||||||
|
<Plus size={16} strokeWidth={1} />
|
||||||
|
<span>Add Event</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</FilterSelection>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
</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 { 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 {
|
interface TopValue {
|
||||||
rowCount?: number;
|
rowCount?: number;
|
||||||
|
|
@ -11,17 +15,40 @@ interface TopValues {
|
||||||
[key: string]: TopValue[];
|
[key: string]: TopValue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProjectFilters {
|
||||||
|
[projectId: string]: Filter[];
|
||||||
|
}
|
||||||
|
|
||||||
export default class FilterStore {
|
export default class FilterStore {
|
||||||
topValues: TopValues = {};
|
topValues: TopValues = {};
|
||||||
|
filters: ProjectFilters = {};
|
||||||
|
commonFilters: Filter[] = [];
|
||||||
|
isLoadingFilters: boolean = true;
|
||||||
|
|
||||||
|
filterCache: Record<string, Filter[]> = {};
|
||||||
|
private pendingFetches: Record<string, Promise<Filter[]>> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
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[]) => {
|
setTopValues = (key: string, values: Record<string, any> | TopValue[]) => {
|
||||||
const vals = Array.isArray(values) ? values : values.data;
|
const vals = Array.isArray(values) ? values : values.data;
|
||||||
this.topValues[key] = vals?.filter(
|
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 = {};
|
this.topValues = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchTopValues = async (key: string, siteId: string, source?: string) => {
|
fetchTopValues = async (id: string, siteId: string, source?: string) => {
|
||||||
const valKey = `${siteId}_${key}${source || ''}`
|
const valKey = `${siteId}_${id}${source || ''}`;
|
||||||
|
|
||||||
if (this.topValues[valKey] && this.topValues[valKey].length) {
|
if (this.topValues[valKey] && this.topValues[valKey].length) {
|
||||||
return Promise.resolve(this.topValues[valKey]);
|
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);
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,18 +28,18 @@ export const checkValues = (key: any, value: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterMap = ({
|
export const filterMap = ({
|
||||||
category,
|
category,
|
||||||
value,
|
value,
|
||||||
key,
|
key,
|
||||||
operator,
|
operator,
|
||||||
sourceOperator,
|
sourceOperator,
|
||||||
source,
|
source,
|
||||||
custom,
|
custom,
|
||||||
isEvent,
|
isEvent,
|
||||||
filters,
|
filters,
|
||||||
sort,
|
sort,
|
||||||
order
|
order
|
||||||
}: any) => ({
|
}: any) => ({
|
||||||
value: checkValues(key, value),
|
value: checkValues(key, value),
|
||||||
custom,
|
custom,
|
||||||
type: category === FilterCategory.METADATA ? FilterKey.METADATA : key,
|
type: category === FilterCategory.METADATA ? FilterKey.METADATA : key,
|
||||||
|
|
@ -60,37 +60,22 @@ export const TAB_MAP: any = {
|
||||||
|
|
||||||
class SearchStore {
|
class SearchStore {
|
||||||
list: SavedSearch[] = [];
|
list: SavedSearch[] = [];
|
||||||
|
|
||||||
latestRequestTime: number | null = null;
|
latestRequestTime: number | null = null;
|
||||||
|
|
||||||
latestList = List();
|
latestList = List();
|
||||||
|
|
||||||
alertMetricId: number | null = null;
|
alertMetricId: number | null = null;
|
||||||
|
|
||||||
instance = new Search();
|
instance = new Search();
|
||||||
|
|
||||||
savedSearch: ISavedSearch = new SavedSearch();
|
savedSearch: ISavedSearch = new SavedSearch();
|
||||||
|
|
||||||
filterSearchList: any = {};
|
filterSearchList: any = {};
|
||||||
|
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
|
||||||
pageSize = PER_PAGE;
|
pageSize = PER_PAGE;
|
||||||
|
|
||||||
activeTab = { name: 'All', type: 'all' };
|
activeTab = { name: 'All', type: 'all' };
|
||||||
|
|
||||||
scrollY = 0;
|
scrollY = 0;
|
||||||
|
|
||||||
sessions = List();
|
sessions = List();
|
||||||
|
|
||||||
total: number = 0;
|
total: number = 0;
|
||||||
latestSessionCount: number = 0;
|
latestSessionCount: number = 0;
|
||||||
loadingFilterSearch = false;
|
loadingFilterSearch = false;
|
||||||
|
|
||||||
isSaving: boolean = false;
|
isSaving: boolean = false;
|
||||||
|
|
||||||
activeTags: any[] = [];
|
activeTags: any[] = [];
|
||||||
|
|
||||||
urlParsed: boolean = false;
|
urlParsed: boolean = false;
|
||||||
searchInProgress = false;
|
searchInProgress = false;
|
||||||
|
|
||||||
|
|
@ -146,7 +131,7 @@ class SearchStore {
|
||||||
|
|
||||||
editSavedSearch(instance: Partial<SavedSearch>) {
|
editSavedSearch(instance: Partial<SavedSearch>) {
|
||||||
this.savedSearch = new 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(
|
this.filterSearchList = response.reduce(
|
||||||
(
|
(
|
||||||
acc: Record<string, { projectId: number; value: string }[]>,
|
acc: Record<string, { projectId: number; value: string }[]>,
|
||||||
item: any,
|
item: any
|
||||||
) => {
|
) => {
|
||||||
const { projectId, type, value } = item;
|
const { projectId, type, value } = item;
|
||||||
if (!acc[type]) acc[type] = [];
|
if (!acc[type]) acc[type] = [];
|
||||||
acc[type].push({ projectId, value });
|
acc[type].push({ projectId, value });
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{},
|
{}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((error: any) => {
|
.catch((error: any) => {
|
||||||
|
|
@ -207,7 +192,7 @@ class SearchStore {
|
||||||
|
|
||||||
resetTags = () => {
|
resetTags = () => {
|
||||||
this.activeTags = ['all'];
|
this.activeTags = ['all'];
|
||||||
}
|
};
|
||||||
|
|
||||||
toggleTag(tag?: iTag) {
|
toggleTag(tag?: iTag) {
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
|
|
@ -302,6 +287,7 @@ class SearchStore {
|
||||||
(i: FilterItem) => i.key === filter.key
|
(i: FilterItem) => i.key === filter.key
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// new random key
|
||||||
filter.value = checkFilterValue(filter.value);
|
filter.value = checkFilterValue(filter.value);
|
||||||
filter.filters = filter.filters
|
filter.filters = filter.filters
|
||||||
? filter.filters.map((subFilter: any) => ({
|
? filter.filters.map((subFilter: any) => ({
|
||||||
|
|
@ -319,6 +305,7 @@ class SearchStore {
|
||||||
oldFilter.merge(updatedFilter);
|
oldFilter.merge(updatedFilter);
|
||||||
this.updateFilter(index, updatedFilter);
|
this.updateFilter(index, updatedFilter);
|
||||||
} else {
|
} else {
|
||||||
|
filter.key = Math.random().toString(36).substring(7);
|
||||||
this.instance.filters.push(filter);
|
this.instance.filters.push(filter);
|
||||||
this.instance = new Search({
|
this.instance = new Search({
|
||||||
...this.instance.toData()
|
...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(
|
addFilterByKeyAndValue(
|
||||||
key: any,
|
key: any,
|
||||||
value: any,
|
value: any,
|
||||||
operator?: string,
|
operator?: string,
|
||||||
sourceOperator?: string,
|
sourceOperator?: string,
|
||||||
source?: string,
|
source?: string
|
||||||
) {
|
) {
|
||||||
const defaultFilter = { ...filtersMap[key] };
|
const defaultFilter = { ...filtersMap[key] };
|
||||||
defaultFilter.value = value;
|
defaultFilter.value = value;
|
||||||
|
|
@ -353,20 +351,19 @@ class SearchStore {
|
||||||
this.addFilter(defaultFilter);
|
this.addFilter(defaultFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshFilterOptions() {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSearch = (search: Partial<Search>) => {
|
updateSearch = (search: Partial<Search>) => {
|
||||||
this.instance = Object.assign(this.instance, search);
|
this.instance = Object.assign(this.instance, search);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateFilter = (index: number, search: Partial<FilterItem>) => {
|
updateFilter = (key: string, search: Partial<FilterItem>) => {
|
||||||
const newFilters = this.instance.filters.map((_filter: any, i: any) => {
|
const newFilters = this.instance.filters.map((f: any) => {
|
||||||
if (i === index) {
|
if (f.key === key) {
|
||||||
return search;
|
return {
|
||||||
|
...f,
|
||||||
|
...search
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return _filter;
|
return f;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.instance = new Search({
|
this.instance = new Search({
|
||||||
|
|
@ -375,9 +372,9 @@ class SearchStore {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
removeFilter = (index: number) => {
|
removeFilter = (key: string) => {
|
||||||
const newFilters = this.instance.filters.filter(
|
const newFilters = this.instance.filters.filter(
|
||||||
(_filter: any, i: any) => i !== index,
|
(f: any) => f.key !== key
|
||||||
);
|
);
|
||||||
|
|
||||||
this.instance = new Search({
|
this.instance = new Search({
|
||||||
|
|
@ -390,13 +387,9 @@ class SearchStore {
|
||||||
this.scrollY = y;
|
this.scrollY = y;
|
||||||
};
|
};
|
||||||
|
|
||||||
async fetchAutoplaySessions(page: number): Promise<void> {
|
async fetchSessions(
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchSessions = async (
|
|
||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
bookmarked: boolean = false,
|
bookmarked: boolean = false
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (this.searchInProgress) return;
|
if (this.searchInProgress) return;
|
||||||
const filter = this.instance.toSearch();
|
const filter = this.instance.toSearch();
|
||||||
|
|
|
||||||
|
|
@ -61,33 +61,22 @@ export default class Filter implements IFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
filterId: string = '';
|
filterId: string = '';
|
||||||
|
|
||||||
name: string = '';
|
name: string = '';
|
||||||
|
|
||||||
autoOpen = false;
|
autoOpen = false;
|
||||||
|
|
||||||
filters: FilterItem[] = [];
|
filters: FilterItem[] = [];
|
||||||
|
|
||||||
excludes: FilterItem[] = [];
|
excludes: FilterItem[] = [];
|
||||||
|
|
||||||
eventsOrder: string = 'then';
|
eventsOrder: string = 'then';
|
||||||
|
|
||||||
eventsOrderSupport: string[] = ['then', 'or', 'and'];
|
eventsOrderSupport: string[] = ['then', 'or', 'and'];
|
||||||
|
|
||||||
startTimestamp: number = 0;
|
startTimestamp: number = 0;
|
||||||
|
|
||||||
endTimestamp: number = 0;
|
endTimestamp: number = 0;
|
||||||
|
|
||||||
eventsHeader: string = 'EVENTS';
|
eventsHeader: string = 'EVENTS';
|
||||||
|
|
||||||
page: number = 1;
|
page: number = 1;
|
||||||
|
|
||||||
limit: number = 10;
|
limit: number = 10;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
filters: any[] = [],
|
filters: any[] = [],
|
||||||
private readonly isConditional = false,
|
private readonly isConditional = false,
|
||||||
private readonly isMobile = false,
|
private readonly isMobile = false
|
||||||
) {
|
) {
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this, {
|
||||||
filters: observable,
|
filters: observable,
|
||||||
|
|
@ -101,7 +90,7 @@ export default class Filter implements IFilter {
|
||||||
merge: action,
|
merge: action,
|
||||||
addExcludeFilter: action,
|
addExcludeFilter: action,
|
||||||
updateFilter: action,
|
updateFilter: action,
|
||||||
replaceFilters: action,
|
replaceFilters: action
|
||||||
});
|
});
|
||||||
this.filters = filters.map((i) => new FilterItem(i));
|
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(
|
new FilterItem(undefined, this.isConditional, this.isMobile).fromJson(
|
||||||
i,
|
i,
|
||||||
undefined,
|
undefined,
|
||||||
isHeatmap,
|
isHeatmap
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
this.eventsOrder = json.eventsOrder;
|
this.eventsOrder = json.eventsOrder;
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -156,7 +145,7 @@ export default class Filter implements IFilter {
|
||||||
fromData(data: any) {
|
fromData(data: any) {
|
||||||
this.name = data.name;
|
this.name = data.name;
|
||||||
this.filters = data.filters.map((i: Record<string, any>) =>
|
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;
|
this.eventsOrder = data.eventsOrder;
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -168,7 +157,7 @@ export default class Filter implements IFilter {
|
||||||
filters: this.filters.map((i) => i.toJson()),
|
filters: this.filters.map((i) => i.toJson()),
|
||||||
eventsOrder: this.eventsOrder,
|
eventsOrder: this.eventsOrder,
|
||||||
startTimestamp: this.startTimestamp,
|
startTimestamp: this.startTimestamp,
|
||||||
endTimestamp: this.endTimestamp,
|
endTimestamp: this.endTimestamp
|
||||||
};
|
};
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +171,7 @@ export default class Filter implements IFilter {
|
||||||
const json = {
|
const json = {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
filters: this.filters.map((i) => i.toJson()),
|
filters: this.filters.map((i) => i.toJson()),
|
||||||
eventsOrder: this.eventsOrder,
|
eventsOrder: this.eventsOrder
|
||||||
};
|
};
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
@ -204,12 +193,12 @@ export default class Filter implements IFilter {
|
||||||
this.addFilter({
|
this.addFilter({
|
||||||
...filtersMap[FilterKey.LOCATION],
|
...filtersMap[FilterKey.LOCATION],
|
||||||
value: [''],
|
value: [''],
|
||||||
operator: 'isAny',
|
operator: 'isAny'
|
||||||
});
|
});
|
||||||
this.addFilter({
|
this.addFilter({
|
||||||
...filtersMap[FilterKey.CLICK],
|
...filtersMap[FilterKey.CLICK],
|
||||||
value: [''],
|
value: [''],
|
||||||
operator: 'onAny',
|
operator: 'onAny'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,7 +206,7 @@ export default class Filter implements IFilter {
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
filters: this.filters.map((i) => i.toJson()),
|
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,
|
value: any,
|
||||||
operator: undefined,
|
operator: undefined,
|
||||||
sourceOperator: undefined,
|
sourceOperator: undefined,
|
||||||
source: undefined,
|
source: undefined
|
||||||
) {
|
) {
|
||||||
let defaultFilter = { ...filtersMap[key] };
|
let defaultFilter = { ...filtersMap[key] };
|
||||||
if (defaultFilter) {
|
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 {
|
import {
|
||||||
conditionalFiltersMap,
|
conditionalFiltersMap,
|
||||||
filtersMap,
|
filtersMap,
|
||||||
mobileConditionalFiltersMap,
|
mobileConditionalFiltersMap
|
||||||
} from 'Types/filter/newFilter';
|
} from 'Types/filter/newFilter';
|
||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
|
||||||
import { pageUrlOperators } from '../../constants/filterOptions';
|
import { pageUrlOperators } from '@/constants/filterOptions';
|
||||||
|
|
||||||
export default class FilterItem {
|
export default class FilterItem {
|
||||||
type: string = '';
|
type: string = '';
|
||||||
|
|
||||||
category: FilterCategory = FilterCategory.METADATA;
|
category: FilterCategory = FilterCategory.METADATA;
|
||||||
|
|
||||||
subCategory: string = '';
|
subCategory: string = '';
|
||||||
|
|
||||||
key: string = '';
|
key: string = '';
|
||||||
|
|
||||||
label: string = '';
|
label: string = '';
|
||||||
|
|
||||||
value: any = [''];
|
value: any = [''];
|
||||||
|
|
||||||
isEvent: boolean = false;
|
isEvent: boolean = false;
|
||||||
|
|
||||||
operator: string = '';
|
operator: string = '';
|
||||||
|
|
||||||
hasSource: boolean = false;
|
hasSource: boolean = false;
|
||||||
|
|
||||||
source: string = '';
|
source: string = '';
|
||||||
|
|
||||||
sourceOperator: string = '';
|
sourceOperator: string = '';
|
||||||
|
|
||||||
sourceOperatorOptions: any = [];
|
sourceOperatorOptions: any = [];
|
||||||
|
|
||||||
filters: FilterItem[] = [];
|
filters: FilterItem[] = [];
|
||||||
|
|
||||||
operatorOptions: any[] = [];
|
operatorOptions: any[] = [];
|
||||||
|
|
||||||
options: any[] = [];
|
options: any[] = [];
|
||||||
|
|
||||||
isActive: boolean = true;
|
isActive: boolean = true;
|
||||||
|
|
||||||
completed: number = 0;
|
completed: number = 0;
|
||||||
|
|
||||||
dropped: number = 0;
|
dropped: number = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
data: any = {},
|
data: any = {},
|
||||||
private readonly isConditional?: boolean,
|
private readonly isConditional?: boolean,
|
||||||
private readonly isMobile?: boolean,
|
private readonly isMobile?: boolean
|
||||||
) {
|
) {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
|
||||||
if (Array.isArray(data.filters)) {
|
if (Array.isArray(data.filters)) {
|
||||||
data.filters = data.filters.map(
|
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,
|
sourceOperator: this.sourceOperator,
|
||||||
filters: Array.isArray(this.filters)
|
filters: Array.isArray(this.filters)
|
||||||
? this.filters.map((i) => i.toJson())
|
? this.filters.map((i) => i.toJson())
|
||||||
: [],
|
: []
|
||||||
};
|
};
|
||||||
if (this.type === FilterKey.DURATION) {
|
if (this.type === FilterKey.DURATION) {
|
||||||
json.value = this.value.map((i: any) => (!i ? 0 : i));
|
json.value = this.value.map((i: any) => (!i ? 0 : i));
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
CUSTOM_RANGE,
|
CUSTOM_RANGE,
|
||||||
DATE_RANGE_VALUES,
|
DATE_RANGE_VALUES,
|
||||||
getDateRangeFromValue,
|
getDateRangeFromValue
|
||||||
} from 'App/dateRange';
|
} from 'App/dateRange';
|
||||||
import Filter, { IFilter } from 'App/mstore/types/filter';
|
import Filter, { IFilter } from 'App/mstore/types/filter';
|
||||||
import FilterItem from 'App/mstore/types/filterItem';
|
import FilterItem from 'App/mstore/types/filterItem';
|
||||||
|
|
@ -25,7 +25,7 @@ interface ISearch {
|
||||||
userDevice?: string;
|
userDevice?: string;
|
||||||
fid0?: string;
|
fid0?: string;
|
||||||
events: Event[];
|
events: Event[];
|
||||||
filters: IFilter[];
|
filters: FilterItem[];
|
||||||
minDuration?: number;
|
minDuration?: number;
|
||||||
maxDuration?: number;
|
maxDuration?: number;
|
||||||
custom: Record<string, any>;
|
custom: Record<string, any>;
|
||||||
|
|
@ -46,62 +46,36 @@ interface ISearch {
|
||||||
|
|
||||||
export default class Search {
|
export default class Search {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
searchId?: number;
|
searchId?: number;
|
||||||
|
|
||||||
referrer?: string;
|
referrer?: string;
|
||||||
|
|
||||||
userBrowser?: string;
|
userBrowser?: string;
|
||||||
|
|
||||||
userOs?: string;
|
userOs?: string;
|
||||||
|
|
||||||
userCountry?: string;
|
userCountry?: string;
|
||||||
|
|
||||||
userDevice?: string;
|
userDevice?: string;
|
||||||
|
|
||||||
fid0?: string;
|
fid0?: string;
|
||||||
|
|
||||||
events: Event[];
|
events: Event[];
|
||||||
|
|
||||||
filters: FilterItem[];
|
filters: FilterItem[];
|
||||||
|
|
||||||
minDuration?: number;
|
minDuration?: number;
|
||||||
|
|
||||||
maxDuration?: number;
|
maxDuration?: number;
|
||||||
|
|
||||||
custom: Record<string, any>;
|
custom: Record<string, any>;
|
||||||
|
|
||||||
rangeValue: string;
|
rangeValue: string;
|
||||||
|
|
||||||
startDate: number;
|
startDate: number;
|
||||||
|
|
||||||
endDate: number;
|
endDate: number;
|
||||||
|
|
||||||
groupByUser: boolean;
|
groupByUser: boolean;
|
||||||
|
|
||||||
sort: string;
|
sort: string;
|
||||||
|
|
||||||
order: string;
|
order: string;
|
||||||
|
|
||||||
viewed?: boolean;
|
viewed?: boolean;
|
||||||
|
|
||||||
consoleLogCount?: number;
|
consoleLogCount?: number;
|
||||||
|
|
||||||
eventsCount?: number;
|
eventsCount?: number;
|
||||||
|
|
||||||
suspicious?: boolean;
|
suspicious?: boolean;
|
||||||
|
|
||||||
consoleLevel?: string;
|
consoleLevel?: string;
|
||||||
|
|
||||||
strict: boolean;
|
strict: boolean;
|
||||||
|
|
||||||
eventsOrder: string;
|
eventsOrder: string;
|
||||||
|
|
||||||
limit: number;
|
limit: number;
|
||||||
|
|
||||||
constructor(initialData?: Partial<ISearch>) {
|
constructor(initialData?: Partial<ISearch>) {
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this, {
|
||||||
filters: observable,
|
filters: observable
|
||||||
});
|
});
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
name: '',
|
name: '',
|
||||||
|
|
@ -131,7 +105,7 @@ export default class Search {
|
||||||
strict: false,
|
strict: false,
|
||||||
eventsOrder: 'then',
|
eventsOrder: 'then',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
...initialData,
|
...initialData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,7 +145,7 @@ export default class Search {
|
||||||
toSearch() {
|
toSearch() {
|
||||||
const js: any = { ...this };
|
const js: any = { ...this };
|
||||||
js.filters = this.filters.map((filter: any) =>
|
js.filters = this.filters.map((filter: any) =>
|
||||||
new FilterItem(filter).toJson(),
|
new FilterItem(filter).toJson()
|
||||||
);
|
);
|
||||||
|
|
||||||
const { startDate, endDate } = this.getDateRange(
|
const { startDate, endDate } = this.getDateRange(
|
||||||
|
|
@ -191,7 +165,7 @@ export default class Search {
|
||||||
private getDateRange(
|
private getDateRange(
|
||||||
rangeName: string,
|
rangeName: string,
|
||||||
customStartDate: number,
|
customStartDate: number,
|
||||||
customEndDate: number,
|
customEndDate: number
|
||||||
roundMinutes?: number,
|
roundMinutes?: number,
|
||||||
): { startDate: number; endDate: number } {
|
): { startDate: number; endDate: number } {
|
||||||
let endDate = new Date().getTime();
|
let endDate = new Date().getTime();
|
||||||
|
|
@ -207,7 +181,9 @@ export default class Search {
|
||||||
break;
|
break;
|
||||||
case CUSTOM_RANGE:
|
case CUSTOM_RANGE:
|
||||||
if (!customStartDate || !customEndDate) {
|
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;
|
startDate = customStartDate;
|
||||||
endDate = customEndDate;
|
endDate = customEndDate;
|
||||||
|
|
@ -244,16 +220,17 @@ export default class Search {
|
||||||
eventsOrder,
|
eventsOrder,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
|
filters,
|
||||||
// events: events.map((event: any) => new Event(event)),
|
// events: events.map((event: any) => new Event(event)),
|
||||||
filters: filters.map((i: any) => {
|
// filters: filters.map((i: any) => {
|
||||||
const filter = new Filter(i).toData();
|
// const filter = new Filter(i).toData();
|
||||||
if (Array.isArray(i.filters)) {
|
// if (Array.isArray(i.filters)) {
|
||||||
filter.filters = i.filters.map((f: any) =>
|
// filter.filters = i.filters.map((f: any) =>
|
||||||
new Filter({ ...f, subFilter: i.type }).toData(),
|
// new Filter({ ...f, subFilter: i.type }).toData()
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
return filter;
|
// return filter;
|
||||||
}),
|
// })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,27 @@ export default class FilterService {
|
||||||
this.client = client || new APIClient();
|
this.client = client || new APIClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTopValues = async (key: string, source?: string) => {
|
fetchTopValues = async (name: string, source?: string) => {
|
||||||
let path = `/PROJECT_ID/events/search?type=${key}`;
|
// 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) {
|
if (source) {
|
||||||
path += `&source=${source}`;
|
path += `&source=${source}`;
|
||||||
}
|
}
|
||||||
const response = await this.client.get(path);
|
const response = await this.client.get(path);
|
||||||
return await response.json();
|
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 {
|
export enum FilterType {
|
||||||
STRING = 'STRING',
|
STRING = 'string',
|
||||||
ISSUE = 'ISSUE',
|
ISSUE = 'ISSUE',
|
||||||
BOOLEAN = 'BOOLEAN',
|
BOOLEAN = 'BOOLEAN',
|
||||||
NUMBER = 'NUMBER',
|
NUMBER = 'NUMBER',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue