ui: finish with omnisearch thing
This commit is contained in:
parent
9909511e94
commit
614c0655c2
30 changed files with 726 additions and 912 deletions
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import SessionSearchField from 'Shared/SessionSearchField';
|
||||
import { MODULES } from 'Components/Client/Modules';
|
||||
|
||||
import AssistStats from '../../AssistStats';
|
||||
|
|
@ -9,7 +8,7 @@ import Recordings from '../RecordingsList/Recordings';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
function AssistSearchField() {
|
||||
function AssistSearchActions() {
|
||||
const { searchStoreLive, userStore } = useStore();
|
||||
const modules = userStore.account.settings?.modules ?? [];
|
||||
const isEnterprise = userStore.isEnterprise
|
||||
|
|
@ -27,9 +26,6 @@ function AssistSearchField() {
|
|||
};
|
||||
return (
|
||||
<div className="flex items-center w-full gap-2">
|
||||
<div style={{ width: '60%' }}>
|
||||
<SessionSearchField />
|
||||
</div>
|
||||
{isEnterprise && modules.includes(MODULES.OFFLINE_RECORDINGS)
|
||||
? <Button type="primary" ghost onClick={showRecords}>Training Videos</Button> : null
|
||||
}
|
||||
|
|
@ -50,4 +46,4 @@ function AssistSearchField() {
|
|||
);
|
||||
}
|
||||
|
||||
export default observer(AssistSearchField);
|
||||
export default observer(AssistSearchActions);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './AssistSearchActions'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './AssistSearchField'
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import LiveSessionList from 'Shared/LiveSessionList';
|
||||
import LiveSessionSearch from 'Shared/LiveSessionSearch';
|
||||
import AssistSearchField from './AssistSearchField';
|
||||
import AssistSearchActions from './AssistSearchActions';
|
||||
|
||||
function AssistView() {
|
||||
return (
|
||||
<div className="w-full mx-auto" style={{ maxWidth: '1360px'}}>
|
||||
<AssistSearchActions />
|
||||
<LiveSessionSearch />
|
||||
<div className="my-4" />
|
||||
<LiveSessionList />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite';
|
|||
import React from 'react';
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
import { gradientBox } from 'App/components/shared/SessionSearchField/AiSessionSearchField';
|
||||
import { gradientBox } from 'App/components/shared/SessionFilters/AiSessionSearchField';
|
||||
import aiSpinner from 'App/lottie/aiSpinner.json';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Icon, Input } from 'UI';
|
||||
|
|
|
|||
|
|
@ -2,181 +2,219 @@ import React, { useEffect, useState } from 'react';
|
|||
import FilterList from 'Shared/Filters/FilterList';
|
||||
import SeriesName from './SeriesName';
|
||||
import cn from 'classnames';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import ExcludeFilters from './ExcludeFilters';
|
||||
import AddStepButton from "Components/Dashboard/components/FilterSeries/AddStepButton";
|
||||
import {Button, Space} from "antd";
|
||||
import {ChevronDown, ChevronUp, Trash} from "lucide-react";
|
||||
import { Button, Space } from 'antd';
|
||||
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
|
||||
|
||||
|
||||
const FilterCountLabels = observer((props: { filters: any, toggleExpand: any }) => {
|
||||
const FilterCountLabels = observer(
|
||||
(props: { filters: any; toggleExpand: any }) => {
|
||||
const events = props.filters.filter((i: any) => i && i.isEvent).length;
|
||||
const filters = props.filters.filter((i: any) => i && !i.isEvent).length;
|
||||
return <div className="flex items-center">
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Space>
|
||||
{events > 0 && (
|
||||
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
|
||||
{`${events} Event${events > 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
)}
|
||||
{events > 0 && (
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
onClick={props.toggleExpand}
|
||||
>
|
||||
{`${events} Event${events > 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{filters > 0 && (
|
||||
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
|
||||
{`${filters} Filter${filters > 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
)}
|
||||
{filters > 0 && (
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
size="small"
|
||||
onClick={props.toggleExpand}
|
||||
>
|
||||
{`${filters} Filter${filters > 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>;
|
||||
});
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const FilterSeriesHeader = observer((props: {
|
||||
expanded: boolean,
|
||||
hidden: boolean,
|
||||
seriesIndex: number,
|
||||
series: any,
|
||||
onRemove: (seriesIndex: any) => void,
|
||||
canDelete: boolean | undefined,
|
||||
toggleExpand: () => void
|
||||
}) => {
|
||||
|
||||
const onUpdate = (name: any) => {
|
||||
props.series.update('name', name)
|
||||
}
|
||||
return <div className={cn("border-b px-5 h-12 flex items-center relative", {hidden: props.hidden})}>
|
||||
<Space className="mr-auto" size={30}>
|
||||
<SeriesName
|
||||
seriesIndex={props.seriesIndex}
|
||||
name={props.series.name}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
{!props.expanded &&
|
||||
<FilterCountLabels filters={props.series.filter.filters} toggleExpand={props.toggleExpand}/>}
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button onClick={props.onRemove}
|
||||
size="small"
|
||||
disabled={!props.canDelete}
|
||||
icon={<Trash size={14}/>}/>
|
||||
<Button onClick={props.toggleExpand}
|
||||
size="small"
|
||||
icon={props.expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
|
||||
</Space>
|
||||
</div>;
|
||||
})
|
||||
|
||||
interface Props {
|
||||
const FilterSeriesHeader = observer(
|
||||
(props: {
|
||||
expanded: boolean;
|
||||
hidden: boolean;
|
||||
seriesIndex: number;
|
||||
series: any;
|
||||
onRemoveSeries: (seriesIndex: any) => void;
|
||||
canDelete?: boolean;
|
||||
supportsEmpty?: boolean;
|
||||
hideHeader?: boolean;
|
||||
emptyMessage?: any;
|
||||
observeChanges?: () => void;
|
||||
excludeFilterKeys?: Array<string>;
|
||||
canExclude?: boolean;
|
||||
expandable?: boolean;
|
||||
onRemove: (seriesIndex: any) => void;
|
||||
canDelete: boolean | undefined;
|
||||
toggleExpand: () => void;
|
||||
}) => {
|
||||
const onUpdate = (name: any) => {
|
||||
props.series.update('name', name);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={cn('px-4 h-12 flex items-center relative', {
|
||||
hidden: props.hidden,
|
||||
})}
|
||||
>
|
||||
<Space className="mr-auto" size={30}>
|
||||
<SeriesName
|
||||
seriesIndex={props.seriesIndex}
|
||||
name={props.series.name}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
{!props.expanded && (
|
||||
<FilterCountLabels
|
||||
filters={props.series.filter.filters}
|
||||
toggleExpand={props.toggleExpand}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
onClick={props.onRemove}
|
||||
size="small"
|
||||
disabled={!props.canDelete}
|
||||
icon={<Trash size={14} />}
|
||||
/>
|
||||
<Button
|
||||
onClick={props.toggleExpand}
|
||||
size="small"
|
||||
icon={
|
||||
props.expanded ? (
|
||||
<ChevronUp size={16} />
|
||||
) : (
|
||||
<ChevronDown size={16} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface Props {
|
||||
seriesIndex: number;
|
||||
series: any;
|
||||
onRemoveSeries: (seriesIndex: any) => void;
|
||||
canDelete?: boolean;
|
||||
supportsEmpty?: boolean;
|
||||
hideHeader?: boolean;
|
||||
emptyMessage?: any;
|
||||
observeChanges?: () => void;
|
||||
excludeFilterKeys?: Array<string>;
|
||||
canExclude?: boolean;
|
||||
expandable?: boolean;
|
||||
}
|
||||
|
||||
function FilterSeries(props: Props) {
|
||||
const {
|
||||
observeChanges = () => {
|
||||
},
|
||||
canDelete,
|
||||
hideHeader = false,
|
||||
emptyMessage = 'Add an event or filter step to define the series.',
|
||||
supportsEmpty = true,
|
||||
excludeFilterKeys = [],
|
||||
canExclude = false,
|
||||
expandable = false
|
||||
} = props;
|
||||
const [expanded, setExpanded] = useState(!expandable);
|
||||
const {series, seriesIndex} = props;
|
||||
const [prevLength, setPrevLength] = useState(0);
|
||||
const {
|
||||
observeChanges = () => {},
|
||||
canDelete,
|
||||
hideHeader = false,
|
||||
emptyMessage = 'Add an event or filter step to define the series.',
|
||||
supportsEmpty = true,
|
||||
excludeFilterKeys = [],
|
||||
canExclude = false,
|
||||
expandable = false,
|
||||
} = props;
|
||||
const [expanded, setExpanded] = useState(!expandable);
|
||||
const { series, seriesIndex } = props;
|
||||
const [prevLength, setPrevLength] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (series.filter.filters.length === 1 && prevLength === 0 && seriesIndex === 0) {
|
||||
setExpanded(true);
|
||||
}
|
||||
setPrevLength(series.filter.filters.length);
|
||||
}, [series.filter.filters.length]);
|
||||
|
||||
const onUpdateFilter = (filterIndex: any, filter: any) => {
|
||||
series.filter.updateFilter(filterIndex, filter);
|
||||
observeChanges();
|
||||
};
|
||||
|
||||
const onFilterMove = (newFilters: any) => {
|
||||
series.filter.replaceFilters(newFilters.toArray())
|
||||
observeChanges();
|
||||
useEffect(() => {
|
||||
if (
|
||||
series.filter.filters.length === 1 &&
|
||||
prevLength === 0 &&
|
||||
seriesIndex === 0
|
||||
) {
|
||||
setExpanded(true);
|
||||
}
|
||||
setPrevLength(series.filter.filters.length);
|
||||
}, [series.filter.filters.length]);
|
||||
|
||||
const onChangeEventsOrder = (_: any, {name, value}: any) => {
|
||||
series.filter.updateKey(name, value);
|
||||
observeChanges();
|
||||
};
|
||||
const onUpdateFilter = (filterIndex: any, filter: any) => {
|
||||
series.filter.updateFilter(filterIndex, filter);
|
||||
observeChanges();
|
||||
};
|
||||
|
||||
const onRemoveFilter = (filterIndex: any) => {
|
||||
series.filter.removeFilter(filterIndex);
|
||||
observeChanges();
|
||||
};
|
||||
const onFilterMove = (newFilters: any) => {
|
||||
series.filter.replaceFilters(newFilters.toArray());
|
||||
observeChanges();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg shadow-sm bg-white">
|
||||
{canExclude && <ExcludeFilters filter={series.filter}/>}
|
||||
const onChangeEventsOrder = (_: any, { name, value }: any) => {
|
||||
series.filter.updateKey(name, value);
|
||||
observeChanges();
|
||||
};
|
||||
|
||||
{!hideHeader && (
|
||||
<FilterSeriesHeader hidden={hideHeader}
|
||||
seriesIndex={seriesIndex}
|
||||
series={series}
|
||||
onRemove={props.onRemoveSeries}
|
||||
canDelete={canDelete}
|
||||
expanded={expanded}
|
||||
toggleExpand={() => setExpanded(!expanded)}/>
|
||||
const onRemoveFilter = (filterIndex: any) => {
|
||||
series.filter.removeFilter(filterIndex);
|
||||
observeChanges();
|
||||
};
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
series.filter.addFilter(filter);
|
||||
observeChanges();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{canExclude && <ExcludeFilters filter={series.filter} />}
|
||||
|
||||
{!hideHeader && (
|
||||
<FilterSeriesHeader
|
||||
hidden={hideHeader}
|
||||
seriesIndex={seriesIndex}
|
||||
series={series}
|
||||
onRemove={props.onRemoveSeries}
|
||||
canDelete={canDelete}
|
||||
expanded={expanded}
|
||||
toggleExpand={() => setExpanded(!expanded)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{expandable && (
|
||||
<Space
|
||||
className="justify-between w-full px-5 py-2 cursor-pointer"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<div>
|
||||
{!expanded && (
|
||||
<FilterCountLabels
|
||||
filters={series.filter.filters}
|
||||
toggleExpand={() => setExpanded(!expanded)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
icon={
|
||||
expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{expandable && (
|
||||
<Space className="justify-between w-full px-5 py-2 cursor-pointer" onClick={() => setExpanded(!expanded)}>
|
||||
<div>{!expanded && <FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>}</div>
|
||||
<Button size="small"
|
||||
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{expanded && (
|
||||
<>
|
||||
<div className="p-5">
|
||||
{series.filter.filters.length > 0 ? (
|
||||
<FilterList
|
||||
filter={series.filter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
supportsEmpty={supportsEmpty}
|
||||
onFilterMove={onFilterMove}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
// actions={[
|
||||
// expandable && (
|
||||
// <Button onClick={() => setExpanded(!expanded)}
|
||||
// size="small"
|
||||
// icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
|
||||
// )
|
||||
// ]}
|
||||
/>
|
||||
) : (
|
||||
<div className="color-gray-medium">{emptyMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="border-t h-12 flex items-center">
|
||||
<div className="-mx-4 px-5">
|
||||
<AddStepButton excludeFilterKeys={excludeFilterKeys} series={series}/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{expanded ? (
|
||||
<FilterList
|
||||
filter={series.filter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
supportsEmpty={supportsEmpty}
|
||||
onFilterMove={onFilterMove}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(FilterSeries);
|
||||
|
|
|
|||
|
|
@ -44,17 +44,6 @@ function WidgetFormNew() {
|
|||
|
||||
export default observer(WidgetFormNew);
|
||||
|
||||
|
||||
function DefineSteps({ metric, excludeFilterKeys }: any) {
|
||||
return (
|
||||
<div className="px-4 py-2 bg-white rounded-lg shadow-sm flex items-center">
|
||||
<Typography.Text strong>Filter</Typography.Text>
|
||||
<AddStepButton excludeFilterKeys={excludeFilterKeys} series={metric.series[0]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const FilterSection = observer(({ metric, excludeFilterKeys }: any) => {
|
||||
// const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries');
|
||||
// const tableOptions = metricOf.filter((i) => i.type === 'table');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
|||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import NoSessionsMessage from 'Shared/NoSessionsMessage';
|
||||
import MainSearchBar from 'Shared/MainSearchBar';
|
||||
import SessionSearch from 'Shared/SessionSearch';
|
||||
import SearchActions from 'Shared/SearchActions';
|
||||
import SessionsTabOverview from 'Shared/SessionsTabOverview/SessionsTabOverview';
|
||||
import FFlagsList from 'Components/FFlags';
|
||||
import NewFFlag from 'Components/FFlags/NewFFlag';
|
||||
|
|
@ -32,15 +32,14 @@ function Overview({ match: { params } }: IProps) {
|
|||
React.useEffect(() => {
|
||||
searchStore.setActiveTab(tab);
|
||||
}, [tab]);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact strict
|
||||
path={[withSiteId(sessions(), siteId), withSiteId(notes(), siteId), withSiteId(bookmarks(), siteId)]}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<NoSessionsMessage siteId={siteId} />
|
||||
<SearchActions />
|
||||
<MainSearchBar />
|
||||
<SessionSearch />
|
||||
<div className="my-4" />
|
||||
<SessionsTabOverview />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { KEYS } from 'Types/filter/customFilter';
|
|||
import { capitalize } from 'App/utils';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import AssistSearchField from 'App/components/Assist/AssistSearchField';
|
||||
import AssistSearchField from 'App/components/Assist/AssistSearchActions';
|
||||
import LiveSessionSearch from 'Shared/LiveSessionSearch';
|
||||
import cn from 'classnames';
|
||||
import Session from 'App/mstore/types/session';
|
||||
|
|
@ -70,7 +70,7 @@ function AssistSessionsModal(props: ConnectProps) {
|
|||
icon="arrow-repeat"
|
||||
/>
|
||||
</Tooltip>
|
||||
<AssistSearchField />
|
||||
<AssistSearchActions />
|
||||
</div>
|
||||
<div className="flex self-end items-center gap-2" w-full>
|
||||
<span className="color-gray-medium">Sort By</span>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { Button, Checkbox, Input } from 'antd';
|
||||
import cn from 'classnames';
|
||||
|
||||
export function AutocompleteModal({
|
||||
onClose,
|
||||
onApply,
|
||||
values,
|
||||
handleFocus,
|
||||
loadOptions,
|
||||
options,
|
||||
isLoading,
|
||||
}: {
|
||||
values: string[];
|
||||
onClose: () => void;
|
||||
onApply: (values: string[]) => void;
|
||||
handleFocus: () => void;
|
||||
loadOptions: (query: string) => void;
|
||||
options: { value: string; label: string }[];
|
||||
placeholder?: string;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [selectedValues, setSelectedValues] = React.useState<string[]>(
|
||||
values.filter((i) => i.length > 0)
|
||||
);
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
setQuery(value);
|
||||
loadOptions(value);
|
||||
};
|
||||
const onSelectOption = (item: { value: string; label: string }) => {
|
||||
const selected = isSelected(item);
|
||||
if (!selected) {
|
||||
setSelectedValues([...selectedValues, item.value]);
|
||||
} else {
|
||||
setSelectedValues(selectedValues.filter((i) => i !== item.value));
|
||||
}
|
||||
};
|
||||
const isSelected = (item: { value: string; label: string }) => {
|
||||
return selectedValues.includes(item.value);
|
||||
};
|
||||
|
||||
const applyValues = () => {
|
||||
onApply(selectedValues);
|
||||
};
|
||||
|
||||
const sortedOptions = React.useMemo(() => {
|
||||
if (values[0] && values[0].length) {
|
||||
const sorted = options.sort((a, b) => {
|
||||
return values.includes(a.value) ? -1 : 1;
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
return options;
|
||||
}, [options.length]);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-0 mt-2 p-4 bg-white rounded-xl shadow border-gray-light z-10'
|
||||
)}
|
||||
style={{ minWidth: 320, minHeight: 100, top: '100%' }}
|
||||
>
|
||||
<Input.Search
|
||||
value={query}
|
||||
onFocus={handleFocus}
|
||||
loading={isLoading}
|
||||
onChange={(e) => handleInputChange(e.target.value)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={'flex flex-col gap-2 overflow-y-auto py-2'}
|
||||
style={{ maxHeight: 200 }}
|
||||
>
|
||||
{sortedOptions.map((item) => (
|
||||
<div
|
||||
onClick={() => onSelectOption(item)}
|
||||
className={
|
||||
'cursor-pointer w-full py-1 hover:bg-active-blue rounded px-2'
|
||||
}
|
||||
>
|
||||
<Checkbox checked={isSelected(item)} /> {item.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{query.length ? (
|
||||
<div className={'border-y border-y-gray-light py-2'}>
|
||||
<div
|
||||
className={
|
||||
'rounded cursor-pointer text-blue hover:bg-active-blue px-2 py-1'
|
||||
}
|
||||
onClick={() => onApply([query])}
|
||||
>
|
||||
Apply "{query}"
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={'flex gap-2 items-center pt-2'}>
|
||||
<Button type={'primary'} onClick={applyValues}>
|
||||
Apply
|
||||
</Button>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value: string[];
|
||||
params?: any;
|
||||
onApplyValues: (values: string[]) => void;
|
||||
modalRenderer: (props: any) => React.ReactElement;
|
||||
placeholder?: string;
|
||||
modalProps?: any;
|
||||
mapValues?: (value: string) => string;
|
||||
}
|
||||
|
||||
export function AutoCompleteContainer(props: Props) {
|
||||
const filterValueContainer = useRef<HTMLDivElement>(null);
|
||||
const [showValueModal, setShowValueModal] = useState(false);
|
||||
const isEmpty = props.value.length === 0 || !props.value[0].length;
|
||||
const onClose = () => setShowValueModal(false);
|
||||
const onApply = (values: string[]) => {
|
||||
props.onApplyValues(values);
|
||||
setShowValueModal(false);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'rounded border border-gray-light px-2 relative w-fit whitespace-nowrap flex items-center'
|
||||
}
|
||||
style={{ height: 26 }}
|
||||
ref={filterValueContainer}
|
||||
>
|
||||
<div
|
||||
onClick={() => setShowValueModal(true)}
|
||||
className={'flex items-center gap-2 cursor-pointer'}
|
||||
>
|
||||
{!isEmpty ? (
|
||||
<>
|
||||
<div
|
||||
className={'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'}
|
||||
>
|
||||
{props.mapValues
|
||||
? props.mapValues(props.value[0])
|
||||
: props.value[0]}
|
||||
</div>
|
||||
{props.value.length > 1 ? (
|
||||
<div
|
||||
className={
|
||||
'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'
|
||||
}
|
||||
>
|
||||
+ {props.value.length - 1} More
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className={'text-disabled-text'}>
|
||||
{props.placeholder ? props.placeholder : 'Select values'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showValueModal ? (
|
||||
<props.modalRenderer
|
||||
{...props.modalProps}
|
||||
params={props.params}
|
||||
onClose={onClose}
|
||||
onApply={onApply}
|
||||
values={props.value}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,117 +1,25 @@
|
|||
import React, { useState, useEffect, useCallback, useRef, ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import APIClient from 'App/api_client';
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { debounce } from 'App/utils';
|
||||
import stl from './FilterAutoComplete.module.css';
|
||||
import colors from 'App/theme/colors';
|
||||
import Select from 'react-select';
|
||||
import cn from 'classnames';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { searchService} from 'App/services';
|
||||
|
||||
const dropdownStyles = {
|
||||
option: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
minWidth: 150,
|
||||
transition: 'all 0.3s',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
backgroundColor: state.isFocused ? colors['active-blue'] : 'transparent',
|
||||
color: state.isFocused ? colors.teal : 'black',
|
||||
fontSize: '14px',
|
||||
'&:hover': {
|
||||
transition: 'all 0.2s',
|
||||
backgroundColor: colors['active-blue']
|
||||
},
|
||||
'&:focus': {
|
||||
transition: 'all 0.2s',
|
||||
backgroundColor: colors['active-blue']
|
||||
}
|
||||
}),
|
||||
control: (provided: any) => {
|
||||
const obj = {
|
||||
...provided,
|
||||
border: 'solid thin transparent !important',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
height: '26px',
|
||||
minHeight: '26px',
|
||||
borderRadius: '.5rem',
|
||||
boxShadow: 'none !important'
|
||||
};
|
||||
return obj;
|
||||
},
|
||||
valueContainer: (provided: any) => ({
|
||||
...provided,
|
||||
// paddingRight: '0px',
|
||||
width: 'fit-content',
|
||||
alignItems: 'center',
|
||||
height: '26px',
|
||||
padding: '0 3px'
|
||||
}),
|
||||
indicatorsContainer: (provided: any) => ({
|
||||
...provided,
|
||||
padding: '0px',
|
||||
height: '26px'
|
||||
}),
|
||||
menu: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
top: 0,
|
||||
borderRadius: '3px',
|
||||
border: `1px solid ${colors['gray-light']}`,
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '1px 1px 1px rgba(0, 0, 0, 0.1)',
|
||||
position: 'absolute',
|
||||
width: 'unset',
|
||||
maxWidth: '300px',
|
||||
overflow: 'hidden',
|
||||
zIndex: 100
|
||||
}),
|
||||
menuList: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
padding: 0
|
||||
}),
|
||||
noOptionsMessage: (provided: any) => ({
|
||||
...provided,
|
||||
whiteSpace: 'nowrap !important'
|
||||
// minWidth: 'fit-content',
|
||||
}),
|
||||
container: (provided: any) => ({
|
||||
...provided,
|
||||
top: '18px',
|
||||
position: 'absolute'
|
||||
}),
|
||||
input: (provided: any) => ({
|
||||
...provided,
|
||||
height: '22px',
|
||||
'& input:focus': {
|
||||
border: 'none !important'
|
||||
}
|
||||
}),
|
||||
singleValue: (provided: any, state: { isDisabled: any }) => {
|
||||
const opacity = state.isDisabled ? 0.5 : 1;
|
||||
const transition = 'opacity 300ms';
|
||||
|
||||
return {
|
||||
...provided,
|
||||
opacity,
|
||||
transition,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '20px'
|
||||
};
|
||||
}
|
||||
};
|
||||
import { searchService } from 'App/services';
|
||||
import { AutocompleteModal, AutoCompleteContainer } from './AutocompleteModal';
|
||||
|
||||
type FilterParam = { [key: string]: any };
|
||||
|
||||
function processKey(input: FilterParam): FilterParam {
|
||||
const result: FilterParam = {};
|
||||
for (const key in input) {
|
||||
if (input.type === 'metadata' && typeof input[key] === 'string' && input[key].startsWith('_')) {
|
||||
if (
|
||||
input.type === 'metadata' &&
|
||||
typeof input[key] === 'string' &&
|
||||
input[key].startsWith('_')
|
||||
) {
|
||||
result[key] = input[key].substring(1);
|
||||
} else {
|
||||
result[key] = input[key];
|
||||
|
|
@ -123,190 +31,110 @@ function processKey(input: FilterParam): FilterParam {
|
|||
interface Props {
|
||||
showOrButton?: boolean;
|
||||
showCloseButton?: boolean;
|
||||
onRemoveValue?: () => void;
|
||||
onAddValue?: () => void;
|
||||
onRemoveValue?: (ind: number) => void;
|
||||
onAddValue?: (ind: number) => void;
|
||||
endpoint?: string;
|
||||
method?: string;
|
||||
params?: any;
|
||||
headerText?: string;
|
||||
placeholder?: string;
|
||||
onSelect: (e: any, item: any) => void;
|
||||
onSelect: (e: any, item: any, index: number) => void;
|
||||
value: any;
|
||||
icon?: string;
|
||||
hideOrText?: boolean;
|
||||
onApplyValues: (values: string[]) => void;
|
||||
}
|
||||
|
||||
const FilterAutoComplete: React.FC<Props> = ({
|
||||
showCloseButton = false,
|
||||
placeholder = 'Type to search',
|
||||
method = 'GET',
|
||||
showOrButton = false,
|
||||
endpoint = '',
|
||||
params = {},
|
||||
value = '',
|
||||
hideOrText = false,
|
||||
onSelect,
|
||||
onRemoveValue,
|
||||
onAddValue
|
||||
}: Props) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [options, setOptions] = useState<{ value: string; label: string }[]>([]);
|
||||
const [query, setQuery] = useState(value);
|
||||
const [menuIsOpen, setMenuIsOpen] = useState(false);
|
||||
const [initialFocus, setInitialFocus] = useState(false);
|
||||
const [previousQuery, setPreviousQuery] = useState(value);
|
||||
const selectRef = useRef<any>(null);
|
||||
const inputRef = useRef<any>(null);
|
||||
const { filterStore } = useStore();
|
||||
const _params = processKey(params);
|
||||
const filterKey = `${_params.type}${_params.key || ''}`;
|
||||
const topValues = filterStore.topValues[filterKey] || [];
|
||||
const [topValuesLoading, setTopValuesLoading] = useState(false);
|
||||
const FilterAutoComplete = observer(
|
||||
({
|
||||
params = {},
|
||||
onClose,
|
||||
onApply,
|
||||
values,
|
||||
}: { params: any, values: string[], onClose: () => void, onApply: (values: string[]) => void }) => {
|
||||
const [options, setOptions] = useState<{ value: string; label: string }[]>(
|
||||
[]
|
||||
);
|
||||
const [initialFocus, setInitialFocus] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { filterStore } = useStore();
|
||||
const _params = processKey(params);
|
||||
const filterKey = `${_params.type}${_params.key || ''}`;
|
||||
const topValues = filterStore.topValues[filterKey] || [];
|
||||
|
||||
const loadTopValues = () => {
|
||||
setTopValuesLoading(true);
|
||||
filterStore.fetchTopValues(_params.type, _params.key).finally(() => {
|
||||
setTopValuesLoading(false);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
const loadTopValues = () => {
|
||||
void filterStore.fetchTopValues(_params.type, _params.key);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (topValues.length > 0) {
|
||||
const mappedValues = topValues.map((i) => ({ value: i.value, label: i.value }));
|
||||
setOptions(mappedValues);
|
||||
if (!query.length && initialFocus) {
|
||||
setMenuIsOpen(true);
|
||||
useEffect(() => {
|
||||
if (topValues.length > 0) {
|
||||
const mappedValues = topValues.map((i) => ({
|
||||
value: i.value,
|
||||
label: i.value,
|
||||
}));
|
||||
setOptions(mappedValues);
|
||||
}
|
||||
}
|
||||
}, [topValues, initialFocus, query.length]);
|
||||
}, [topValues, initialFocus]);
|
||||
|
||||
useEffect(loadTopValues, [_params.type]);
|
||||
useEffect(loadTopValues, [_params.type]);
|
||||
|
||||
useEffect(() => {
|
||||
setQuery(value);
|
||||
}, [value]);
|
||||
const loadOptions = async (
|
||||
inputValue: string,
|
||||
) => {
|
||||
if (!inputValue.length) {
|
||||
const mappedValues = topValues.map((i) => ({
|
||||
value: i.value,
|
||||
label: i.value,
|
||||
}));
|
||||
setOptions(mappedValues);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await searchService.fetchAutoCompleteValues({
|
||||
..._params,
|
||||
q: inputValue,
|
||||
});
|
||||
const _options =
|
||||
data.map((i: any) => ({ value: i.value, label: i.value })) || [];
|
||||
setOptions(_options);
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadOptions = async (inputValue: string, callback: (options: { value: string; label: string }[]) => void) => {
|
||||
if (!inputValue.length) {
|
||||
const mappedValues = topValues.map((i) => ({ value: i.value, label: i.value }));
|
||||
setOptions(mappedValues);
|
||||
callback(mappedValues);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 500), [
|
||||
params,
|
||||
topValues,
|
||||
]);
|
||||
|
||||
try {
|
||||
// const response = await new APIClient()[method.toLowerCase()](endpoint, { ..._params, q: inputValue });
|
||||
const data = await searchService.fetchAutoCompleteValues({ ..._params, q: inputValue })
|
||||
// const data = await response.json();
|
||||
const _options = data.map((i: any) => ({ value: i.value, label: i.value })) || [];
|
||||
setOptions(_options);
|
||||
callback(_options);
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const handleInputChange = (newValue: string) => {
|
||||
setInitialFocus(true);
|
||||
debouncedLoadOptions(newValue);
|
||||
};
|
||||
|
||||
const debouncedLoadOptions = useCallback(debounce(loadOptions, 1000), [params, topValues]);
|
||||
|
||||
const handleInputChange = (newValue: string) => {
|
||||
setLoading(true);
|
||||
setInitialFocus(true);
|
||||
setQuery(newValue);
|
||||
debouncedLoadOptions(newValue, () => {
|
||||
selectRef.current?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (item: { value: string }) => {
|
||||
setMenuIsOpen(false);
|
||||
setQuery(item.value);
|
||||
onSelect(null, item.value);
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
setInitialFocus(true);
|
||||
if (!query.length) {
|
||||
setLoading(topValuesLoading);
|
||||
setMenuIsOpen(!topValuesLoading && topValues.length > 0);
|
||||
const handleFocus = () => {
|
||||
setInitialFocus(true);
|
||||
setOptions(topValues.map((i) => ({ value: i.value, label: i.value })));
|
||||
} else {
|
||||
setMenuIsOpen(true);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setMenuIsOpen(false);
|
||||
setInitialFocus(false);
|
||||
if (query !== previousQuery) {
|
||||
onSelect(null, query);
|
||||
}
|
||||
setPreviousQuery(query);
|
||||
};
|
||||
console.log(options)
|
||||
return <AutocompleteModal
|
||||
values={values}
|
||||
onClose={onClose}
|
||||
onApply={onApply}
|
||||
handleFocus={handleFocus}
|
||||
loadOptions={handleInputChange}
|
||||
options={options}
|
||||
isLoading={loading}
|
||||
/>
|
||||
}
|
||||
);
|
||||
|
||||
const selected = value ? options.find((i) => i.value === query) : null;
|
||||
const uniqueOptions = options.filter((i) => i.value !== query);
|
||||
const selectOptionsArr = query.length ? [{ value: query, label: query }, ...uniqueOptions] : options;
|
||||
function AutoCompleteController(props: Props) {
|
||||
return <AutoCompleteContainer {...props} modalRenderer={FilterAutoComplete} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<div className={cn(stl.wrapper, 'relative')}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="w-full rounded px-2 no-focus"
|
||||
value={query}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => handleInputChange(e.target.value)}
|
||||
onClick={handleFocus}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
inputRef.current.blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{loading && (
|
||||
<div
|
||||
className="absolute top-0 right-0"
|
||||
style={{
|
||||
marginTop: '5px',
|
||||
marginRight: !showCloseButton || (showCloseButton && !showOrButton) ? '34px' : '62px'
|
||||
}}
|
||||
>
|
||||
<Icon name="spinner" className="animate-spin" size="14" />
|
||||
</div>
|
||||
)}
|
||||
<Select
|
||||
ref={selectRef}
|
||||
options={selectOptionsArr}
|
||||
value={selected}
|
||||
onChange={(e) => handleChange(e as { value: string })}
|
||||
menuIsOpen={initialFocus && menuIsOpen}
|
||||
menuPlacement="auto"
|
||||
styles={dropdownStyles}
|
||||
components={{
|
||||
Control: () => null
|
||||
}}
|
||||
/>
|
||||
<div className={stl.right}>
|
||||
{showCloseButton && (
|
||||
<div onClick={onRemoveValue}>
|
||||
<Icon name="close" size="12" />
|
||||
</div>
|
||||
)}
|
||||
{/*{showOrButton && (*/}
|
||||
{/* <div onClick={onAddValue} className="color-teal">*/}
|
||||
{/* <span className="px-1">or</span>*/}
|
||||
{/* </div>*/}
|
||||
{/*)}*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(FilterAutoComplete);
|
||||
export default AutoCompleteController;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import stl from './FilterAutoCompleteLocal.module.css';
|
||||
import { Input } from 'antd';
|
||||
|
||||
interface Props {
|
||||
showOrButton?: boolean;
|
||||
|
|
@ -58,7 +59,7 @@ function FilterAutoCompleteLocal(props: Props & { index: number }) {
|
|||
return (
|
||||
<div className="relative flex items-center">
|
||||
<div className={stl.wrapper}>
|
||||
<input
|
||||
<Input
|
||||
name="query"
|
||||
onInput={onInputChange}
|
||||
value={query}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import styles from './FilterDuration.module.css';
|
||||
import { Input } from 'UI'
|
||||
import { Input } from 'antd'
|
||||
|
||||
const fromMs = value => value ? `${ value / 1000 / 60 }` : ''
|
||||
const toMs = value => value !== '' ? value * 1000 * 60 : null
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { Space } from 'antd';
|
||||
import { List } from 'immutable';
|
||||
import { GripHorizontal } from 'lucide-react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button, Card } from 'antd';
|
||||
import { Button } from 'antd';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
import FilterItem from '../FilterItem';
|
||||
|
|
@ -44,7 +42,6 @@ function FilterList(props: Props) {
|
|||
|
||||
const filters = filter.filters;
|
||||
const hasEvents = filters.filter((i: any) => i.isEvent).length > 0;
|
||||
const hasFilters = filters.filter((i: any) => !i.isEvent).length > 0;
|
||||
|
||||
let rowIndex = 0;
|
||||
const cannotDeleteFilter = hasEvents && !supportsEmpty;
|
||||
|
|
@ -97,7 +94,7 @@ function FilterList(props: Props) {
|
|||
(event: Record<string, any>) => {
|
||||
event.preventDefault();
|
||||
if (draggedInd === null) return;
|
||||
const newItems = filters.toArray();
|
||||
const newItems = filters;
|
||||
const newPosition = calculateNewPosition(
|
||||
draggedInd,
|
||||
hoveredItem.i,
|
||||
|
|
@ -107,23 +104,32 @@ function FilterList(props: Props) {
|
|||
const reorderedItem = newItems.splice(draggedInd, 1)[0];
|
||||
newItems.splice(newPosition, 0, reorderedItem);
|
||||
|
||||
props.onFilterMove?.(List(newItems));
|
||||
props.onFilterMove?.(newItems);
|
||||
setHoveredItem({ i: null, position: null });
|
||||
setDraggedItem(null);
|
||||
},
|
||||
[draggedInd, hoveredItem, filters, props.onFilterMove]
|
||||
);
|
||||
|
||||
const eventsNum = filters.filter((i: any) => i.isEvent).size;
|
||||
const eventsNum = filters.filter((i: any) => i.isEvent).length;
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{onlyFilters ? null : (<Card size={'small'}>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="widget-wrapper flex flex-col">
|
||||
{onlyFilters ? null : (<div className={'border-b border-b-gray-lighter py-2 px-4'}>
|
||||
<div className="flex items-center mb-2 gap-2">
|
||||
<div className="font-semibold">
|
||||
{filter.eventsHeader || 'Events'}
|
||||
</div>
|
||||
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
|
||||
<Button
|
||||
icon={<Icon name={'filter'} />}
|
||||
type="default"
|
||||
size={'small'}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
|
||||
<Space>
|
||||
<div className={'ml-auto'}>
|
||||
{!hideEventsOrder && (
|
||||
<EventsOrder
|
||||
filter={filter}
|
||||
|
|
@ -132,7 +138,7 @@ function FilterList(props: Props) {
|
|||
)}
|
||||
{actions &&
|
||||
actions.map((action, index) => <div key={index}>{action}</div>)}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex flex-col'}>
|
||||
{filters.map((filter: any, filterIndex: number) =>
|
||||
|
|
@ -193,10 +199,9 @@ function FilterList(props: Props) {
|
|||
) : null
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-2" />
|
||||
</Card>)}
|
||||
</div>)}
|
||||
|
||||
<Card size={'small'}>
|
||||
<div className={'py-2 px-4'}>
|
||||
<div className={'flex items-center gap-2 mb-2'}>
|
||||
<div className="font-semibold">Filters</div>
|
||||
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
|
||||
|
|
@ -232,7 +237,7 @@ function FilterList(props: Props) {
|
|||
</div>
|
||||
) : null
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const dropdownStyles = {
|
|||
valueContainer: (provided: any) => ({
|
||||
...provided,
|
||||
width: 'fit-content',
|
||||
height: 26,
|
||||
'& input': {
|
||||
marginTop: '-3px',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ function FilterValue(props: Props) {
|
|||
props.onUpdate({ ...filter, value: newValue });
|
||||
};
|
||||
|
||||
const onApplyValues = (values: string[]) => {
|
||||
props.onUpdate({ ...filter, value: values });
|
||||
}
|
||||
|
||||
const onRemoveValue = (valueIndex: any) => {
|
||||
const newValue = filter.value.filter(
|
||||
(_: any, index: any) => index !== valueIndex
|
||||
|
|
@ -37,6 +41,7 @@ function FilterValue(props: Props) {
|
|||
};
|
||||
|
||||
const onChange = (e: any, item: any, valueIndex: any) => {
|
||||
console.log('item', item, valueIndex);
|
||||
const newValues = filter.value.map((_: any, _index: any) => {
|
||||
if (_index === valueIndex) {
|
||||
return item;
|
||||
|
|
@ -86,7 +91,6 @@ function FilterValue(props: Props) {
|
|||
|
||||
const renderValueFiled = (value: any[]) => {
|
||||
const showOrButton = filter.value.length > 1;
|
||||
const valueIndex = 0;
|
||||
const BaseFilterLocalAutoComplete = (props) => (
|
||||
<FilterAutoCompleteLocal
|
||||
value={value}
|
||||
|
|
@ -98,6 +102,16 @@ function FilterValue(props: Props) {
|
|||
{...props}
|
||||
/>
|
||||
);
|
||||
const BaseDropDown = (props) => (
|
||||
<FilterValueDropdown
|
||||
value={value}
|
||||
placeholder={filter.placeholder}
|
||||
options={filter.options}
|
||||
onApplyValues={onApplyValues}
|
||||
// onChange={(item, index) => onChange(null, { value: item.value }, index)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
switch (filter.type) {
|
||||
case FilterType.NUMBER_MULTIPLE:
|
||||
return <BaseFilterLocalAutoComplete type="number" />;
|
||||
|
|
@ -113,22 +127,13 @@ function FilterValue(props: Props) {
|
|||
return <BaseFilterLocalAutoComplete />;
|
||||
case FilterType.DROPDOWN:
|
||||
return (
|
||||
<FilterValueDropdown
|
||||
value={value}
|
||||
placeholder={filter.placeholder}
|
||||
options={filter.options}
|
||||
onChange={(item, index) => onChange(null, { value: item.value }, index)}
|
||||
/>
|
||||
<BaseDropDown />
|
||||
);
|
||||
case FilterType.ISSUE:
|
||||
case FilterType.MULTIPLE_DROPDOWN:
|
||||
return (
|
||||
<FilterValueDropdown
|
||||
<BaseDropDown
|
||||
search={true}
|
||||
value={value}
|
||||
placeholder={filter.placeholder}
|
||||
options={filter.options}
|
||||
onChange={(item, index) => onChange(null, { value: item.value }, index)}
|
||||
onAddValue={onAddValue}
|
||||
onRemoveValue={(ind) => onRemoveValue(ind)}
|
||||
showCloseButton={showCloseButton}
|
||||
|
|
@ -151,14 +156,14 @@ function FilterValue(props: Props) {
|
|||
value={value}
|
||||
showCloseButton={showCloseButton}
|
||||
showOrButton={showOrButton}
|
||||
onAddValue={onAddValue}
|
||||
onRemoveValue={() => onRemoveValue(valueIndex)}
|
||||
onApplyValues={onApplyValues}
|
||||
onRemoveValue={(index) => onRemoveValue(index)}
|
||||
method={'GET'}
|
||||
endpoint="/PROJECT_ID/events/search"
|
||||
params={getParms(filter.key)}
|
||||
headerText={''}
|
||||
placeholder={filter.placeholder}
|
||||
onSelect={(e, item) => onChange(e, item, valueIndex)}
|
||||
onSelect={(e, item, index) => onChange(e, item, index)}
|
||||
icon={filter.icon}
|
||||
/>
|
||||
);
|
||||
|
|
@ -177,63 +182,4 @@ function FilterValue(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
// const isEmpty = filter.value.length === 0 || !filter.value[0].length;
|
||||
// return (
|
||||
// <div
|
||||
// className={
|
||||
// 'rounded border border-gray-light px-2 relative w-fit whitespace-nowrap'
|
||||
// }
|
||||
// style={{ height: 26 }}
|
||||
// ref={filterValueContainer}
|
||||
// >
|
||||
// <div onClick={() => setShowValueModal(true)} className={'flex items-center gap-2 '}>
|
||||
// {!isEmpty ? (
|
||||
// <>
|
||||
// <div
|
||||
// className={
|
||||
// 'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'
|
||||
// }
|
||||
// >
|
||||
// {filter.value[0]}
|
||||
// </div>
|
||||
// <div
|
||||
// className={
|
||||
// 'rounded-xl bg-gray-lighter leading-none px-1 py-0.5'
|
||||
// }
|
||||
// >
|
||||
// + {filter.value.length - 1} More
|
||||
// </div>
|
||||
// </>
|
||||
// ) : (
|
||||
// <div className={'text-disabled-text'}>Select values</div>
|
||||
// )}
|
||||
// </div>
|
||||
// {showValueModal ? (
|
||||
// <div
|
||||
// className={cn(
|
||||
// 'absolute left-0 mt-6 flex items-center gap-2 bg-white border shadow border-gray-light z-10',
|
||||
// {
|
||||
// 'grid-cols-2': filter.hasSource,
|
||||
// 'grid-cols-3': !filter.hasSource,
|
||||
// }
|
||||
// )}
|
||||
// style={{ minWidth: 200, minHeight: 100, top: '100%' }}
|
||||
// >
|
||||
// {filter.type === FilterType.DURATION
|
||||
// ? renderValueFiled(filter.value, 0)
|
||||
// : filter.value &&
|
||||
// filter.value.map((value: any, valueIndex: any) => (
|
||||
// <div key={valueIndex}>
|
||||
// {renderValueFiled(value, valueIndex)}
|
||||
// </div>
|
||||
// ))}
|
||||
// <div>
|
||||
// <Button>Apply</Button>
|
||||
// <Button>Cancel</Button>
|
||||
// </div>
|
||||
// </div>
|
||||
// ) : null}
|
||||
// </div>
|
||||
// );
|
||||
|
||||
export default observer(FilterValue);
|
||||
|
|
|
|||
|
|
@ -1,136 +1,41 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { AutoCompleteContainer, AutocompleteModal } from "../FilterAutoComplete/AutocompleteModal";
|
||||
import stl from './FilterValueDropdown.module.css';
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
const dropdownStyles = {
|
||||
control: (provided: any) => {
|
||||
const obj = {
|
||||
...provided,
|
||||
border: 'solid thin transparent !important',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
height: '26px',
|
||||
minHeight: '26px',
|
||||
borderRadius: '.5rem',
|
||||
boxShadow: 'none !important',
|
||||
};
|
||||
return obj;
|
||||
},
|
||||
valueContainer: (provided: any) => ({
|
||||
...provided,
|
||||
// paddingRight: '0px',
|
||||
width: 'fit-content',
|
||||
alignItems: 'center',
|
||||
height: '26px',
|
||||
padding: '0 3px',
|
||||
}),
|
||||
// placeholder: (provided: any) => ({
|
||||
// ...provided,
|
||||
// }),
|
||||
indicatorsContainer: (provided: any) => ({
|
||||
...provided,
|
||||
padding: '0px',
|
||||
height: '26px',
|
||||
}),
|
||||
option: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
menu: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
top: 20,
|
||||
left: 0,
|
||||
minWidth: 'fit-content',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
container: (provided: any) => ({
|
||||
...provided,
|
||||
width: '100%',
|
||||
}),
|
||||
input: (provided: any) => ({
|
||||
...provided,
|
||||
height: '22px',
|
||||
'& input:focus': {
|
||||
border: 'none !important',
|
||||
},
|
||||
}),
|
||||
singleValue: (provided: any, state: { isDisabled: any }) => {
|
||||
const opacity = state.isDisabled ? 0.5 : 1;
|
||||
const transition = 'opacity 300ms';
|
||||
|
||||
return {
|
||||
...provided,
|
||||
opacity,
|
||||
transition,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '20px',
|
||||
};
|
||||
},
|
||||
};
|
||||
interface Props {
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (value: any, ind: number) => void;
|
||||
className?: string;
|
||||
options: any[];
|
||||
search?: boolean;
|
||||
showCloseButton?: boolean;
|
||||
showOrButton?: boolean;
|
||||
onRemoveValue?: (ind: number) => void;
|
||||
onAddValue?: (ind: number) => void;
|
||||
isMultiple?: boolean;
|
||||
index: number;
|
||||
onApply: (values: string[]) => void;
|
||||
onClose: () => void;
|
||||
values: string[];
|
||||
}
|
||||
function FilterValueDropdown(props: Props) {
|
||||
const {
|
||||
placeholder = 'Select',
|
||||
isMultiple = true,
|
||||
search = false,
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
showCloseButton = true,
|
||||
showOrButton = true,
|
||||
index,
|
||||
onApply,
|
||||
onClose,
|
||||
values,
|
||||
} = props;
|
||||
const [query, setQuery] = React.useState('');
|
||||
|
||||
const filteredOptions = query.length ? options.filter((option) => {
|
||||
return option.label.toLowerCase().includes(query.toLowerCase());
|
||||
}) : options
|
||||
return (
|
||||
<div className="relative flex items-center w-full">
|
||||
<div className={stl.wrapper}>
|
||||
<Select
|
||||
isSearchable={search}
|
||||
options={options}
|
||||
name="issue_type"
|
||||
value={value ? options.find((item) => item.value === value) : null}
|
||||
onChange={(value: any) => onChange(value.value, index)}
|
||||
placeholder={placeholder}
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
<div className={stl.right}>
|
||||
{showCloseButton && (
|
||||
<div onClick={() => props.onRemoveValue?.(index)}>
|
||||
<Icon name="close" size="12" />
|
||||
</div>
|
||||
)}
|
||||
{showOrButton && (
|
||||
<div onClick={() => props.onAddValue?.(index)} className="color-teal">
|
||||
<span className="px-1">or</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!showOrButton && isMultiple && <div className="ml-3">or</div>}
|
||||
</div>
|
||||
<AutocompleteModal
|
||||
values={values}
|
||||
onClose={onClose}
|
||||
onApply={onApply}
|
||||
loadOptions={setQuery}
|
||||
options={filteredOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface MainProps {
|
||||
placeholder?: string;
|
||||
value: string[];
|
||||
onChange: (value: any, ind: number) => void;
|
||||
onApplyValues: (values: string[]) => void;
|
||||
className?: string;
|
||||
options: any[];
|
||||
search?: boolean;
|
||||
|
|
@ -142,16 +47,17 @@ interface MainProps {
|
|||
}
|
||||
|
||||
function FilterDropdownController(props: MainProps) {
|
||||
return props.value.map((value, index) => (
|
||||
<FilterValueDropdown
|
||||
{...props}
|
||||
key={index}
|
||||
value={value}
|
||||
index={index}
|
||||
showOrButton={index === props.value.length - 1}
|
||||
showCloseButton={props.value.length > 1}
|
||||
/>
|
||||
))
|
||||
const mapValues = (value: string) => {
|
||||
return props.options.find((option) => option.value === value)?.label || value;
|
||||
}
|
||||
|
||||
return <AutoCompleteContainer
|
||||
value={props.value}
|
||||
onApplyValues={props.onApplyValues}
|
||||
modalRenderer={FilterValueDropdown}
|
||||
modalProps={{ options: props.options }}
|
||||
mapValues={mapValues}
|
||||
/>
|
||||
}
|
||||
|
||||
export default FilterDropdownController;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,13 @@
|
|||
import React from 'react';
|
||||
import SessionSearchField from 'Shared/SessionSearchField';
|
||||
import AiSessionSearchField from 'Shared/SessionSearchField/AiSessionSearchField';
|
||||
import SavedSearch from 'Shared/SavedSearch';
|
||||
import { Button } from 'antd';
|
||||
import TagList from './components/TagList';
|
||||
import SessionFilters from 'Shared/SessionFilters';
|
||||
import AiSessionSearchField from 'Shared/SessionFilters/AiSessionSearchField';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
const MainSearchBar = (props: Props) => {
|
||||
const MainSearchBar = () => {
|
||||
const { searchStore, projectsStore } = useStore();
|
||||
const appliedFilter = searchStore.instance;
|
||||
const savedSearch = searchStore.savedSearch;
|
||||
const projectId = projectsStore.siteId;
|
||||
const currSite = React.useRef(projectId);
|
||||
const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.length > 0;
|
||||
const hasSavedSearch = savedSearch && savedSearch.exists();
|
||||
const hasSearch = hasFilters || hasSavedSearch;
|
||||
|
||||
// @ts-ignore
|
||||
const originStr = window.env.ORIGIN || window.location.origin;
|
||||
|
|
@ -33,24 +21,9 @@ const MainSearchBar = (props: Props) => {
|
|||
}
|
||||
}, [projectId]);
|
||||
return (
|
||||
<div className="flex items-center flex-wrap">
|
||||
<div style={{ flex: 3, marginRight: '10px' }}>
|
||||
{isSaas ? <AiSessionSearchField /> : <SessionSearchField />}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 my-2 xl:my-0" style={{ flex: 2 }}>
|
||||
<TagList />
|
||||
<SavedSearch />
|
||||
<Button
|
||||
// variant={hasSearch ? 'text-primary' : 'text'}
|
||||
// className="ml-auto font-medium"
|
||||
type="link"
|
||||
disabled={!hasSearch}
|
||||
onClick={() => searchStore.clearSearch()}
|
||||
className="ml-auto font-medium"
|
||||
>
|
||||
Clear Search
|
||||
</Button>
|
||||
</div>
|
||||
<div className={'flex flex-col gap-2 w-full'}>
|
||||
{isSaas ? <AiSessionSearchField /> : null}
|
||||
<SessionFilters />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button } from 'UI';
|
||||
import { Button } from 'antd';
|
||||
import SaveSearchModal from 'Shared/SaveSearchModal';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
|
||||
function SaveFilterButton() {
|
||||
function SaveFilterButton({ disabled }: { disabled?: boolean }) {
|
||||
const { searchStore } = useStore();
|
||||
const savedSearch = searchStore.savedSearch;
|
||||
const [showModal, setshowModal] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
className="mr-2"
|
||||
type="link"
|
||||
disabled={disabled}
|
||||
onClick={() => setshowModal(true)}
|
||||
icon="zoom-in">
|
||||
{savedSearch.exists() ? 'UPDATE SEARCH' : 'SAVE SEARCH'}
|
||||
>
|
||||
{savedSearch.exists() ? 'Update Search' : 'Save Search'}
|
||||
</Button>
|
||||
{showModal && (<SaveSearchModal show={showModal} closeHandler={() => setshowModal(false)} />)}
|
||||
{showModal && (
|
||||
<SaveSearchModal
|
||||
show={showModal}
|
||||
closeHandler={() => setshowModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import SaveFilterButton from 'Shared/SaveFilterButton';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import TagList from '../MainSearchBar/components/TagList';
|
||||
import SavedSearch from '../SavedSearch/SavedSearch';
|
||||
import { Button } from 'antd';
|
||||
|
||||
function SearchActions() {
|
||||
const { aiFiltersStore, searchStore, customFieldStore } = useStore();
|
||||
const appliedFilter = searchStore.instance;
|
||||
const metaLoading = customFieldStore.isLoading;
|
||||
const hasEvents =
|
||||
appliedFilter.filters.filter((i: any) => i.isEvent).length > 0;
|
||||
const hasFilters =
|
||||
appliedFilter.filters.filter((i: any) => !i.isEvent).length > 0;
|
||||
const savedSearch = searchStore.savedSearch;
|
||||
const hasSavedSearch = savedSearch && savedSearch.exists();
|
||||
const hasSearch = hasFilters || hasSavedSearch;
|
||||
|
||||
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
|
||||
return !metaLoading ? (
|
||||
<div className={'mb-2'}>
|
||||
<div className={'flex items-center gap-2 w-full'}>
|
||||
<TagList />
|
||||
<SavedSearch />
|
||||
<div className={'ml-auto'} />
|
||||
<Button
|
||||
type="link"
|
||||
disabled={!hasSearch}
|
||||
onClick={() => searchStore.clearSearch()}
|
||||
className="font-medium"
|
||||
>
|
||||
Clear Search
|
||||
</Button>
|
||||
<SaveFilterButton disabled={!hasEvents && !hasFilters} />
|
||||
</div>
|
||||
{showPanel ? (
|
||||
<>
|
||||
{aiFiltersStore.isLoading ? (
|
||||
<div className={'font-semibold flex items-center gap-2 p-4'}>
|
||||
<AnimatedSVG name={ICONS.LOADER} size={18} />
|
||||
<span>Translating your query into search steps...</span>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default observer(SearchActions);
|
||||
1
frontend/app/components/shared/SearchActions/index.ts
Normal file
1
frontend/app/components/shared/SearchActions/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SearchActions';
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { debounce } from 'App/utils';
|
||||
import FilterList from 'Shared/Filters/FilterList';
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler';
|
||||
import { FilterKey } from 'App/types/filter/filterType';
|
||||
import { addOptionsToFilter } from 'App/types/filter/newFilter';
|
||||
|
||||
let debounceFetch: any = () => {};
|
||||
function SessionFilters() {
|
||||
const {
|
||||
searchStore,
|
||||
projectsStore,
|
||||
customFieldStore,
|
||||
tagWatchStore,
|
||||
} = useStore();
|
||||
|
||||
const appliedFilter = searchStore.instance;
|
||||
const metaLoading = customFieldStore.isLoading;
|
||||
const saveRequestPayloads =
|
||||
projectsStore.instance?.saveRequestPayloads ?? false;
|
||||
|
||||
useSessionSearchQueryHandler({
|
||||
appliedFilter,
|
||||
loading: metaLoading,
|
||||
onBeforeLoad: async () => {
|
||||
const tags = await tagWatchStore.getTags();
|
||||
if (tags) {
|
||||
addOptionsToFilter(
|
||||
FilterKey.TAGGED_ELEMENT,
|
||||
tags.map((tag) => ({
|
||||
label: tag.name,
|
||||
value: tag.tagId.toString(),
|
||||
}))
|
||||
);
|
||||
searchStore.refreshFilterOptions();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
debounceFetch();
|
||||
}, [appliedFilter.filters]);
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
searchStore.addFilter(filter);
|
||||
};
|
||||
|
||||
const onUpdateFilter = (filterIndex: any, filter: any) => {
|
||||
searchStore.updateFilter(filterIndex, filter);
|
||||
};
|
||||
|
||||
const onFilterMove = (newFilters: any) => {
|
||||
searchStore.updateFilter(0, {
|
||||
...appliedFilter,
|
||||
filters: newFilters,
|
||||
});
|
||||
|
||||
debounceFetch();
|
||||
};
|
||||
|
||||
const onRemoveFilter = (filterIndex: any) => {
|
||||
searchStore.removeFilter(filterIndex);
|
||||
|
||||
debounceFetch();
|
||||
};
|
||||
|
||||
const onChangeEventsOrder = (e: any, { value }: any) => {
|
||||
searchStore.edit({
|
||||
eventsOrder: value,
|
||||
});
|
||||
|
||||
debounceFetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<FilterList
|
||||
filter={appliedFilter}
|
||||
onAddFilter={onAddFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
onFilterMove={onFilterMove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(SessionFilters);
|
||||
1
frontend/app/components/shared/SessionFilters/index.ts
Normal file
1
frontend/app/components/shared/SessionFilters/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionFilters';
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import FilterList from 'Shared/Filters/FilterList';
|
||||
import FilterSelection from 'Shared/Filters/FilterSelection';
|
||||
import SaveFilterButton from 'Shared/SaveFilterButton';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { addOptionsToFilter } from 'Types/filter/newFilter';
|
||||
import { Button, Loader } from 'UI';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { debounce } from 'App/utils';
|
||||
import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler';
|
||||
|
||||
let debounceFetch: any = () => {
|
||||
};
|
||||
|
||||
function SessionSearch() {
|
||||
const { tagWatchStore, aiFiltersStore, searchStore, customFieldStore, projectsStore } = useStore();
|
||||
const appliedFilter = searchStore.instance;
|
||||
const metaLoading = customFieldStore.isLoading;
|
||||
const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).length > 0;
|
||||
const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).length > 0;
|
||||
const saveRequestPayloads = projectsStore.instance?.saveRequestPayloads ?? false;
|
||||
|
||||
useSessionSearchQueryHandler({
|
||||
appliedFilter,
|
||||
loading: metaLoading,
|
||||
onBeforeLoad: async () => {
|
||||
const tags = await tagWatchStore.getTags();
|
||||
if (tags) {
|
||||
addOptionsToFilter(
|
||||
FilterKey.TAGGED_ELEMENT,
|
||||
tags.map((tag) => ({
|
||||
label: tag.name,
|
||||
value: tag.tagId.toString()
|
||||
}))
|
||||
);
|
||||
searchStore.refreshFilterOptions();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
|
||||
// void searchStore.fetchSessions(true)
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
debounceFetch();
|
||||
}, [appliedFilter.filters]);
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
searchStore.addFilter(filter);
|
||||
};
|
||||
|
||||
const onUpdateFilter = (filterIndex: any, filter: any) => {
|
||||
searchStore.updateFilter(filterIndex, filter);
|
||||
};
|
||||
|
||||
const onFilterMove = (newFilters: any) => {
|
||||
searchStore.updateFilter(0, {
|
||||
...appliedFilter,
|
||||
filters: newFilters
|
||||
});
|
||||
|
||||
debounceFetch();
|
||||
};
|
||||
|
||||
const onRemoveFilter = (filterIndex: any) => {
|
||||
const newFilters = appliedFilter.filters.filter((_filter: any, i: any) => {
|
||||
return i !== filterIndex;
|
||||
});
|
||||
|
||||
searchStore.removeFilter(filterIndex);
|
||||
|
||||
debounceFetch();
|
||||
};
|
||||
|
||||
const onChangeEventsOrder = (e: any, { value }: any) => {
|
||||
searchStore.edit({
|
||||
eventsOrder: value
|
||||
});
|
||||
|
||||
debounceFetch();
|
||||
};
|
||||
|
||||
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
|
||||
return !metaLoading ? (
|
||||
<>
|
||||
{showPanel ? (
|
||||
<div className="border bg-white rounded-lg mt-4">
|
||||
<div className="p-5">
|
||||
{aiFiltersStore.isLoading ? (
|
||||
<div className={'font-semibold flex items-center gap-2 mb-2'}>
|
||||
<AnimatedSVG name={ICONS.LOADER} size={18} />
|
||||
<span>Translating your query into search steps...</span>
|
||||
</div>
|
||||
) : null}
|
||||
{hasEvents || hasFilters ? (
|
||||
<FilterList
|
||||
filter={appliedFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
onFilterMove={onFilterMove}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasEvents || hasFilters ? (
|
||||
<div className="border-t px-5 py-1 flex items-center -mx-2">
|
||||
<div>
|
||||
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
|
||||
<Button variant="text-primary" className="mr-2" icon="plus">
|
||||
ADD STEP
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<SaveFilterButton />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default observer(SessionSearch);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SessionSearch';
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Input } from 'UI';
|
||||
import FilterModal from 'Shared/Filters/FilterModal';
|
||||
import { debounce } from 'App/utils';
|
||||
import { assist as assistRoute, isRoute } from 'App/routes';
|
||||
|
||||
const ASSIST_ROUTE = assistRoute();
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
function SessionSearchField(props: Props) {
|
||||
const { searchStore, searchStoreLive } = useStore();
|
||||
const isLive =
|
||||
isRoute(ASSIST_ROUTE, window.location.pathname) ||
|
||||
window.location.pathname.includes('multiview');
|
||||
|
||||
const fetchFilterSearch = isLive
|
||||
? searchStoreLive.fetchFilterSearch.bind(searchStoreLive)
|
||||
: searchStore.fetchFilterSearch.bind(searchStore);
|
||||
|
||||
const debounceFetchFilterSearch = React.useCallback(
|
||||
debounce(fetchFilterSearch, 1000),
|
||||
[]
|
||||
);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const onSearchChange = ({ target: { value } }: any) => {
|
||||
setSearchQuery(value);
|
||||
debounceFetchFilterSearch({ q: value });
|
||||
};
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
isLive
|
||||
? searchStoreLive.addFilterByKeyAndValue(filter.key, filter.value)
|
||||
: searchStore.addFilterByKeyAndValue(filter.key, filter.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
icon="search"
|
||||
onFocus={() => setShowModal(true)}
|
||||
onBlur={() => setTimeout(setShowModal, 200, false)}
|
||||
onChange={onSearchChange}
|
||||
placeholder={'Search sessions using any captured event (click, input, page, error...)'}
|
||||
style={{ minWidth: 360 }}
|
||||
id="search"
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
className="hover:border-gray-medium text-lg placeholder-lg h-9 shadow-sm"
|
||||
/>
|
||||
|
||||
{showModal && (
|
||||
<div className="absolute left-0 shadow-sm rounded-lg bg-white z-50">
|
||||
<FilterModal
|
||||
isMainSearch={true}
|
||||
onFilterClick={onAddFilter}
|
||||
isLive={isLive}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(SessionSearchField);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SessionSearchField';
|
||||
|
|
@ -436,3 +436,9 @@ p {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none; /* Remove default styling */
|
||||
appearance: none;
|
||||
margin: 0; /* Fix margin if necessary */
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue