Skip to content

refactor(console): extract reusable FilterMenu component from toolbar duplications #273

Description

@r2dedios

Problem

Five toolbar components share nearly identical dropdown filter logic (menu toggle, open/close state, keyboard handling, click-outside detection, selection callbacks). This results in ~2500 lines of duplicated code across:

File Lines Filters
console/src/app/Actions/AuditLogs/AuditLogsTableToolbar.tsx 609 Account, Provider, Action, Result, Requester
console/src/app/Actions/Scheduler/components/ActionsToolBar.tsx 731 Account, Provider, Action, Status
console/src/app/Clusters/components/ClustersTableToolbar.tsx 468 Cluster Name, Account Name, Status, Provider
console/src/app/Nodes/components/NodesTableToolbar.tsx 432 Instance Name, Provider, Status
console/src/app/Accounts/components/AccountsToolbar.tsx 313 Account Name, Provider

Each toolbar repeats the same pattern per dropdown filter:

  1. State: const [isMenuOpen, setIsMenuOpen] = useState(false)
  2. Refs: toggleRef, menuRef, containerRef (via usePopperContainer)
  3. Effect for keyboard/click-outside handlers (using refs to avoid stale closures)
  4. Effect to register/cleanup window event listeners
  5. Toggle click handler with stopPropagation + setTimeout focus
  6. Select handler (single-select or multi-select with badge count)
  7. JSX: MenuToggle + Menu + MenuContent + MenuList + MenuItem + Popper

This pattern is repeated 2-4 times per toolbar, totaling ~15 nearly identical instances across the codebase.

Proposed Solution

Option A: Reusable FilterMenuSelect component

Create console/src/app/components/common/FilterMenuSelect.tsx:

interface FilterMenuSelectProps<T extends string> {
  label: string;              // "Filter by provider"
  options: { id: T; label: string }[];
  selected: T | T[] | null;  // single or multi-select
  onSelect: (value: T) => void;
  onClear?: () => void;
  isMulti?: boolean;          // multi-select with badge count
  width?: string;             // default "200px"
}

This component encapsulates:

  • All open/close state management
  • Keyboard (Escape/Tab) and click-outside handlers
  • Popper positioning via usePopperContainer
  • Single-select (close on pick) vs multi-select (stay open, show badge) behavior
  • The full MenuTogglePopperMenuMenuListMenuItem JSX tree

Option B: useFilterMenu() hook + slim wrapper

If the JSX customization varies too much between toolbars, extract a hook instead:

const { isOpen, toggleRef, menuRef, containerRef, containerElement, onToggleClick, getSortParams } = useFilterMenu();

This handles state, refs, and event listeners — each toolbar still renders its own JSX but without the boilerplate.

Recommendation

Option A covers all current use cases. The only variation across toolbars is the list of options and single vs multi-select — both handled by props. Start with one toolbar (e.g., AccountsToolbar — smallest at 313 lines), validate, then migrate the others.

Expected Impact

  • ~1500-1800 lines removed across 5 files
  • Each toolbar reduced to ~50-150 lines (just filter composition + search inputs)
  • Single place to fix menu accessibility or behavior bugs

Files to Modify

  • New: console/src/app/components/common/FilterMenuSelect.tsx
  • Modify: All 5 toolbar files listed above
  • Possibly modify: console/src/app/hooks/usePopperContainer.ts (may be absorbed into the new component)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions