diff --git a/frontend/src/ui-components/AcmTable/AcmTable.tsx b/frontend/src/ui-components/AcmTable/AcmTable.tsx index 5e8929ee46..7de886c7e1 100644 --- a/frontend/src/ui-components/AcmTable/AcmTable.tsx +++ b/frontend/src/ui-components/AcmTable/AcmTable.tsx @@ -56,6 +56,7 @@ import { debounce } from 'debounce' import Fuse from 'fuse.js' import get from 'get-value' import { + cloneElement, createContext, FormEvent, Fragment, @@ -85,6 +86,9 @@ type SortFn = (a: T, b: T) => number type CellFn = (item: T, search: string) => ReactNode type SearchFn = (item: T) => string | boolean | number | string[] | boolean[] | number[] +// when a filter has more then this many options, give it its own dropdown +const SPLIT_FILTER_THRESHOLD = 30 + /* istanbul ignore next */ export interface IAcmTableColumn { /** the header of the column */ @@ -247,6 +251,10 @@ export interface ITableFilter extends TableFilterBase { options: TableFilterOption[] showEmptyOptions?: boolean } +interface IValidFilters { + filter: ITableFilter + options: { option: TableFilterOption; count: number }[] +} export interface ITableAdvancedFilter extends TableFilterBase { availableOperators: SearchOperator[] @@ -1515,7 +1523,7 @@ export function AcmTable(props: AcmTableProps) { function TableColumnFilters( props: Readonly<{ id?: string; filters: ITableFilter[]; filterCounts: FilterCounts | undefined; items?: T[] }> ) { - const [isOpen, setIsOpen] = useState(false) + const [isOpen, setIsOpen] = useState([false]) const { id, filters, items, filterCounts } = props const { filterSelections, addFilterValue, removeFilterValue, removeFilter } = useTableFilterSelections({ id, @@ -1558,10 +1566,13 @@ function TableColumnFilters( }, [filterSelections]) const filterSelectGroups = useMemo(() => { - const validFilters: { - filter: ITableFilter - options: { option: TableFilterOption; count: number }[] - }[] = [] + const filterGroups = [ + { + allFilters: [] as ITableFilter[], + groupSelections: [] as FilterSelectOptionObject[], + validFilters: [] as IValidFilters[], + }, + ] for (const filter of filters) { const options: { option: TableFilterOption; count: number }[] = [] for (const option of filter.options) { @@ -1579,82 +1590,158 @@ function TableColumnFilters( options.push({ option, count: count ?? 0 }) } } + + // split filters up if some have more then SPLIT_FILTER_THRESHOLD options + // (like an environment with lots of clusters) + let group = filterGroups[0] /* istanbul ignore else */ if (options.length) { - validFilters.push({ filter, options }) + if (options.length > SPLIT_FILTER_THRESHOLD) { + filterGroups.push({ + allFilters: [] as ITableFilter[], + groupSelections: [] as FilterSelectOptionObject[], + validFilters: [] as IValidFilters[], + }) + group = filterGroups[filterGroups.length - 1] + } + group.validFilters.push({ filter, options }) } + group.allFilters.push(filter) + } + + // if user has made selections and there are multiple filters + // because some have lots of options, split the selections up by filter + filterGroups[0].groupSelections = selections + if (filterGroups.length > 1) { + let allSelections = [...selections] + filterGroups.forEach((group, inx) => { + if (inx !== 0) { + const remainingSelections = [] as FilterSelectOptionObject[] + filterGroups[inx].groupSelections = allSelections.filter((selected) => { + // there should only be one validFilter in extra filter dropdowns + // just for the type filter type (ex: cluster) in this dropdown + if (group.validFilters[0].filter.id !== selected.filterId) { + remainingSelections.push(selected) + return false + } + return true + }) + allSelections = remainingSelections + } + }) + filterGroups[0].groupSelections = allSelections } - return validFilters.map((filter) => ( - - {filter.options.map((option) => { - const key = `${filter.filter.id}-${option.option.value}` + return filterGroups.map(({ allFilters, groupSelections, validFilters }) => { + return { + groupFilters: allFilters, + groupSelections, + groupSelectionList: validFilters.map((filter) => { return ( - -
- {option.option.label} - - {option.count} - -
-
+ + {filter.options.map((option) => { + const key = `${filter.filter.id}-${option.option.value}` + return ( + +
+ {option.option.label} + + {option.count} + +
+
+ ) + })} +
) - })} -
- )) + }), + } + }) }, [filterCounts, filters, items, selections]) + const onFilterOptions = useCallback( + (_: any, textInput: string, inx: number) => { + const options = filterSelectGroups[inx].groupSelectionList + if (textInput === '') { + return options + } else { + const filteredGroups = options + .map((group) => { + const filteredGroup = cloneElement(group, { + children: group.props.children.filter((item: { props: { value: { value: string } } }) => { + return item.props.value.value.toLowerCase().includes(textInput.toLowerCase()) + }), + }) + if (filteredGroup.props.children.length > 0) return filteredGroup + }) + .filter(Boolean) as JSX.Element[] + return filteredGroups + } + }, + [filterSelectGroups] + ) + return ( - {filters.reduce( - (acc, current) => ( - ) => { - const currentCategorySelected = filterSelections[current.id] ?? [] - return currentCategorySelected.includes(option.value) - }) - .map((option: TableFilterOption) => { - return { key: option.value, node: option.label } - })} - deleteChip={(_category, chip) => { - chip = chip as ToolbarChip - onDelete(current.id, chip) - }} - deleteChipGroup={() => onDeleteGroup(current.id)} - categoryName={current.label} - > - {acc} - - ), - - )} +
+ {filterSelectGroups.map(({ groupFilters, groupSelections, groupSelectionList }, inx) => { + return groupFilters.reduce( + (acc, current) => ( + ) => { + const currentCategorySelected = filterSelections[current.id] ?? [] + return currentCategorySelected.includes(option.value) + }) + .map((option: TableFilterOption) => { + return { key: option.value, node: option.label } + })} + deleteChip={(_category, chip) => { + chip = chip as ToolbarChip + onDelete(current.id, chip) + }} + deleteChipGroup={() => onDeleteGroup(current.id)} + categoryName={current.label} + > + {acc} + + ), + + ) + })} +
) }