diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx index a5ccf2a1e..5c5aa709e 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx @@ -1,10 +1,12 @@ import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { Icon, useStyles2 } from '@grafana/ui'; -import React, { memo, useRef } from 'react'; +import { Icon, Tooltip, useStyles2 } from '@grafana/ui'; +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; @@ -13,21 +15,59 @@ interface Props { export const AdHocFiltersComboboxRenderer = memo(function AdHocFiltersComboboxRenderer({ model }: Props) { const { filters, readOnly } = model.useState(); const styles = useStyles2(getStyles); + const wrapperRef = useRef(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 handleFilterCollapse = useCallback( + (shouldCollapse: boolean, filtersLength: number) => () => { + debouncedSetActive(shouldCollapse, filtersLength); + }, + [debouncedSetActive] + ); + + useLayoutEffect(() => { + updateCollapseThreshold(!!model.state.collapseFilters ? true : false, filters.length); + // TODO: remove below before merging + // updateCollapseThreshold(true, filters.length); // for testing locally + + // needs to run only on first render + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return (
{ focusOnWipInputRef.current?.(); }} + ref={wrapperRef} + onFocusCapture={handleFilterCollapse(false, filters.length)} + // TODO: remove below before merging + // onBlurCapture={handleFilterCollapse(true, filters.length)} // for testing locally + onBlurCapture={handleFilterCollapse(!!model.state.collapseFilters ? true : false, filters.length)} > - {filters.map((filter, index) => ( + {(collapseThreshold ? filters.slice(0, collapseThreshold) : filters).map((filter, index) => ( ))} + {collapseThreshold ? ( + + {filters.slice(collapseThreshold).map((filter, i) => { + const keyLabel = filter.keyLabel ?? filter.key; + const valueLabel = filter.valueLabels?.join(', ') || filter.values?.join(', ') || filter.value; + return ( + + {keyLabel} {filter.operator} {valueLabel}
+
+ ); + })} +
+ } + > +
+{filters.length - collapseThreshold} filters
+ + ) : null} + {!readOnly ? : null} ); @@ -72,4 +132,17 @@ const getStyles = (theme: GrafanaTheme2) => ({ color: theme.colors.text.secondary, alignSelf: 'center', }), + basePill: css({ + display: 'flex', + alignItems: 'center', + background: theme.colors.action.selected, + border: `1px solid ${theme.colors.border.weak}`, + padding: theme.spacing(0.125, 1, 0.125, 1), + color: theme.colors.text.primary, + overflow: 'hidden', + whiteSpace: 'nowrap', + minHeight: theme.spacing(2.75), + ...theme.typography.bodySmall, + cursor: 'pointer', + }), }); diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/utils.ts b/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/utils.ts index 78f0078d9..327b5bb5a 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/utils.ts +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/utils.ts @@ -211,3 +211,28 @@ 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(); + + // without paddings variable wrapper height is 26px + // therefore trigger only when higher than that + if (rect.height - 6 > 26) { + const componentLineSpan = (rect.height - 6) / 26; + // magic number but dividing filters by line span +1 should yield + // number of filters that will fit in 1 line + 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;