align Add filter controls

This commit is contained in:
rcourtman 2026-08-03 14:50:42 +01:00
parent 37dfdf95ad
commit 3afc8bda15
7 changed files with 69 additions and 21 deletions

View file

@ -3898,6 +3898,11 @@ not need a feature-owned reset. Route-owned or multi-facet platform surfaces
may supply one composite reset, which the toolbar delegates exactly once;
otherwise the shared `FilterBar` fallback clears its search and inline status
catalog without forcing every platform table to repeat reset plumbing.
Platform table toolbars also use the compact Add filter variant: the visible
uppercase field label and labelled-field shell stay hidden while the native
select retains its accessible `Filter` name. Provider surfaces must inherit
that treatment from `PlatformTableToolbar` instead of choosing different
label and width chrome page by page.
Primary filters with small, stable option sets should stay one-click controls
inside that same `FilterDef[]` catalog by setting `inline: true`; `FilterBar`
renders those as unlabeled compact segmented controls in the same second-row
@ -3965,10 +3970,10 @@ The Add filter select and adjacent action/popover triggers share the canonical
`filterToolbarControlClass` height. When the Add filter label is visually
hidden, its `FormSelect` must also omit the labelled group's outer padded/ring
shell so the native select does not become a double-framed, oversized control;
its extra width remains intentional space for dynamic filter-value labels. The
visible `Add filter` value is a disabled, hidden placeholder, not an actionable
option repeated at the top of the opened native list; only real filter values
belong in that choice list.
the select uses one stable compact width instead of expanding to its longest
hidden option. The visible `Add filter` value is a disabled, hidden
placeholder, not an actionable option repeated at the top of the opened native
list; only real filter values belong in that choice list.
Counts or orientation strips presented as part of a filtered resource table
must derive from that table's canonical filtered collection as well. They must
not continue showing page-wide inventory totals after FilterBar state, saved

View file

@ -1,15 +1,17 @@
{
"version": 1,
"base_sha": "e3fdfc98b413db1b29b18cb65bb18bd10bdd3e18",
"verified_at": "2026-08-03T13:09:09Z",
"base_sha": "37dfdf95ad4d9136973133006b03e45a421884c1",
"verified_at": "2026-08-03T13:38:29Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/shared/FilterBar/AddFilterMenu.tsx",
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx"
],
"content_sha256": {
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "b94fdef76a18a9ef95f393f11445b8da613593064e03260c9349f804d39fb2fb"
"frontend-modern/src/components/shared/FilterBar/AddFilterMenu.tsx": "b9be43892868440a5cd08a29e730f9ed67cf8f7bc0997d1af1b600d6a1d42bf8",
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "42d877508eaac7af7b90c97ceac3cc279554cff019981518ab77e940b24ec2bd"
},
"routes": ["/truenas/overview", "/kubernetes/overview", "/vmware/networks"],
"routes": ["/docker/overview", "/kubernetes/workloads"],
"viewports": [
{
"width": 1280,
@ -21,18 +23,18 @@
}
],
"states": [
"Desktop TrueNAS alert toolbar with default, search-only, and combined search/status state",
"Phone-width TrueNAS alert toolbar with collapsed and expanded filter rails",
"Desktop Kubernetes overview with URL-owned combined search/status state",
"Desktop vSphere networks with table-local search state"
"Desktop Docker container toolbar before and after applying its longest host filter",
"Desktop Kubernetes workload toolbar with dynamic cluster and namespace filters available",
"Phone-width Docker toolbar with the filter rail collapsed, expanded, and host-filtered",
"Phone-width Kubernetes workload toolbar with the filter rail expanded"
],
"interactions": [
"Entered TrueNAS alert search text and confirmed contextual Clear filters appeared without requiring table-specific reset plumbing",
"Selected Warning with search active and confirmed one Clear filters action restored blank search and All status",
"Expanded the phone-width TrueNAS filter rail and confirmed Clear filters and the result count aligned without clipping or overflow",
"Cleared phone-width search state and confirmed the Clear action disappeared while the status default remained selected",
"Loaded Kubernetes overview with ?q=payments&status=degraded, confirmed both controls hydrated, and verified its feature-owned composite reset returned the exact default route",
"Entered and cleared vSphere network search text to confirm the shared fallback applies across another table-local platform consumer",
"Read the browser console after the final cross-platform pass and confirmed no warnings or errors"
"Measured Docker and Kubernetes Add filter controls at 120 by 28 pixels beside the 95 by 28 pixel Saved trigger instead of the previous 204-pixel content-expanded select",
"Confirmed platform toolbars no longer render the redundant visible uppercase Filter label while the select retains its accessible Filter name",
"Inspected the native filter option projection and confirmed the disabled Add filter placeholder is absent from its actionable option list",
"Applied the longest Docker host option, confirmed the compact control gave way to a host chip, Clear filters, and the filtered 14 of 70 result count without overflow",
"Cleared the Docker host filter and confirmed the chip and Clear action disappeared, the Add filter control returned, and the canonical route stayed unchanged",
"Expanded the Docker and Kubernetes phone-width filter rails and confirmed Add filter, Saved, View, Clear filters, chips, and result counts wrapped without clipping",
"Read the browser console after the final desktop and responsive pass and confirmed no warnings or errors"
]
}

View file

@ -113,7 +113,7 @@ export const AddFilterMenu: Component<AddFilterMenuProps> = (props) => {
fieldBaseClass={fieldBaseClass()}
labelClass={props.showLabel === false ? 'sr-only' : filterLabelClass}
selectBaseClass={`${filterToolbarControlClass} border border-transparent bg-surface-hover px-2 text-muted outline-none ring-1 ring-border-subtle transition-colors hover:bg-surface hover:text-base-content focus:border-blue-500`}
selectClass="min-w-[7.5rem]"
selectClass="w-[7.5rem]"
value=""
onChange={handleChange}
aria-label="Filter"

View file

@ -264,6 +264,8 @@ describe('FilterBar', () => {
const label = document.querySelector(`label[for="${select.id}"]`);
expect(label).toHaveClass('sr-only');
expect(select).toHaveClass('h-7');
expect(select).toHaveClass('w-[7.5rem]');
expect(select).not.toHaveClass('min-w-[7.5rem]');
expect(select.parentElement).not.toHaveClass('p-0.5');
});
});

View file

@ -8185,7 +8185,8 @@ describe('shared primitive guardrails', () => {
expect(addFilterMenuSource).toContain('option.value !== filter.defaultValue');
expect(addFilterMenuSource).toContain('GROUP_ORDER');
expect(addFilterMenuSource).toContain('filterGroupClass');
expect(addFilterMenuSource).toContain('min-w-[7.5rem]');
expect(addFilterMenuSource).toContain('selectClass="w-[7.5rem]"');
expect(addFilterMenuSource).not.toContain('min-w-[7.5rem]');
expect(addFilterMenuSource).toContain('bg-surface-hover');
expect(addFilterMenuSource).toContain('ring-border-subtle');
expect(addFilterMenuSource).toContain('aria-label="Filter"');
@ -8249,6 +8250,7 @@ describe('shared primitive guardrails', () => {
expect(filterBarSource).toContain('aria-label="Filter actions"');
expect(sharedPlatformPageSource).toContain('viewOptions?: JSX.Element;');
expect(sharedPlatformPageSource).toContain('viewOptions={props.viewOptions}');
expect(sharedPlatformPageSource).toContain('showAddFilterLabel={false}');
expect(sharedPlatformPageSource).toContain('trailingControls={');
expect(sharedPlatformPageSource).not.toContain('ViewOptionsMenu');
expect(sharedPlatformPageSource).toContain(

View file

@ -281,6 +281,42 @@ describe('filterPlatformResources', () => {
});
describe('PlatformTableToolbar', () => {
it('keeps the Add filter control compact without losing its accessible label', () => {
const [node, setNode] = createSignal('all');
render(() =>
PlatformTableToolbar({
search: () => '',
onSearchChange: () => undefined,
searchPlaceholder: 'Search rows',
status: 'all',
onStatusChange: () => undefined,
statusOptions: [{ value: 'all', label: 'All' }],
filters: [
{
id: 'node',
label: 'Node',
options: () => [
{ value: 'all', label: 'All' },
{ value: 'pve1', label: 'pve1' },
],
value: node,
setValue: setNode,
defaultValue: 'all',
},
],
visible: 2,
total: 3,
rowNoun: 'rows',
}),
);
const select = screen.getByRole('combobox', { name: 'Filter' });
const label = document.querySelector(`label[for="${select.id}"]`);
expect(label).toHaveClass('sr-only');
expect(select.parentElement).not.toHaveClass('p-0.5');
});
it('shows and clears search-only state without requiring a custom reset owner', () => {
const [search, setSearch] = createSignal('archive');

View file

@ -1058,6 +1058,7 @@ export function PlatformTableToolbar<T extends string | number>(props: {
tips: props.searchTips,
}}
filters={allFilters}
showAddFilterLabel={false}
savedViewsKey={props.savedViewsKey}
viewOptions={props.viewOptions}
trailingControls={