mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-20 01:01:20 +00:00
Clarify charts toggle affordance
This commit is contained in:
parent
0bbba3b818
commit
f5d90f0fe5
13 changed files with 125 additions and 83 deletions
|
|
@ -341,7 +341,12 @@ work extends shared components instead of creating new local variants.
|
|||
of staying as loose filter-row children, and those controls must stay
|
||||
grouped when dense toolbars wrap so popovers remain viewport-safe instead of
|
||||
drifting off-screen from page-local flex behavior. Shared `FilterToolbarPanel` owns
|
||||
default filter-popover geometry, while narrow consumers such as
|
||||
default filter-popover geometry, and `FilterToolbar` owns the shared chart
|
||||
visibility display action: Workloads, Storage, Infrastructure, and future
|
||||
summary-bearing pages must use `ChartVisibilityToggleButton` so the
|
||||
affordance exposes one `Show charts` / `Hide charts` pressed-state contract
|
||||
instead of rebuilding a one-option segmented control or an in-summary
|
||||
collapse chevron page by page. 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.
|
||||
|
|
|
|||
|
|
@ -829,7 +829,10 @@ that clips trailing actions. The workload shell must route table display
|
|||
actions such as grouped/list mode and chart visibility through
|
||||
`PageControls.toolbarTrailing`, leaving route/filter selects as primary toolbar
|
||||
children so the display-action cluster wraps together with Columns/Reset across
|
||||
narrow desktop widths.
|
||||
narrow desktop widths. Workload chart visibility is a display preference, not
|
||||
an in-summary collapse affordance: the toolbar action must expose explicit
|
||||
`Show charts` / `Hide charts` pressed state, and hiding charts must remove the
|
||||
summary section rather than leaving an empty collapsed summary band on screen.
|
||||
The Workloads-owned filter-config assembly now lives in
|
||||
`frontend-modern/src/components/Workloads/useWorkloadsState.ts`, so future
|
||||
filter runtime changes must extend through those owners instead of
|
||||
|
|
|
|||
|
|
@ -102,7 +102,10 @@ state.
|
|||
by the Storage page model and exposed through the shared `PageControls`
|
||||
trailing action rail. Storage must keep the charts toggle, column picker,
|
||||
and reset affordance on that shared rail so the Workloads and Storage
|
||||
filter sections collapse through the same primitive contract.
|
||||
filter sections collapse through the same primitive contract. The charts
|
||||
toggle must read as an explicit `Show charts` / `Hide charts` pressed
|
||||
display action, and the off state must remove the summary section fully
|
||||
instead of leaving a collapsed summary shell in the interface.
|
||||
Ceph table shells on the storage route share the same frontend-primitives
|
||||
table contract: `frontend-modern/src/pages/Ceph.tsx` may own Ceph-specific
|
||||
columns and rows, but horizontal overflow and scrollbar hiding must route
|
||||
|
|
|
|||
|
|
@ -365,6 +365,11 @@ AI-only summary payloads, or page-local heuristics.
|
|||
the row may highlight in place through the shared active-resource id; if it
|
||||
is off-screen, the page must offer an explicit `Jump to row` affordance
|
||||
rather than auto-scrolling or collapsing the table on hover.
|
||||
12a. Keep infrastructure summary visibility as a page-display preference, not a
|
||||
unified-resource filter. `frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx`
|
||||
may hide or restore the summary section through the shared
|
||||
`ChartVisibilityToggleButton`, but that control must not mutate resource
|
||||
identity, table membership, route-backed focus, or summary-hover scope.
|
||||
13. Keep infrastructure cluster headers as canonical summary scope. Grouped
|
||||
headers in `frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx`
|
||||
must publish cluster scope from the same `ResourceGroup` / unified-resource
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Component, For, Show, type JSX } from 'solid-js';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import {
|
||||
ChartVisibilityToggleButton,
|
||||
FilterDivider,
|
||||
FilterSegmentedControl,
|
||||
LabeledFilterSelect,
|
||||
|
|
@ -115,31 +116,9 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
|
|||
});
|
||||
const chartsToolbarAction = () =>
|
||||
props.onChartsToggle ? (
|
||||
<FilterSegmentedControl
|
||||
class="hidden lg:inline-flex"
|
||||
value={props.chartsCollapsed?.() ? 'hidden' : 'shown'}
|
||||
onChange={() => props.onChartsToggle?.()}
|
||||
aria-label="Charts"
|
||||
options={[
|
||||
{
|
||||
value: 'shown',
|
||||
title: props.chartsCollapsed?.() ? 'Show charts' : 'Hide charts',
|
||||
label: (
|
||||
<>
|
||||
<svg
|
||||
class="w-3 h-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</svg>
|
||||
Charts
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
<ChartVisibilityToggleButton
|
||||
collapsed={props.chartsCollapsed?.() ?? false}
|
||||
onToggle={() => props.onChartsToggle?.()}
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -151,8 +151,11 @@ describe('StorageControls', () => {
|
|||
/>
|
||||
));
|
||||
|
||||
const chartsButton = screen.getByRole('button', { name: /charts/i });
|
||||
const chartsButton = screen.getByRole('button', { name: 'Hide charts' });
|
||||
expect(chartsButton.closest('.page-controls-toolbar-actions')).not.toBeNull();
|
||||
expect(chartsButton).toHaveTextContent('Charts');
|
||||
expect(chartsButton).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(chartsButton).toHaveAttribute('title', 'Hide charts');
|
||||
|
||||
fireEvent.click(chartsButton);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { Component, For, Show } from 'solid-js';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { FilterSegmentedControl, LabeledFilterSelect } from '@/components/shared/FilterToolbar';
|
||||
import {
|
||||
ChartVisibilityToggleButton,
|
||||
LabeledFilterSelect,
|
||||
} from '@/components/shared/FilterToolbar';
|
||||
import { GroupedTableModeSegmentedControl } from '@/components/shared/GroupedTableModeSegmentedControl';
|
||||
import { PageControls } from '@/components/shared/PageControls';
|
||||
import { SearchInput } from '@/components/shared/SearchInput';
|
||||
|
|
@ -72,31 +75,9 @@ export const WorkloadsFilter: Component<WorkloadsFilterProps> = (props) => {
|
|||
/>
|
||||
|
||||
<Show when={props.onChartsToggle}>
|
||||
<FilterSegmentedControl
|
||||
class="hidden lg:inline-flex"
|
||||
value={props.chartsCollapsed?.() ? 'hidden' : 'shown'}
|
||||
onChange={() => props.onChartsToggle?.()}
|
||||
aria-label="Charts"
|
||||
options={[
|
||||
{
|
||||
value: 'shown',
|
||||
title: props.chartsCollapsed?.() ? 'Show charts' : 'Hide charts',
|
||||
label: (
|
||||
<>
|
||||
<svg
|
||||
class="w-3 h-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</svg>
|
||||
Charts
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
<ChartVisibilityToggleButton
|
||||
collapsed={props.chartsCollapsed?.() ?? false}
|
||||
onToggle={() => props.onChartsToggle?.()}
|
||||
/>
|
||||
</Show>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -557,7 +557,23 @@ describe('WorkloadsFilter', () => {
|
|||
onChartsToggle: vi.fn(),
|
||||
});
|
||||
render(() => <WorkloadsFilter {...props} />);
|
||||
expect(screen.getByText('Charts')).toBeInTheDocument();
|
||||
const chartsButton = screen.getByRole('button', { name: 'Hide charts' });
|
||||
expect(chartsButton).toBeInTheDocument();
|
||||
expect(chartsButton).toHaveTextContent('Charts');
|
||||
expect(chartsButton).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(chartsButton).toHaveAttribute('title', 'Hide charts');
|
||||
});
|
||||
|
||||
it('labels the Charts button as a show action when charts are collapsed', () => {
|
||||
const props = makeProps({
|
||||
chartsCollapsed: vi.fn(() => true),
|
||||
onChartsToggle: vi.fn(),
|
||||
});
|
||||
render(() => <WorkloadsFilter {...props} />);
|
||||
const chartsButton = screen.getByRole('button', { name: 'Show charts' });
|
||||
expect(chartsButton).toHaveTextContent('Charts');
|
||||
expect(chartsButton).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(chartsButton).toHaveAttribute('title', 'Show charts');
|
||||
});
|
||||
|
||||
it('does not render Charts button when onChartsToggle is not provided', () => {
|
||||
|
|
@ -573,7 +589,7 @@ describe('WorkloadsFilter', () => {
|
|||
onChartsToggle,
|
||||
});
|
||||
render(() => <WorkloadsFilter {...props} />);
|
||||
fireEvent.click(screen.getByText('Charts'));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Hide charts' }));
|
||||
expect(onChartsToggle).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { cleanup, render, screen, waitFor } from '@solidjs/testing-library';
|
|||
import { For, createSignal } from 'solid-js';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
ChartVisibilityToggleButton,
|
||||
FilterHeader,
|
||||
FilterToolbarPanel,
|
||||
FilterSegmentedControl,
|
||||
|
|
@ -49,6 +50,34 @@ describe('FilterHeader', () => {
|
|||
expect(onChange).toHaveBeenCalledWith('warnings');
|
||||
});
|
||||
|
||||
it('keeps chart visibility as an explicit display toggle action', () => {
|
||||
const [collapsed, setCollapsed] = createSignal(false);
|
||||
const onToggle = vi.fn(() => setCollapsed((current) => !current));
|
||||
|
||||
render(() => (
|
||||
<ChartVisibilityToggleButton
|
||||
collapsed={collapsed()}
|
||||
onToggle={onToggle}
|
||||
data-testid="charts-toggle"
|
||||
/>
|
||||
));
|
||||
|
||||
const hideButton = screen.getByRole('button', { name: 'Hide charts' });
|
||||
expect(hideButton).toHaveTextContent('Charts');
|
||||
expect(hideButton).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(hideButton).toHaveAttribute('title', 'Hide charts');
|
||||
expect(hideButton).toHaveClass('hidden');
|
||||
expect(hideButton).toHaveClass('lg:inline-flex');
|
||||
|
||||
hideButton.click();
|
||||
|
||||
const showButton = screen.getByRole('button', { name: 'Show charts' });
|
||||
expect(onToggle).toHaveBeenCalledTimes(1);
|
||||
expect(showButton).toHaveTextContent('Charts');
|
||||
expect(showButton).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(showButton).toHaveAttribute('title', 'Show charts');
|
||||
});
|
||||
|
||||
it('keeps shared toggle behavior on shell, runtime, and model owners', () => {
|
||||
expect(toggleSource).toContain('useToggleState');
|
||||
expect(toggleSource).toContain('getToggleTrackClass');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, For, JSX, Show, createEffect, onCleanup, splitProps } from 'solid-js';
|
||||
import BarChartIcon from 'lucide-solid/icons/bar-chart';
|
||||
import ListFilterIcon from 'lucide-solid/icons/list-filter';
|
||||
import { segmentedButtonClass } from '@/utils/segmentedButton';
|
||||
|
||||
|
|
@ -330,6 +331,33 @@ export const FilterActionButton: Component<FilterActionButtonProps> = (props) =>
|
|||
);
|
||||
};
|
||||
|
||||
interface ChartVisibilityToggleButtonProps extends Omit<
|
||||
JSX.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'aria-label' | 'aria-pressed' | 'children' | 'onClick' | 'title'
|
||||
> {
|
||||
collapsed: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export const ChartVisibilityToggleButton: Component<ChartVisibilityToggleButtonProps> = (props) => {
|
||||
const [local, buttonProps] = splitProps(props, ['collapsed', 'onToggle', 'class']);
|
||||
const label = () => (local.collapsed ? 'Show charts' : 'Hide charts');
|
||||
return (
|
||||
<FilterActionButton
|
||||
{...buttonProps}
|
||||
class={`hidden lg:inline-flex ${local.class ?? ''}`.trim()}
|
||||
active={!local.collapsed}
|
||||
aria-label={label()}
|
||||
aria-pressed={!local.collapsed}
|
||||
title={label()}
|
||||
onClick={() => local.onToggle()}
|
||||
>
|
||||
<BarChartIcon class="h-3 w-3" />
|
||||
Charts
|
||||
</FilterActionButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface FilterToolbarPanelProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
children: JSX.Element;
|
||||
widthClass?: string;
|
||||
|
|
|
|||
|
|
@ -120,6 +120,12 @@ describe('page controls guardrails', () => {
|
|||
it('keeps display controls and utility actions on the shared toolbar rail', () => {
|
||||
expect(workloadsFilterSource).toContain('toolbarTrailing={');
|
||||
expect(storageFilterSource).toContain('toolbarTrailing={');
|
||||
expect(workloadsFilterSource).toContain('ChartVisibilityToggleButton');
|
||||
expect(storageFilterSource).toContain('ChartVisibilityToggleButton');
|
||||
expect(infrastructurePageSurfaceSource).toContain('ChartVisibilityToggleButton');
|
||||
expect(workloadsFilterSource).not.toContain('aria-label="Charts"');
|
||||
expect(storageFilterSource).not.toContain('aria-label="Charts"');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('aria-label="Charts"');
|
||||
expect(recoveryHistorySectionSource).not.toContain('toolbarClass="lg:flex-nowrap"');
|
||||
expect(recoveryHistorySectionSource).not.toContain('ml-auto flex items-center gap-2');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { useNavigate } from '@solidjs/router';
|
|||
import { buildInfrastructureOnboardingPath } from '@/components/Settings/infrastructureWorkspaceModel';
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { FilterSegmentedControl, LabeledFilterSelect } from '@/components/shared/FilterToolbar';
|
||||
import {
|
||||
ChartVisibilityToggleButton,
|
||||
LabeledFilterSelect,
|
||||
} from '@/components/shared/FilterToolbar';
|
||||
import { GroupedTableModeSegmentedControl } from '@/components/shared/GroupedTableModeSegmentedControl';
|
||||
import { PageControls } from '@/components/shared/PageControls';
|
||||
import { PageHeader } from '@/components/shared/PageHeader';
|
||||
|
|
@ -235,31 +238,9 @@ export function InfrastructurePageSurface() {
|
|||
onChange={(value) => setGroupingMode(value as GroupingMode)}
|
||||
/>
|
||||
|
||||
<FilterSegmentedControl
|
||||
class="hidden lg:inline-flex"
|
||||
value={summaryCollapsed() ? 'hidden' : 'shown'}
|
||||
onChange={() => setSummaryCollapsed((c) => !c)}
|
||||
aria-label="Charts"
|
||||
options={[
|
||||
{
|
||||
value: 'shown',
|
||||
title: summaryCollapsed() ? 'Show charts' : 'Hide charts',
|
||||
label: (
|
||||
<>
|
||||
<svg
|
||||
class="w-3 h-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</svg>
|
||||
Charts
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
<ChartVisibilityToggleButton
|
||||
collapsed={summaryCollapsed()}
|
||||
onToggle={() => setSummaryCollapsed((collapsed) => !collapsed)}
|
||||
/>
|
||||
</PageControls>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ describe('InfrastructurePageSurface guardrails', () => {
|
|||
);
|
||||
expect(infrastructurePageSurfaceSource).toContain('data-summary-clear-ignore');
|
||||
expect(infrastructurePageSurfaceSource).toContain('GroupedTableModeSegmentedControl');
|
||||
expect(infrastructurePageSurfaceSource).toContain('ChartVisibilityToggleButton');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('aria-label="Charts"');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('FilterSegmentedControl');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('aria-label="Group by"');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('aria-label="Group By"');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain("title: 'Grouped table view'");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue