From fc0bcd320479625f167cf6dbb603833d584ff057 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 30 Apr 2026 14:02:33 +0100 Subject: [PATCH] Use full-width workloads filter deck --- .../subsystems/frontend-primitives.md | 6 +- .../subsystems/performance-and-scalability.md | 9 ++- .../components/Workloads/WorkloadsFilter.tsx | 10 ++- .../__tests__/WorkloadsFilter.test.tsx | 19 +++-- .../shared/PageControls.guardrails.test.ts | 4 + .../components/shared/PageControls.test.tsx | 35 +++++++++ .../src/components/shared/PageControls.tsx | 73 +++++++++++-------- 7 files changed, 110 insertions(+), 46 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 5e7e141a7..5447072c7 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -369,7 +369,11 @@ work extends shared components instead of creating new local variants. filter surfaces may opt into the `PageControls` stacked action layout so display, chart, column, and reset actions occupy their own compact grouped row instead of splitting the filter row into left/right zones or creating a - full-width form-like divider. Narrow consumers such as + full-width form-like divider. When a dense surface would otherwise read as a + small form floating in a mostly empty full-width panel, it should route the + filter rows and trailing actions through `PageControls` `controlDeckClass` + so the controls read as one compact command deck instead of unrelated + left-aligned fragments. Narrow consumers such as `ColumnPicker` must opt into their panel width through that primitive rather than layering competing width classes page by page. 4. Add guardrail tests when a new shared pattern is introduced. diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 8ac938f26..c01e60746 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -200,10 +200,11 @@ regression protection. 17. Extend workload control defaults, persistent view preferences, keyboard reset behavior, column-visibility ownership, and tag-search flow through `frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts` and `frontend-modern/src/components/Workloads/workloadsFilterModel.ts` rather than rebuilding sort/search/grouping state, reset drift, or column-toggle plumbing inside `frontend-modern/src/components/Workloads/useWorkloadsState.ts` 18. Extend workload filter active-count, reset semantics, and mobile toolbar state through `frontend-modern/src/components/Workloads/workloadsFilterModel.ts` and `frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts`, rather than rebuilding filter-local state inside `frontend-modern/src/components/Workloads/WorkloadsFilter.tsx` Workloads filter presentation may consume frontend-primitives responsive - toggle controls, but the workload-owned state model must remain the source - of truth for active filter counting, reset behavior, and mobile collapse so - changing between wide toggle and narrow select presentation does not add a - second filter state path. + toggle controls and the shared compact control-deck wrapper, but the + workload-owned state model must remain the source of truth for active + filter counting, reset behavior, and mobile collapse so changing between + wide toggle and narrow select presentation does not add a second filter + state path or layout-measurement path on the workload hot path. 19. Extend threshold-slider value-position math, title/label derivation, and drag scroll-lock runtime through `frontend-modern/src/components/Workloads/thresholdSliderModel.ts` and `frontend-modern/src/components/Workloads/useThresholdSliderState.ts` rather than rebuilding slider-local state and pointer lifecycle inside `frontend-modern/src/components/Workloads/ThresholdSlider.tsx` 20. Extend stacked disk-bar capacity math, segment/tooltip derivation, and resize-observer runtime through `frontend-modern/src/components/Workloads/stackedDiskBarModel.ts` and `frontend-modern/src/components/Workloads/useStackedDiskBarState.ts` rather than rebuilding disk-bar-local state, mode branching, and tooltip shaping inside `frontend-modern/src/components/Workloads/StackedDiskBar.tsx` 21. Extend stacked memory-bar capacity math, balloon/swap derivation, and resize-observer runtime through `frontend-modern/src/components/Workloads/stackedMemoryBarModel.ts` and `frontend-modern/src/components/Workloads/useStackedMemoryBarState.ts` rather than rebuilding memory-bar-local state, tooltip shaping, and label-fit logic inside `frontend-modern/src/components/Workloads/StackedMemoryBar.tsx` diff --git a/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx b/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx index 4ca341bad..668a4b698 100644 --- a/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx @@ -77,7 +77,9 @@ export const WorkloadsFilter: Component = (props) => { }} showFilters={showToolbarFilters()} actionsLayout="stacked" - toolbarActionsClass="page-controls-toolbar-actions inline-flex max-w-full flex-wrap items-center gap-2 rounded-md bg-surface-hover p-0.5" + controlDeckClass="workloads-filter-control-deck inline-flex max-w-full flex-col items-start gap-1 rounded-md border border-border-subtle bg-surface-alt/60 p-1" + filterControlsClass="page-controls-filter-controls inline-flex max-w-full min-w-0" + toolbarActionsClass="page-controls-toolbar-actions inline-flex max-w-full flex-wrap items-center gap-2 pt-0.5" toolbarTrailing={ <> = (props) => { } > -
-
+
+
= (props) => {
-
+
{(hostFilter) => ( { const { container } = render(() => ); expect(screen.getByTestId('column-picker')).toBeInTheDocument(); - const primaryControls = container.querySelector('.workloads-filter-primary-controls'); - const secondaryControls = container.querySelector('.workloads-filter-secondary-controls'); + const primaryControls = container.querySelector( + '.workloads-filter-primary-controls', + ); + const secondaryControls = container.querySelector( + '.workloads-filter-secondary-controls', + ); + const controlDeck = container.querySelector('.workloads-filter-control-deck'); expect(primaryControls).not.toBeNull(); expect(secondaryControls).not.toBeNull(); + expect(controlDeck).not.toBeNull(); expect(primaryControls!).toContainElement(screen.getByRole('group', { name: 'Type' })); expect(primaryControls!).toContainElement(screen.getByRole('group', { name: 'Status' })); expect(secondaryControls!).toContainElement( @@ -187,10 +193,13 @@ describe('WorkloadsFilter', () => { expect(secondaryControls!.compareDocumentPosition(primaryControls!)).toBe( Node.DOCUMENT_POSITION_PRECEDING, ); - const toolbarActions = container.querySelector('.page-controls-toolbar-actions'); + const toolbarActions = container.querySelector('.page-controls-toolbar-actions'); expect(toolbarActions).not.toBeNull(); - expect(toolbarActions!).toHaveClass('rounded-md'); - expect(toolbarActions!).toHaveClass('bg-surface-hover'); + expect(controlDeck!).toHaveClass('inline-flex'); + expect(controlDeck!).toHaveClass('bg-surface-alt/60'); + expect(controlDeck!).toContainElement(toolbarActions!); + expect(controlDeck!).toContainElement(primaryControls!); + expect(toolbarActions!).toHaveClass('pt-0.5'); expect(toolbarActions!).not.toHaveClass('ml-auto'); expect(toolbarActions!).not.toHaveClass('border-t'); expect(toolbarActions!).toContainElement(screen.getByText('Grouped')); diff --git a/frontend-modern/src/components/shared/PageControls.guardrails.test.ts b/frontend-modern/src/components/shared/PageControls.guardrails.test.ts index 5b8676e62..a95ba0666 100644 --- a/frontend-modern/src/components/shared/PageControls.guardrails.test.ts +++ b/frontend-modern/src/components/shared/PageControls.guardrails.test.ts @@ -130,6 +130,8 @@ describe('page controls guardrails', () => { expect(pageControlsSource).toContain('page-controls-filter-controls'); expect(pageControlsSource).toContain('page-controls-toolbar-actions ml-auto'); expect(pageControlsSource).toContain('actionsLayout?:'); + expect(pageControlsSource).toContain('controlDeckClass?:'); + expect(pageControlsSource).toContain('const toolbarControls = () => ('); expect(pageControlsSource).toContain("actionsLayout() === 'stacked'"); expect(pageControlsSource).toContain( 'shrink-0 flex-wrap items-center justify-end gap-2 self-start', @@ -149,6 +151,8 @@ describe('page controls guardrails', () => { expect(workloadsFilterSource).toContain('WORKLOAD_STATUS_FILTER_OPTIONS'); expect(workloadsFilterSource).toContain('workloads-filter-primary-controls'); expect(workloadsFilterSource).toContain('workloads-filter-secondary-controls'); + expect(workloadsFilterSource).toContain('workloads-filter-control-deck'); + expect(workloadsFilterSource).toContain('controlDeckClass='); expect(workloadsFilterSource).toContain('actionsLayout="stacked"'); expect(workloadsFilterSource).toContain('page-controls-toolbar-actions inline-flex'); expect(workloadsFilterSource).toContain('xl:flex-col xl:items-start'); diff --git a/frontend-modern/src/components/shared/PageControls.test.tsx b/frontend-modern/src/components/shared/PageControls.test.tsx index 6ac0be29c..b2437f833 100644 --- a/frontend-modern/src/components/shared/PageControls.test.tsx +++ b/frontend-modern/src/components/shared/PageControls.test.tsx @@ -132,4 +132,39 @@ describe('PageControls', () => { expect(filterControls!).not.toContainElement(columnsButton); expect(screen.queryByTestId('desktop-utility')).not.toBeInTheDocument(); }); + + it('can wrap filters and trailing actions in one compact control deck', () => { + render(() => ( + Search
} + showFilters={true} + actionsLayout="stacked" + controlDeckClass="page-controls-control-deck" + filterControlsClass="page-controls-filter-controls compact-filters" + toolbarActionsClass="page-controls-toolbar-actions compact-actions" + toolbarTrailing={
Grouped List
} + columnVisibility={{ + availableToggles: () => [{ id: 'subject', label: 'Subject' }], + isHiddenByUser: () => false, + toggle: vi.fn(), + resetToDefaults: vi.fn(), + }} + > +
Filters
+ + )); + + const deck = screen + .getByText('Filters', { selector: 'div' }) + .closest('.page-controls-control-deck'); + const trailing = screen.getByTestId('toolbar-trailing'); + const columnsButton = screen.getByRole('button', { name: /columns/i }); + + expect(deck).not.toBeNull(); + expect(deck!).toContainElement(screen.getByText('Filters', { selector: 'div' })); + expect(deck!).toContainElement(trailing); + expect(deck!).toContainElement(columnsButton); + expect(deck!.querySelector('.page-controls-filter-controls')).toHaveClass('compact-filters'); + expect(deck!.querySelector('.page-controls-toolbar-actions')).toHaveClass('compact-actions'); + }); }); diff --git a/frontend-modern/src/components/shared/PageControls.tsx b/frontend-modern/src/components/shared/PageControls.tsx index 20b319dc5..edee809b4 100644 --- a/frontend-modern/src/components/shared/PageControls.tsx +++ b/frontend-modern/src/components/shared/PageControls.tsx @@ -48,6 +48,7 @@ interface PageControlsProps extends JSX.HTMLAttributes { resetAction?: PageControlsResetAction; mobileFilters?: PageControlsMobileFilters; actionsLayout?: 'inline' | 'stacked'; + controlDeckClass?: string; filterControlsClass?: string; toolbarActionsClass?: string; } @@ -71,6 +72,7 @@ export const PageControls: Component = (props) => { 'resetAction', 'mobileFilters', 'actionsLayout', + 'controlDeckClass', 'filterControlsClass', 'toolbarActionsClass', 'class', @@ -108,6 +110,42 @@ export const PageControls: Component = (props) => { (actionsLayout() === 'stacked' ? 'page-controls-toolbar-actions inline-flex w-full flex-wrap items-center gap-2 border-t border-border-subtle pt-2' : 'page-controls-toolbar-actions ml-auto inline-flex shrink-0 flex-wrap items-center justify-end gap-2 self-start'); + const toolbarControls = () => ( + <> +
{local.children}
+ +
+ {local.toolbarTrailing} + + + + {activeUtilityActions()} + + + + + + + + + + local.resetAction?.onClick()} + title={local.resetAction?.title} + class={local.resetAction?.class} + > + {local.resetAction?.icon} + {local.resetAction?.label ?? 'Reset'} + + +
+ + ); return ( = (props) => { class={local.class} > -
{local.children}
- -
- {local.toolbarTrailing} - - - - {activeUtilityActions()} - - - - - - - - - - local.resetAction?.onClick()} - title={local.resetAction?.title} - class={local.resetAction?.class} - > - {local.resetAction?.icon} - {local.resetAction?.label ?? 'Reset'} - - -
+ + {(controlDeckClass) =>
{toolbarControls()}
} +
{local.children}