import { observer } from 'mobx-react-lite'; import { CalendarInput, RuleFormProps, useFieldStats, useVisualCount } from './tabs'; import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { useCompututaion } from '../../store'; import { useVirtualizer } from '@tanstack/react-virtual'; import { Popover, Transition } from '@headlessui/react'; import { _unstable_encodeRuleValue, classNames, formatDate } from '../../utils'; import { parsedOffsetDate } from '../../lib/op/offset'; import LoadingLayer from '../../components/loadingLayer'; import { IFilterField, IKeyWord } from '../../interfaces'; import { ArrowRightIcon, CheckIcon, ChevronUpDownIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import { getTemporalRange, withComputedField } from '../../computation'; import { Slider } from '../../components/rangeslider'; import { createStreamedValueHook, useDebounceValueBind } from '../../hooks'; import { GLOBAL_CONFIG } from '../../config'; import { debounce } from 'lodash-es'; import { Input } from '@/components/ui/input'; import { SparseArray } from './array'; const sortConfig = { key: 'value', ascending: true, } as const; export const SimpleSearcher = observer(function SimpleSearcher({ field, onChange }: RuleFormProps) { if (field.rule?.type !== 'regexp') return null; const { value, caseSensitive } = field.rule; return (
{ onChange({ type: 'regexp', value: e.target.value, caseSensitive: !!caseSensitive, }); }} />
{ onChange({ type: 'regexp', value: value, caseSensitive: v, }); }} >
); }); function Toggle(props: { children?: React.ReactNode; value: boolean; onChange?: (v: boolean) => void; label?: string }) { return (
{ props.onChange?.(!props.value); }} className={classNames( props.value ? 'bg-gray-100 dark:bg-zinc-700' : 'hover:bg-gray-100 dark:hover:bg-zinc-700', 'rounded cursor-pointer p-1 w-6 h-6 flex items-center justify-center' )} > {props.children}
); } export const SimpleOneOfSelector = observer(function SimpleOneOfSelector({ field, onChange, allFields, displayOffset }: RuleFormProps) { const computation = useCompututaion(); const [keywordValue, setKeyword] = useState(''); const [isCaseSenstive, setIsCaseSenstive] = useState(false); const [isWord, setIsWord] = useState(false); const [isRegexp, setIsRegexp] = useState(false); const keyword = useMemo( () => keywordValue ? { value: keywordValue, caseSenstive: isCaseSenstive, word: isWord, regexp: isRegexp, } : undefined, [keywordValue, isCaseSenstive, isWord, isRegexp] ); const debouncer = useMemo(() => { const { timeout, ...options } = GLOBAL_CONFIG.KEYWORD_DEBOUNCE_SETTING; return function any>(f: T) { return debounce(f, timeout, options); }; }, []); const debouncedKeyword = createStreamedValueHook(debouncer)(keyword); const enableKeyword = field.semanticType === 'nominal'; const searchKeyword = enableKeyword ? debouncedKeyword : undefined; const { data, currentRows, handleSelect, loadData, loading } = useVisualCount( field, `${sortConfig.key}${sortConfig.ascending ? '' : '_dsc'}`, computation, onChange, allFields, { displayOffset, keyword: searchKeyword, } ); if (field.rule?.type !== 'one of' && field.rule?.type !== 'not in') return null; return (
{field.rule.value.length > 0 && (
{field.rule.type === 'not in' ? 'exclude: ' : ''} {Array.from(field.rule.value).slice(0, 3).join(', ')}
{field.rule.value.length > 3 &&
+{field.rule.value.length - 3}
}
)} {field.rule.value.length === 0 && (
{field.rule.type === 'one of' ? 'Select Values...' : 'Select Values to Exclude...'}
)}
{enableKeyword && (
{ setKeyword(e.target.value); }} />
)} {loading && (
)} {!loading && ( )}
); }); const tableRow = 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none space-x-2 hover:bg-gray-100 dark:hover:bg-gray-800'; function VirtualList({ field, distinctTotal, data, handleSelect, loadData, displayOffset, }: { field: IFilterField; distinctTotal?: number; data: SparseArray<{ value: string | number; count: number }>; handleSelect: (value: string | number, checked: boolean, itemNum: number) => void; loadData: (index: number) => void; displayOffset?: number; }) { const parentRef = useRef(null); const rowVirtualizer = useVirtualizer({ count: distinctTotal ?? 0, getScrollElement: () => parentRef.current, estimateSize: () => 32, overscan: 10, }); const ruleSet = useMemo( () => field.rule && (field.rule.type === 'not in' || field.rule?.type === 'one of') ? new Set(field.rule.value.map((x) => _unstable_encodeRuleValue(x))) : null, [field] ); return (
{rowVirtualizer.getVirtualItems().map((vItem) => { const idx = vItem.index; const item = data?.get(idx); if (!item) { return (
loadData(idx)} effectId={`${field.fid}_${sortConfig.key}${sortConfig.ascending ? '' : '_dsc'}_${idx}`} />
); } const { value, count } = item; const checked = (!!ruleSet && field.rule?.type === 'one of' && ruleSet.has(_unstable_encodeRuleValue(value))) || (!!ruleSet && field.rule?.type === 'not in' && !ruleSet.has(_unstable_encodeRuleValue(value))); const displayValue = field.semanticType === 'temporal' ? formatDate(parsedOffsetDate(displayOffset, field.offset)(value)) : `${value}`; return (
handleSelect(value, !checked, count)} >
{displayValue}
); })}
{distinctTotal === 0 &&
No value found.
}
); } const Effecter = (props: { effect: () => void; effectId: any }) => { useEffect(() => { props.effect(); }, [props.effectId]); return null; }; export const SimpleTemporalRange: React.FC = ({ field, allFields, onChange, displayOffset }) => { const computationFunction = useCompututaion(); const [res, setRes] = useState<[number, number, string, boolean]>(() => [0, 0, '', false]); React.useEffect(() => { withComputedField(field, allFields, computationFunction, { timezoneDisplayOffset: displayOffset })((service) => getTemporalRange(service, field.fid, field.offset) ).then(([min, max, format]) => setRes([min, max, format, true])); }, [field.fid]); const [min, max, format, loaded] = res; const offset = field.offset ?? new Date().getTimezoneOffset(); const handleChange = React.useCallback( (value: [number | null, number | null]) => { onChange({ type: 'temporal range', value, format, offset, }); }, [format, offset] ); if (field.rule?.type !== 'temporal range') return null; if (!loaded) { return (
); } return (
handleChange([value, field.rule?.value[1]])} />
handleChange([field.rule?.value[0], value])} />
); }; export const SimpleRange: React.FC = ({ field, onChange, allFields, displayOffset }) => { const computation = useCompututaion(); const [stats] = useFieldStats(field, { values: false, range: true, valuesMeta: false, displayOffset }, 'none', computation, allFields); const range = stats?.range; const handleChange = React.useCallback((value: [number | null, number | null]) => { onChange({ type: 'range', value, }); }, []); if (field.rule?.type !== 'range') return null; if (!range) { return (
); } return ; }; function RangeField({ name, value, range, onChange, }: { name: string; value: [number | null, number | null]; range: [number, number]; onChange: (v: [number | null, number | null]) => void; }) { const [innerValue, setInnerValue] = useDebounceValueBind(value, onChange); // step to last digit const stepDigit = 10 ** Math.floor(Math.log10((range[1] - range[0]) / 100)); return (
[{innerValue[0]},{innerValue[1]}]
setInnerValue([min, max])} />
); }