mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-20 01:01:20 +00:00
Use full-width workloads filter deck
This commit is contained in:
parent
c7164c2906
commit
fc0bcd3204
7 changed files with 110 additions and 46 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ export const WorkloadsFilter: Component<WorkloadsFilterProps> = (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={
|
||||
<>
|
||||
<GroupedTableModeSegmentedControl
|
||||
|
|
@ -94,8 +96,8 @@ export const WorkloadsFilter: Component<WorkloadsFilterProps> = (props) => {
|
|||
</>
|
||||
}
|
||||
>
|
||||
<div class="workloads-filter-control-stack flex w-full min-w-0 flex-col gap-1.5">
|
||||
<div class="workloads-filter-primary-controls flex min-w-0 flex-wrap items-center gap-1.5 xl:flex-col xl:items-start">
|
||||
<div class="workloads-filter-control-stack inline-flex max-w-full min-w-0 flex-col items-start gap-1">
|
||||
<div class="workloads-filter-primary-controls flex min-w-0 flex-wrap items-center gap-1 xl:flex-col xl:items-start">
|
||||
<LabeledFilterToggleGroup
|
||||
id="workloads-type-filter"
|
||||
label="Type"
|
||||
|
|
@ -117,7 +119,7 @@ export const WorkloadsFilter: Component<WorkloadsFilterProps> = (props) => {
|
|||
</div>
|
||||
|
||||
<Show when={hasSecondaryFilters()}>
|
||||
<div class="workloads-filter-secondary-controls flex min-w-0 flex-wrap items-center gap-2 rounded-md bg-surface-alt/60 p-1">
|
||||
<div class="workloads-filter-secondary-controls flex max-w-full min-w-0 flex-wrap items-center gap-1">
|
||||
<Show when={props.hostFilter}>
|
||||
{(hostFilter) => (
|
||||
<LabeledFilterSelect
|
||||
|
|
|
|||
|
|
@ -169,10 +169,16 @@ describe('WorkloadsFilter', () => {
|
|||
const { container } = render(() => <WorkloadsFilter {...props} />);
|
||||
|
||||
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<HTMLElement>(
|
||||
'.workloads-filter-primary-controls',
|
||||
);
|
||||
const secondaryControls = container.querySelector<HTMLElement>(
|
||||
'.workloads-filter-secondary-controls',
|
||||
);
|
||||
const controlDeck = container.querySelector<HTMLElement>('.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<HTMLElement>('.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'));
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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(() => (
|
||||
<PageControls
|
||||
search={<div data-testid="search">Search</div>}
|
||||
showFilters={true}
|
||||
actionsLayout="stacked"
|
||||
controlDeckClass="page-controls-control-deck"
|
||||
filterControlsClass="page-controls-filter-controls compact-filters"
|
||||
toolbarActionsClass="page-controls-toolbar-actions compact-actions"
|
||||
toolbarTrailing={<div data-testid="toolbar-trailing">Grouped List</div>}
|
||||
columnVisibility={{
|
||||
availableToggles: () => [{ id: 'subject', label: 'Subject' }],
|
||||
isHiddenByUser: () => false,
|
||||
toggle: vi.fn(),
|
||||
resetToDefaults: vi.fn(),
|
||||
}}
|
||||
>
|
||||
<div>Filters</div>
|
||||
</PageControls>
|
||||
));
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ interface PageControlsProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|||
resetAction?: PageControlsResetAction;
|
||||
mobileFilters?: PageControlsMobileFilters;
|
||||
actionsLayout?: 'inline' | 'stacked';
|
||||
controlDeckClass?: string;
|
||||
filterControlsClass?: string;
|
||||
toolbarActionsClass?: string;
|
||||
}
|
||||
|
|
@ -71,6 +72,7 @@ export const PageControls: Component<PageControlsProps> = (props) => {
|
|||
'resetAction',
|
||||
'mobileFilters',
|
||||
'actionsLayout',
|
||||
'controlDeckClass',
|
||||
'filterControlsClass',
|
||||
'toolbarActionsClass',
|
||||
'class',
|
||||
|
|
@ -108,6 +110,42 @@ export const PageControls: Component<PageControlsProps> = (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 = () => (
|
||||
<>
|
||||
<div class={filterControlsClass()}>{local.children}</div>
|
||||
|
||||
<div class={toolbarActionsClass()}>
|
||||
<Show when={local.toolbarTrailing}>{local.toolbarTrailing}</Show>
|
||||
|
||||
<Show when={activeUtilityActions()}>
|
||||
<FilterDivider />
|
||||
{activeUtilityActions()}
|
||||
</Show>
|
||||
|
||||
<Show when={showColumnVisibility()}>
|
||||
<FilterDivider />
|
||||
<ColumnPicker
|
||||
columns={local.columnVisibility!.availableToggles()}
|
||||
isHidden={local.columnVisibility!.isHiddenByUser}
|
||||
onToggle={local.columnVisibility!.toggle}
|
||||
onReset={local.columnVisibility!.resetToDefaults}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={showResetAction()}>
|
||||
<FilterDivider />
|
||||
<FilterActionButton
|
||||
onClick={() => local.resetAction?.onClick()}
|
||||
title={local.resetAction?.title}
|
||||
class={local.resetAction?.class}
|
||||
>
|
||||
{local.resetAction?.icon}
|
||||
{local.resetAction?.label ?? 'Reset'}
|
||||
</FilterActionButton>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterHeader
|
||||
|
|
@ -125,38 +163,9 @@ export const PageControls: Component<PageControlsProps> = (props) => {
|
|||
class={local.class}
|
||||
>
|
||||
<Show when={hasTrailingActions()}>
|
||||
<div class={filterControlsClass()}>{local.children}</div>
|
||||
|
||||
<div class={toolbarActionsClass()}>
|
||||
<Show when={local.toolbarTrailing}>{local.toolbarTrailing}</Show>
|
||||
|
||||
<Show when={activeUtilityActions()}>
|
||||
<FilterDivider />
|
||||
{activeUtilityActions()}
|
||||
</Show>
|
||||
|
||||
<Show when={showColumnVisibility()}>
|
||||
<FilterDivider />
|
||||
<ColumnPicker
|
||||
columns={local.columnVisibility!.availableToggles()}
|
||||
isHidden={local.columnVisibility!.isHiddenByUser}
|
||||
onToggle={local.columnVisibility!.toggle}
|
||||
onReset={local.columnVisibility!.resetToDefaults}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={showResetAction()}>
|
||||
<FilterDivider />
|
||||
<FilterActionButton
|
||||
onClick={() => local.resetAction?.onClick()}
|
||||
title={local.resetAction?.title}
|
||||
class={local.resetAction?.class}
|
||||
>
|
||||
{local.resetAction?.icon}
|
||||
{local.resetAction?.label ?? 'Reset'}
|
||||
</FilterActionButton>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={local.controlDeckClass} fallback={toolbarControls()}>
|
||||
{(controlDeckClass) => <div class={controlDeckClass()}>{toolbarControls()}</div>}
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<Show when={!hasTrailingActions()}>{local.children}</Show>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue