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 (
);
});
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 && (
)}
{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])}
/>
);
}