diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx index 66ecaab00..0462a03b8 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx @@ -1,11 +1,12 @@ import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Icon, Tooltip, useStyles2 } from '@grafana/ui'; -import React, { Fragment, memo, useEffect, useRef, useState } from 'react'; +import React, { Fragment, memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { AdHocFiltersVariable } from '../AdHocFiltersVariable'; import { AdHocFilterPill } from './AdHocFilterPill'; import { AdHocFiltersAlwaysWipCombobox } from './AdHocFiltersAlwaysWipCombobox'; import { debounce } from 'lodash'; +import { calculateCollapseThreshold } from './utils'; interface Props { model: AdHocFiltersVariable; @@ -14,37 +15,42 @@ interface Props { export const AdHocFiltersComboboxRenderer = memo(function AdHocFiltersComboboxRenderer({ model }: Props) { const { filters, readOnly } = model.useState(); const styles = useStyles2(getStyles); - const [limitFiltersTo, setLimitFiltersTo] = useState(null); const wrapperRef = useRef(null); - const handleCollapseFilters = (shouldCollapse: boolean) => { - if (!shouldCollapse) { - setLimitFiltersTo(null); - return; - } - if (wrapperRef.current) { - const rect = wrapperRef.current.getBoundingClientRect(); - if (rect.height - 6 > 26) { - const componentLineSpan = (rect.height - 6) / 26; - const filterCutOff = Math.max(1, Math.floor(filters.length / (componentLineSpan + 1))); - setLimitFiltersTo(filterCutOff); - } else { - setLimitFiltersTo(null); - } - } - }; + // ref that focuses on the always wip filter input + // defined in the combobox component via useImperativeHandle + const focusOnWipInputRef = useRef<() => void>(); + + const [collapseThreshold, setCollapseThreshold] = useState(null); + + const updateCollapseThreshold = useCallback( + (shouldCollapse: boolean, filtersLength: number) => { + const filterCollapseThreshold = calculateCollapseThreshold( + !readOnly ? shouldCollapse : false, + filtersLength, + wrapperRef + ); + setCollapseThreshold(filterCollapseThreshold); + }, + [readOnly] + ); + + const debouncedSetActive = useMemo(() => debounce(updateCollapseThreshold, 100), [updateCollapseThreshold]); - const debouncedSetActive = debounce(handleCollapseFilters, 100); + const handleFilterCollapse = useCallback( + (shouldCollapse: boolean, filtersLength: number) => () => { + debouncedSetActive(shouldCollapse, filtersLength); + }, + [debouncedSetActive] + ); - useEffect(() => { - handleCollapseFilters(true); + useLayoutEffect(() => { + // updateCollapseThreshold(!!model.state.collapseFilters ? true : false, filters.length); + updateCollapseThreshold(true, filters.length); + // needs to run only on first render // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // ref that focuses on the always wip filter input - // defined in the combobox component via useImperativeHandle - const focusOnWipInputRef = useRef<() => void>(); - return (
{ - debouncedSetActive(false); - }} - onBlurCapture={(e) => { - debouncedSetActive(true); - }} + onFocusCapture={handleFilterCollapse(false, filters.length)} + // onBlurCapture={handleFilterCollapse(!!model.state.collapseFilters ? true : false, filters.length} + onBlurCapture={handleFilterCollapse(true, filters.length)} > - {(limitFiltersTo ? filters.slice(0, limitFiltersTo) : filters).map((filter, index) => ( + {(collapseThreshold ? filters.slice(0, collapseThreshold) : filters).map((filter, index) => ( ))} - {limitFiltersTo ? ( + {collapseThreshold ? ( - {filters.slice(limitFiltersTo).map((filter, i) => { + {filters.slice(collapseThreshold).map((filter, i) => { const keyLabel = filter.keyLabel ?? filter.key; - // TODO remove when we're on the latest version of @grafana/data - //@ts-expect-error const valueLabel = filter.valueLabels?.join(', ') || filter.values?.join(', ') || filter.value; return ( @@ -89,7 +90,7 @@ export const AdHocFiltersComboboxRenderer = memo(function AdHocFiltersComboboxRe
} > -
+{filters.length - limitFiltersTo} filters
+
+{filters.length - collapseThreshold} filters
) : null} diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/utils.ts b/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/utils.ts index 78f0078d9..8f2f65e19 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/utils.ts +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/utils.ts @@ -211,3 +211,24 @@ export const populateInputValueOnInputTypeSwitch = ({ setInputValue(''); } }; + +export const calculateCollapseThreshold = ( + shouldCollapse: boolean, + filtersLength: number, + wrapperRef: React.RefObject +) => { + if (!shouldCollapse || !wrapperRef.current) { + return null; + } + + const rect = wrapperRef.current.getBoundingClientRect(); + + if (rect.height - 6 > 26) { + const componentLineSpan = (rect.height - 6) / 26; + const filterCutOff = Math.max(1, Math.floor(filtersLength / (componentLineSpan + 1))); + + return filterCutOff; + } + + return null; +}; diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx index 2c4249c9d..a079ae476 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx @@ -103,6 +103,11 @@ export interface AdHocFiltersVariableState extends SceneVariableState { * @internal state of the new filter being added */ _wip?: AdHocFilterWithLabels; + + /** + * Flag that will collapse combobox filters when they become too long + */ + collapseFilters?: boolean; } export type AdHocVariableExpressionBuilderFn = (filters: AdHocFilterWithLabels[]) => string;